diff --git a/.devcontainer/base/Dockerfile b/.devcontainer/base/Dockerfile new file mode 100644 index 0000000000..119594bc6f --- /dev/null +++ b/.devcontainer/base/Dockerfile @@ -0,0 +1,16 @@ +FROM mcr.microsoft.com/devcontainers/typescript-node:20-bookworm + +ENV SHELL=/bin/bash \ + DEBIAN_FRONTEND=noninteractive \ + LANG=C.UTF-8 \ + LC_ALL=C.UTF-8 \ + CI=1 \ + PNPM_HOME=/home/node/.local/share/pnpm \ + PATH=/home/node/.local/share/pnpm:$PATH + + +RUN apt-get update && \ + apt-get --no-install-recommends install -y \ + libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libnss3 libxss1 libasound2 libxtst6 xauth xvfb && \ + apt-get clean && \ + corepack enable && COREPACK_ENABLE_DOWNLOAD_PROMPT=0 corepack prepare pnpm@9.1.2 --activate diff --git a/.devcontainer/base/devcontainer.json b/.devcontainer/base/devcontainer.json new file mode 100644 index 0000000000..187e012e67 --- /dev/null +++ b/.devcontainer/base/devcontainer.json @@ -0,0 +1,28 @@ +{ + "$schema": "https://raw.githubusercontent.com/devcontainers/spec/refs/heads/main/schemas/devContainer.schema.json", + "name": "devcontainer", + "dockerComposeFile": "docker-compose.yml", + "service": "devcontainer", + "workspaceFolder": "/workspaces", + "features": { + "ghcr.io/devcontainers/features/go:1": { + "version": "1.24" + }, + "ghcr.io/guiyomh/features/golangci-lint:0": {}, + "ghcr.io/jungaretti/features/make:1": {} + }, + "forwardPorts": [ + 3000, + 3001, + 4200, + 8080 + ], + "onCreateCommand": "pnpm install -g sass@1.64.1", + "customizations": { + "jetbrains": { + "settings": { + "com.intellij:app:HttpConfigurable.use_proxy_pac": true + } + } + } +} diff --git a/.devcontainer/base/docker-compose.yml b/.devcontainer/base/docker-compose.yml new file mode 100644 index 0000000000..079ad69774 --- /dev/null +++ b/.devcontainer/base/docker-compose.yml @@ -0,0 +1,239 @@ +x-build-cache: &build-cache + cache_from: + - type=gha + cache_to: + - type=gha,mode=max + +services: + + devcontainer: + container_name: devcontainer + build: + context: . + <<: *build-cache + volumes: + - ../../:/workspaces:cached + - /tmp/.X11-unix:/tmp/.X11-unix:cached + - home-dir:/home/node:delegated + command: sleep infinity + working_dir: /workspaces + environment: + ZITADEL_DATABASE_POSTGRES_HOST: db + ZITADEL_EXTERNALSECURE: false + + db: + container_name: db + image: postgres:17.0-alpine3.19 + restart: unless-stopped + volumes: + - postgres-data:/var/lib/postgresql/data + environment: + PGUSER: postgres + POSTGRES_PASSWORD: postgres + healthcheck: + test: [ "CMD-SHELL", "pg_isready" ] + interval: "10s" + timeout: "30s" + retries: 5 + start_period: "20s" + ports: + - "5432:5432" + + mock-zitadel: + container_name: mock-zitadel + build: + context: ../../apps/login/integration/core-mock + <<: *build-cache + ports: + - 22220:22220 + - 22222:22222 + + login-integration: + container_name: login-integration + build: + context: ../.. + dockerfile: build/login/Dockerfile + <<: *build-cache + image: "${LOGIN_TAG:-zitadel-login:local}" + env_file: ../../apps/login/.env.test + network_mode: service:devcontainer + environment: + NODE_ENV: test + PORT: 3001 + depends_on: + mock-zitadel: + condition: service_started + + zitadel: + image: "${ZITADEL_TAG:-ghcr.io/zitadel/zitadel:v4.0.0-rc.2}" + container_name: zitadel + command: 'start-from-init --masterkey "MasterkeyNeedsToHave32Characters" --config /zitadel.yaml --steps /zitadel.yaml' + volumes: + - ../../apps/login/acceptance/pat:/pat:delegated + - ../../apps/login/acceptance/zitadel.yaml:/zitadel.yaml:cached + network_mode: service:devcontainer + healthcheck: + test: + - CMD + - /app/zitadel + - ready + - --config + - /zitadel.yaml + depends_on: + db: + condition: "service_healthy" + + configure-login: + container_name: configure-login + restart: no + build: + context: ../../apps/login/acceptance/setup + dockerfile: ../go-command.Dockerfile + <<: *build-cache + entrypoint: "./setup.sh" + network_mode: service:devcontainer + environment: + PAT_FILE: /pat/zitadel-admin-sa.pat + ZITADEL_API_URL: http://localhost:8080 + WRITE_ENVIRONMENT_FILE: /login-env/.env.test.local + SINK_EMAIL_INTERNAL_URL: http://sink:3333/email + SINK_SMS_INTERNAL_URL: http://sink:3333/sms + SINK_NOTIFICATION_URL: http://sink:3333/notification + LOGIN_BASE_URL: http://localhost:3000/ui/v2/login/ + ZITADEL_API_DOMAIN: localhost + ZITADEL_ADMIN_USER: zitadel-admin@zitadel.localhost + volumes: + - ../../apps/login/acceptance/pat:/pat:cached # Read the PAT file from zitadels setup + - ../../apps/login:/login-env:delegated # Write the environment variables file for the login + depends_on: + zitadel: + condition: "service_healthy" + + login-acceptance: + container_name: login + image: "${LOGIN_TAG:-ghcr.io/zitadel/zitadel-login:v4.0.0-rc.2}" + network_mode: service:devcontainer + volumes: + - ../../apps/login/.env.test.local:/env-files/.env:cached + depends_on: + configure-login: + condition: service_completed_successfully + + mock-notifications: + container_name: mock-notifications + build: + context: ../../apps/login/acceptance/sink + dockerfile: ../go-command.Dockerfile + args: + - LOGIN_TEST_ACCEPTANCE_GOLANG_TAG=${LOGIN_TEST_ACCEPTANCE_GOLANG_TAG:-golang:1.24-alpine} + <<: *build-cache + environment: + PORT: '3333' + command: + - -port + - '3333' + - -email + - '/email' + - -sms + - '/sms' + - -notification + - '/notification' + ports: + - "3333:3333" + depends_on: + configure-login: + condition: "service_completed_successfully" + + mock-oidcrp: + container_name: mock-oidcrp + build: + context: ../../apps/login/acceptance/oidcrp + dockerfile: ../go-command.Dockerfile + args: + - LOGIN_TEST_ACCEPTANCE_GOLANG_TAG=${LOGIN_TEST_ACCEPTANCE_GOLANG_TAG:-golang:1.24-alpine} + <<: *build-cache + network_mode: service:devcontainer + environment: + API_URL: 'http://localhost:8080' + API_DOMAIN: 'localhost' + PAT_FILE: '/pat/zitadel-admin-sa.pat' + LOGIN_URL: 'http://localhost:3000/ui/v2/login' + ISSUER: 'http://localhost:8000' + HOST: 'localhost' + PORT: '8000' + SCOPES: 'openid profile email' + volumes: + - ../../apps/login/acceptance/pat:/pat:cached + depends_on: + configure-login: + condition: "service_completed_successfully" + + # mock-oidcop: + # container_name: mock-oidcop + # build: + # context: ../../apps/login/acceptance/idp/oidc + # dockerfile: ../../go-command.Dockerfile + # args: + # - LOGIN_TEST_ACCEPTANCE_GOLANG_TAG=${LOGIN_TEST_ACCEPTANCE_GOLANG_TAG:-golang:1.24-alpine} + # <<: *build-cache + # network_mode: service:devcontainer + # environment: + # API_URL: 'http://localhost:8080' + # API_DOMAIN: 'localhost' + # PAT_FILE: '/pat/zitadel-admin-sa.pat' + # SCHEMA: 'http' + # HOST: 'localhost' + # PORT: "8004" + # volumes: + # - "../apps/login/packages/acceptance/pat:/pat:cached" + # depends_on: + # configure-login: + # condition: "service_completed_successfully" + + mock-samlsp: + container_name: mock-samlsp + build: + context: ../../apps/login/acceptance/samlsp + dockerfile: ../go-command.Dockerfile + args: + - LOGIN_TEST_ACCEPTANCE_GOLANG_TAG=${LOGIN_TEST_ACCEPTANCE_GOLANG_TAG:-golang:1.24-alpine} + <<: *build-cache + network_mode: service:devcontainer + environment: + API_URL: 'http://localhost:8080' + API_DOMAIN: 'localhost' + PAT_FILE: '/pat/zitadel-admin-sa.pat' + LOGIN_URL: 'http://localhost:3000/ui/v2/login' + IDP_URL: 'http://localhost:8080/saml/v2/metadata' + HOST: 'http://localhost:8001' + PORT: '8001' + volumes: + - "../apps/login/packages/acceptance/pat:/pat:cached" + depends_on: + configure-login: + condition: "service_completed_successfully" + # mock-samlidp: + # container_name: mock-samlidp + # build: + # context: ../../apps/login/acceptance/idp/saml + # dockerfile: ../../go-command.Dockerfile + # args: + # - LOGIN_TEST_ACCEPTANCE_GOLANG_TAG=${LOGIN_TEST_ACCEPTANCE_GOLANG_TAG:-golang:1.24-alpine} + # <<: *build-cache + # network_mode: service:devcontainer + # environment: + # API_URL: 'http://localhost:8080' + # API_DOMAIN: 'localhost' + # PAT_FILE: '/pat/zitadel-admin-sa.pat' + # SCHEMA: 'http' + # HOST: 'localhost' + # PORT: "8003" + # volumes: + # - "../apps/login/packages/acceptance/pat:/pat" + # depends_on: + # configure-login: + # condition: "service_completed_successfully" + +volumes: + postgres-data: + home-dir: diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index 5d49f92cf4..0000000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "zitadel", - "dockerComposeFile": "docker-compose.yml", - "service": "devcontainer", - "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", - "features": { - "ghcr.io/devcontainers/features/go:1": { - "version": "1.22" - }, - "ghcr.io/devcontainers/features/node:1": {}, - "ghcr.io/guiyomh/features/golangci-lint:0": {}, - "ghcr.io/devcontainers/features/docker-outside-of-docker:1": {}, - "ghcr.io/devcontainers/features/github-cli:1": {}, - "ghcr.io/jungaretti/features/make:1": {} - }, - "forwardPorts": [ - 3000, - 4200, - 8080 - ], - "onCreateCommand": "npm install -g sass@1.64.1" -} diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml deleted file mode 100644 index cece28632b..0000000000 --- a/.devcontainer/docker-compose.yml +++ /dev/null @@ -1,31 +0,0 @@ -version: '3.8' -services: - devcontainer: - image: mcr.microsoft.com/devcontainers/base:ubuntu - volumes: - - ../..:/workspaces:cached - - /var/run/docker.sock:/var/run/docker.sock - network_mode: service:db - command: sleep infinity - environment: - ZITADEL_DATABASE_POSTGRES_HOST: db - ZITADEL_DATABASE_POSTGRES_PORT: 5432 - ZITADEL_DATABASE_POSTGRES_DATABASE: zitadel - ZITADEL_DATABASE_POSTGRES_USER_USERNAME: zitadel - ZITADEL_DATABASE_POSTGRES_USER_PASSWORD: zitadel - ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE: disable - ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME: postgres - ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD: postgres - ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_MODE: disable - ZITADEL_EXTERNALSECURE: false - db: - image: postgres:latest - restart: unless-stopped - volumes: - - postgres-data:/var/lib/postgresql/data - environment: - PGUSER: postgres - POSTGRES_PASSWORD: postgres - -volumes: - postgres-data: diff --git a/.devcontainer/login-integration-debug/devcontainer.json b/.devcontainer/login-integration-debug/devcontainer.json new file mode 100644 index 0000000000..525b04788e --- /dev/null +++ b/.devcontainer/login-integration-debug/devcontainer.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://raw.githubusercontent.com/devcontainers/spec/refs/heads/main/schemas/devContainer.schema.json", + "name": "login-integration-debug", + "dockerComposeFile": [ + "../base/docker-compose.yml", + "docker-compose.yml" + ], + "service": "login-integration-debug", + "runServices": ["login-integration-debug"], + "workspaceFolder": "/workspaces", + "forwardPorts": [3001], + "onCreateCommand": "pnpm install --recursive", + "postAttachCommand": "pnpm turbo daemon clean; pnpm turbo @zitadel/login#dev test:integration:login:debug", + "customizations": { + "jetbrains": { + "settings": { + "com.intellij:app:HttpConfigurable.use_proxy_pac": true + } + } + } +} diff --git a/.devcontainer/login-integration-debug/docker-compose.yml b/.devcontainer/login-integration-debug/docker-compose.yml new file mode 100644 index 0000000000..11ce02ee7d --- /dev/null +++ b/.devcontainer/login-integration-debug/docker-compose.yml @@ -0,0 +1,9 @@ +services: + login-integration-debug: + extends: + file: ../base/docker-compose.yml + service: devcontainer + container_name: login-integration-debug + depends_on: + mock-zitadel: + condition: service_started diff --git a/.devcontainer/login-integration/devcontainer.json b/.devcontainer/login-integration/devcontainer.json new file mode 100644 index 0000000000..1b7e02df43 --- /dev/null +++ b/.devcontainer/login-integration/devcontainer.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://raw.githubusercontent.com/devcontainers/spec/refs/heads/main/schemas/devContainer.schema.json", + "name": "login-integration", + "dockerComposeFile": [ + "../base/docker-compose.yml" + ], + "service": "devcontainer", + "runServices": ["login-integration"], + "workspaceFolder": "/workspaces", + "forwardPorts": [3001], + "onCreateCommand": "pnpm install --frozen-lockfile --recursive && cd apps/login/packages/integration && pnpm cypress install && pnpm test:integration:login", + "customizations": { + "jetbrains": { + "settings": { + "com.intellij:app:HttpConfigurable.use_proxy_pac": true + } + } + } +} diff --git a/.devcontainer/turbo-lint-unit-debug/devcontainer.json b/.devcontainer/turbo-lint-unit-debug/devcontainer.json new file mode 100644 index 0000000000..19446687a1 --- /dev/null +++ b/.devcontainer/turbo-lint-unit-debug/devcontainer.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://raw.githubusercontent.com/devcontainers/spec/refs/heads/main/schemas/devContainer.schema.json", + "name": "turbo-lint-unit-debug", + "dockerComposeFile": [ + "../base/docker-compose.yml", + "docker-compose.yml" + ], + "service": "turbo-lint-unit-debug", + "runServices": ["turbo-lint-unit-debug"], + "workspaceFolder": "/workspaces", + "forwardPorts": [3001], + "onCreateCommand": "pnpm install --recursive", + "postAttachCommand": "pnpm turbo daemon clean; pnpm turbo watch lint test:unit", + "customizations": { + "jetbrains": { + "settings": { + "com.intellij:app:HttpConfigurable.use_proxy_pac": true + } + } + } +} diff --git a/.devcontainer/turbo-lint-unit-debug/docker-compose.yml b/.devcontainer/turbo-lint-unit-debug/docker-compose.yml new file mode 100644 index 0000000000..a19a0211e5 --- /dev/null +++ b/.devcontainer/turbo-lint-unit-debug/docker-compose.yml @@ -0,0 +1,6 @@ +services: + turbo-lint-unit-debug: + extends: + file: ../base/docker-compose.yml + service: devcontainer + container_name: turbo-lint-unit-debug diff --git a/.devcontainer/turbo-lint-unit/devcontainer.json b/.devcontainer/turbo-lint-unit/devcontainer.json new file mode 100644 index 0000000000..f3c4f64355 --- /dev/null +++ b/.devcontainer/turbo-lint-unit/devcontainer.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://raw.githubusercontent.com/devcontainers/spec/refs/heads/main/schemas/devContainer.schema.json", + "name": "turbo-lint-unit", + "dockerComposeFile": [ + "../base/docker-compose.yml" + ], + "service": "devcontainer", + "runServices": ["devcontainer"], + "workspaceFolder": "/workspaces", + "postStartCommand": "pnpm install --frozen-lockfile --recursive && pnpm turbo lint test:unit", + "customizations": { + "jetbrains": { + "settings": { + "com.intellij:app:HttpConfigurable.use_proxy_pac": true + } + } + } +} diff --git a/.github/dependabot.yml b/.github/dependabot.yml index b7354f3f4a..8fa71ba652 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -22,6 +22,19 @@ updates: commit-message: prefix: chore include: scope +- package-ecosystem: npm + directory: '/login' + open-pull-requests-limit: 3 + schedule: + interval: daily + groups: + prod: + dependency-type: production + dev: + dependency-type: development + ignore: + - dependency-name: "eslint" + versions: [ "9.x" ] - package-ecosystem: gomod groups: go: diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f06c4a959c..b805c99060 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,6 +18,7 @@ permissions: packages: write issues: write pull-requests: write + actions: write jobs: core: @@ -30,6 +31,11 @@ jobs: uses: ./.github/workflows/console.yml with: node_version: "20" + + docs: + uses: ./.github/workflows/docs.yml + with: + node_version: "20" buf_version: "latest" version: @@ -47,6 +53,7 @@ jobs: core_cache_path: ${{ needs.core.outputs.cache_path }} console_cache_path: ${{ needs.console.outputs.cache_path }} version: ${{ needs.version.outputs.version }} + node_version: "20" core-unit-test: needs: core @@ -72,7 +79,7 @@ jobs: with: node_version: "18" buf_version: "latest" - go_lint_version: "v1.64.8" + go_lint_version: "latest" core_cache_key: ${{ needs.core.outputs.cache_key }} core_cache_path: ${{ needs.core.outputs.cache_path }} @@ -86,6 +93,15 @@ jobs: with: build_image_name: "ghcr.io/zitadel/zitadel-build" + login-container: + uses: ./.github/workflows/login-container.yml + permissions: + packages: write + id-token: write + with: + login_build_image_name: "ghcr.io/zitadel/zitadel-login-build" + node_version: "20" + e2e: uses: ./.github/workflows/e2e.yml needs: [compile] @@ -98,7 +114,15 @@ jobs: issues: write pull-requests: write needs: - [version, core-unit-test, core-integration-test, lint, container, e2e] + [ + version, + core-unit-test, + core-integration-test, + lint, + container, + login-container, + e2e, + ] if: ${{ github.event_name == 'workflow_dispatch' }} secrets: GCR_JSON_KEY_BASE64: ${{ secrets.GCR_JSON_KEY_BASE64 }} @@ -109,3 +133,6 @@ jobs: semantic_version: "23.0.7" image_name: "ghcr.io/zitadel/zitadel" google_image_name: "europe-docker.pkg.dev/zitadel-common/zitadel-repo/zitadel" + build_image_name_login: ${{ needs.login-container.outputs.login_build_image }} + image_name_login: "ghcr.io/zitadel/zitadel-login" + google_image_name_login: "europe-docker.pkg.dev/zitadel-common/zitadel-repo/zitadel-login" diff --git a/.github/workflows/compile.yml b/.github/workflows/compile.yml index 519586b9ee..0c36624a46 100644 --- a/.github/workflows/compile.yml +++ b/.github/workflows/compile.yml @@ -18,6 +18,9 @@ on: version: required: true type: string + node_version: + required: true + type: string jobs: executable: @@ -27,70 +30,60 @@ jobs: matrix: goos: [linux, darwin, windows] goarch: [amd64, arm64] - + steps: - - - uses: actions/checkout@v4 - - - uses: actions/cache/restore@v4 - timeout-minutes: 1 - name: restore console - with: - path: ${{ inputs.console_cache_path }} - key: ${{ inputs.console_cache_key }} - fail-on-cache-miss: true - - - uses: actions/cache/restore@v4 - timeout-minutes: 1 - name: restore core - with: - path: ${{ inputs.core_cache_path }} - key: ${{ inputs.core_cache_key }} - fail-on-cache-miss: true - - - uses: actions/setup-go@v5 - with: - go-version-file: 'go.mod' - - - name: compile - timeout-minutes: 5 - run: | - GOOS="${{matrix.goos}}" \ - GOARCH="${{matrix.goarch}}" \ - VERSION="${{ inputs.version }}" \ - COMMIT_SHA="${{ github.sha }}" \ - make compile_pipeline - - - name: create folder - run: | - mkdir zitadel-${{ matrix.goos }}-${{ matrix.goarch }} - mv zitadel zitadel-${{ matrix.goos }}-${{ matrix.goarch }}/ - cp LICENSE zitadel-${{ matrix.goos }}-${{ matrix.goarch }}/ - cp README.md zitadel-${{ matrix.goos }}-${{ matrix.goarch }}/ - tar -czvf zitadel-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz zitadel-${{ matrix.goos }}-${{ matrix.goarch }} - - - uses: actions/upload-artifact@v4 - with: - name: zitadel-${{ matrix.goos }}-${{ matrix.goarch }} - path: zitadel-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz - + - uses: actions/checkout@v4 + - uses: actions/cache/restore@v4 + timeout-minutes: 1 + name: restore console + with: + path: ${{ inputs.console_cache_path }} + key: ${{ inputs.console_cache_key }} + fail-on-cache-miss: true + - uses: actions/cache/restore@v4 + timeout-minutes: 1 + name: restore core + with: + path: ${{ inputs.core_cache_path }} + key: ${{ inputs.core_cache_key }} + fail-on-cache-miss: true + - uses: actions/setup-go@v5 + with: + go-version-file: "go.mod" + - name: compile + timeout-minutes: 5 + run: | + GOOS="${{matrix.goos}}" \ + GOARCH="${{matrix.goarch}}" \ + VERSION="${{ inputs.version }}" \ + COMMIT_SHA="${{ github.sha }}" \ + make compile_pipeline + - name: create folder + run: | + mkdir zitadel-${{ matrix.goos }}-${{ matrix.goarch }} + mv zitadel zitadel-${{ matrix.goos }}-${{ matrix.goarch }}/ + cp LICENSE zitadel-${{ matrix.goos }}-${{ matrix.goarch }}/ + cp README.md zitadel-${{ matrix.goos }}-${{ matrix.goarch }}/ + tar -czvf zitadel-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz zitadel-${{ matrix.goos }}-${{ matrix.goarch }} + - uses: actions/upload-artifact@v4 + with: + name: zitadel-${{ matrix.goos }}-${{ matrix.goarch }} + path: zitadel-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz + checksums: runs-on: ubuntu-latest - needs: executable + needs: [executable] steps: - - - uses: actions/download-artifact@v4 - with: - path: executables - - - name: move files one folder up - run: mv */*.tar.gz . && find . -type d -empty -delete - working-directory: executables - - - run: sha256sum * > checksums.txt - working-directory: executables - - - uses: actions/upload-artifact@v4 - with: - name: checksums.txt - path: executables/checksums.txt + - uses: actions/download-artifact@v4 + with: + path: executables + pattern: 'zitadel-*-*' + - name: move files one folder up + run: mv */*.tar.gz . && find . -type d -empty -delete + working-directory: executables + - run: sha256sum * > checksums.txt + working-directory: executables + - uses: actions/upload-artifact@v4 + with: + name: checksums.txt + path: executables/checksums.txt diff --git a/.github/workflows/console.yml b/.github/workflows/console.yml index 3e77757129..b2f8119190 100644 --- a/.github/workflows/console.yml +++ b/.github/workflows/console.yml @@ -1,19 +1,16 @@ name: Build console -on: +on: workflow_call: inputs: node_version: required: true type: string - buf_version: - required: true - type: string outputs: cache_key: value: ${{ jobs.build.outputs.cache_key }} cache_path: - value: ${{ jobs.build.outputs.cache_path }} + value: ${{ jobs.build.outputs.cache_path }} env: cache_path: console/dist/console @@ -25,38 +22,32 @@ jobs: cache_path: ${{ env.cache_path }} runs-on: ubuntu-latest steps: - - - uses: actions/checkout@v4 - - - uses: actions/cache/restore@v4 - timeout-minutes: 1 - continue-on-error: true - id: cache - with: - key: console-${{ hashFiles('console', 'proto', '!console/dist') }} - restore-keys: | - console- - path: ${{ env.cache_path }} - - - if: ${{ steps.cache.outputs.cache-hit != 'true' }} - uses: bufbuild/buf-setup-action@v1 - with: - github_token: ${{ github.token }} - version: ${{ inputs.buf_version }} - - - if: ${{ steps.cache.outputs.cache-hit != 'true' }} - uses: actions/setup-node@v4 - with: - node-version: ${{ inputs.node_version }} - cache: 'yarn' - cache-dependency-path: console/yarn.lock - - - if: ${{ steps.cache.outputs.cache-hit != 'true' }} - run: make console_build - - - if: ${{ steps.cache.outputs.cache-hit != 'true' }} - uses: actions/cache/save@v4 - with: - path: ${{ env.cache_path }} - key: ${{ steps.cache.outputs.cache-primary-key }} - \ No newline at end of file + - uses: actions/checkout@v4 + - uses: actions/cache/restore@v4 + timeout-minutes: 1 + continue-on-error: true + id: cache + with: + key: console-${{ hashFiles('console', 'proto', '!console/dist') }} + restore-keys: | + console- + path: ${{ env.cache_path }} + - if: ${{ steps.cache.outputs.cache-hit != 'true' }} + uses: pnpm/action-setup@v4 + - if: ${{ steps.cache.outputs.cache-hit != 'true' }} + uses: actions/setup-node@v4 + with: + node-version: ${{ inputs.node_version }} + cache: "pnpm" + cache-dependency-path: pnpm-lock.yaml + - if: ${{ steps.cache.outputs.cache-hit != 'true' }} + name: Install dependencies + run: pnpm install --frozen-lockfile + - if: ${{ steps.cache.outputs.cache-hit != 'true' }} + name: Build console with Turbo + run: pnpm turbo build --filter=./console + - if: ${{ steps.cache.outputs.cache-hit != 'true' }} + uses: actions/cache/save@v4 + with: + path: ${{ env.cache_path }} + key: ${{ steps.cache.outputs.cache-primary-key }} diff --git a/.github/workflows/container.yml b/.github/workflows/container.yml index 33ffd4f6af..f762124e00 100644 --- a/.github/workflows/container.yml +++ b/.github/workflows/container.yml @@ -79,7 +79,7 @@ jobs: context: . cache-from: type=gha cache-to: type=gha,mode=max - file: build/Dockerfile + file: build/zitadel/Dockerfile target: artifact platforms: linux/${{ matrix.arch }} push: true @@ -94,7 +94,7 @@ jobs: context: . cache-from: type=gha cache-to: type=gha,mode=max - file: build/Dockerfile + file: build/zitadel/Dockerfile target: final platforms: linux/${{ matrix.arch }} push: true diff --git a/.github/workflows/core.yml b/.github/workflows/core.yml index 13e7c0dee7..c864c650a7 100644 --- a/.github/workflows/core.yml +++ b/.github/workflows/core.yml @@ -25,6 +25,7 @@ env: internal/api/assets/router.go openapi/v2 pkg/grpc/**/*.pb.* + pkg/grpc/**/*.connect.go jobs: build: diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000000..2f0ac3d7af --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,61 @@ +name: Build docs + +on: + workflow_call: + inputs: + node_version: + required: true + type: string + buf_version: + required: true + type: string + outputs: + cache_key: + value: ${{ jobs.build.outputs.cache_key }} + cache_path: + value: ${{ jobs.build.outputs.cache_path }} + +env: + cache_path: docs/build + +jobs: + build: + outputs: + cache_key: ${{ steps.cache.outputs.cache-primary-key }} + cache_path: ${{ env.cache_path }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/cache/restore@v4 + timeout-minutes: 1 + continue-on-error: true + id: cache + with: + key: docs-${{ hashFiles('docs', 'proto', '!docs/build', '!docs/node_modules', '!docs/protoc-gen-connect-openapi') }} + restore-keys: | + docs- + path: ${{ env.cache_path }} + - if: ${{ steps.cache.outputs.cache-hit != 'true' }} + uses: bufbuild/buf-setup-action@v1 + with: + github_token: ${{ github.token }} + version: ${{ inputs.buf_version }} + - if: ${{ steps.cache.outputs.cache-hit != 'true' }} + uses: pnpm/action-setup@v4 + - if: ${{ steps.cache.outputs.cache-hit != 'true' }} + uses: actions/setup-node@v4 + with: + node-version: ${{ inputs.node_version }} + cache: "pnpm" + cache-dependency-path: pnpm-lock.yaml + - if: ${{ steps.cache.outputs.cache-hit != 'true' }} + name: Install dependencies + run: pnpm install + - if: ${{ steps.cache.outputs.cache-hit != 'true' }} + name: Build docs with Turbo + run: pnpm turbo build --filter=./docs + - if: ${{ steps.cache.outputs.cache-hit != 'true' }} + uses: actions/cache/save@v4 + with: + path: ${{ env.cache_path }} + key: ${{ steps.cache.outputs.cache-primary-key }} diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index e717163507..b9c2159e1c 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -12,44 +12,47 @@ jobs: browser: [firefox, chrome] runs-on: ubuntu-latest steps: - - - name: Checkout Repository + - name: Checkout Repository uses: actions/checkout@v4 - - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v4 with: path: .artifacts name: zitadel-linux-amd64 - - - name: Unpack executable + - name: Unpack executable run: | tar -xvf .artifacts/zitadel-linux-amd64.tar.gz mv zitadel-linux-amd64/zitadel ./zitadel - - - name: Set up QEMU + - name: Set up QEMU uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - - name: Start DB and ZITADEL + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: "pnpm" + cache-dependency-path: pnpm-lock.yaml + - name: Install dependencies + run: pnpm install + - name: Install Cypress binary + run: cd ./e2e && pnpm exec cypress install + - name: Start DB and ZITADEL run: | cd ./e2e ZITADEL_IMAGE=zitadel:local docker compose up --detach --wait - - - name: Cypress run + - name: Cypress run uses: cypress-io/github-action@v6 env: CYPRESS_BASE_URL: http://localhost:8080/ui/console CYPRESS_WEBHOOK_HANDLER_HOST: host.docker.internal - CYPRESS_DATABASE_CONNECTION_URL: 'postgresql://root@localhost:26257/zitadel' + CYPRESS_DATABASE_CONNECTION_URL: "postgresql://root@localhost:26257/zitadel" CYPRESS_BACKEND_URL: http://localhost:8080 with: working-directory: e2e browser: ${{ matrix.browser }} config-file: cypress.config.ts - - - uses: actions/upload-artifact@v4 + install: false + - uses: actions/upload-artifact@v4 if: always() with: name: production-tests-${{ matrix.browser }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index e704bdb146..ce824e94e8 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -20,7 +20,6 @@ on: type: string jobs: - lint-skip: name: lint skip runs-on: ubuntu-latest @@ -36,64 +35,52 @@ jobs: continue-on-error: true if: ${{ github.event_name == 'pull_request' }} steps: - - - uses: actions/checkout@v4 - - - uses: bufbuild/buf-setup-action@v1 - with: - version: ${{ inputs.buf_version }} - github_token: ${{ secrets.GITHUB_TOKEN }} - - - name: lint - uses: bufbuild/buf-lint-action@v1 - - - uses: bufbuild/buf-breaking-action@v1 - with: - against: "https://github.com/${{ github.repository }}.git#branch=${{ github.base_ref }}" + - uses: actions/checkout@v4 + - uses: bufbuild/buf-setup-action@v1 + with: + version: ${{ inputs.buf_version }} + github_token: ${{ secrets.GITHUB_TOKEN }} + - name: lint + uses: bufbuild/buf-lint-action@v1 + - uses: bufbuild/buf-breaking-action@v1 + with: + against: "https://github.com/${{ github.repository }}.git#branch=${{ github.base_ref }}" - console: + turbo-lint-unit: if: ${{ github.event_name == 'pull_request' }} - name: console + name: turbo-lint-unit runs-on: ubuntu-latest steps: - - - name: Checkout - uses: actions/checkout@v4 - - - uses: actions/setup-node@v4 - with: - node-version: ${{ inputs.node_version }} - cache: 'yarn' - cache-dependency-path: console/yarn.lock - - - run: cd console && yarn install - - - name: lint - run: make console_lint + - name: Checkout + uses: actions/checkout@v4 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Run lint and unit tests in dev container + uses: devcontainers/ci@v0.3 + with: + push: never + configFile: .devcontainer/turbo-lint-unit/devcontainer.json + runCmd: echo "Successfully ran lint and unit tests in dev container postStartCommand" core: name: core runs-on: ubuntu-latest if: ${{ github.event_name == 'pull_request' }} steps: - - - name: Checkout - uses: actions/checkout@v4 - - - uses: actions/setup-go@v5 - with: - go-version-file: 'go.mod' - - - uses: actions/cache/restore@v4 - timeout-minutes: 1 - name: restore core - with: - path: ${{ inputs.core_cache_path }} - key: ${{ inputs.core_cache_key }} - fail-on-cache-miss: true - - - uses: golangci/golangci-lint-action@v6 - with: - version: ${{ inputs.go_lint_version }} - github-token: ${{ github.token }} - only-new-issues: true + - name: Checkout + uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version-file: "go.mod" + - uses: actions/cache/restore@v4 + timeout-minutes: 1 + name: restore core + with: + path: ${{ inputs.core_cache_path }} + key: ${{ inputs.core_cache_key }} + fail-on-cache-miss: true + - uses: golangci/golangci-lint-action@v8 + with: + version: ${{ inputs.go_lint_version }} + github-token: ${{ github.token }} + only-new-issues: true diff --git a/.github/workflows/login-container.yml b/.github/workflows/login-container.yml new file mode 100644 index 0000000000..e9a0b97648 --- /dev/null +++ b/.github/workflows/login-container.yml @@ -0,0 +1,70 @@ +name: Login Container + +on: + workflow_call: + inputs: + login_build_image_name: + description: 'The image repository name of the standalone login image' + type: string + required: true + node_version: + required: true + type: string + outputs: + login_build_image: + description: 'The full image tag of the standalone login image' + value: '${{ inputs.login_build_image_name }}:${{ github.sha }}' + +permissions: + packages: write + +env: + default_labels: | + org.opencontainers.image.documentation=https://zitadel.com/docs + org.opencontainers.image.vendor=CAOS AG + org.opencontainers.image.licenses=MIT + +jobs: + login-container: + name: Build Login Container + runs-on: ubuntu-latest + permissions: + packages: write + steps: + - uses: actions/checkout@v4 + - name: Login meta + id: login-meta + uses: docker/metadata-action@v5 + with: + images: ${{ inputs.login_build_image_name }} + labels: ${{ env.default_labels}} + annotations: | + manifest:org.opencontainers.image.licenses=MIT + tags: | + type=sha,prefix=,suffix=,format=long + - name: Login to Docker registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Bake login multi-arch + uses: docker/bake-action@v6 + env: + NODE_VERSION: ${{ inputs.node_version }} + with: + source: . + push: true + provenance: true + sbom: true + targets: login-standalone + set: | + *.cache-from=type=gha + *.cache-to=type=gha,mode=max + files: | + ./apps/login/docker-bake.hcl + ./apps/login/docker-bake-release.hcl + ./docker-bake.hcl + cwd://${{ steps.login-meta.outputs.bake-file }} diff --git a/.github/workflows/ready_for_review.yml b/.github/workflows/ready_for_review.yml index 2ead263dc9..db756633f4 100644 --- a/.github/workflows/ready_for_review.yml +++ b/.github/workflows/ready_for_review.yml @@ -13,7 +13,7 @@ jobs: Please make sure you tick the following checkboxes before marking this Pull Request (PR) as ready for review: - - [ ] I am happy with the code + - [ ] I have reviewed my changes and would approve it - [ ] Documentations and examples are up-to-date - [ ] Logical behavior changes are tested automatically - [ ] No debug or dead code @@ -28,4 +28,4 @@ jobs: owner: context.repo.owner, repo: context.repo.repo, body: content - }) \ No newline at end of file + }) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3e40ae8805..bfbc3d6934 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,6 +15,15 @@ on: google_image_name: required: true type: string + build_image_name_login: + required: true + type: string + image_name_login: + required: true + type: string + google_image_name_login: + required: true + type: string secrets: GCR_JSON_KEY_BASE64: description: 'base64 endcrypted key to connect to Google' @@ -96,6 +105,12 @@ jobs: docker buildx imagetools create \ --tag ${{ inputs.google_image_name }}:${{ needs.version.outputs.version }} \ ${{ inputs.build_image_name }} + docker buildx imagetools create \ + --tag ${{ inputs.image_name_login }}:${{ needs.version.outputs.version }} \ + ${{ inputs.build_image_name_login }} + docker buildx imagetools create \ + --tag ${{ inputs.google_image_name_login }}:${{ needs.version.outputs.version }} \ + ${{ inputs.build_image_name_login }} - name: Publish latest if: ${{ github.ref_name == 'next' }} @@ -106,6 +121,9 @@ jobs: docker buildx imagetools create \ --tag ${{ inputs.image_name }}:latest-debug \ ${{ inputs.build_image_name }}-debug + docker buildx imagetools create \ + --tag ${{ inputs.image_name_login }}:latest \ + ${{ inputs.build_image_name_login }} homebrew-tap: runs-on: ubuntu-22.04 @@ -146,3 +164,56 @@ jobs: GH_TOKEN: ${{ steps.generate-token.outputs.token }} run: | gh workflow -R zitadel/zitadel-charts run bump.yml + + npm-packages: + runs-on: ubuntu-latest + needs: version + if: ${{ github.ref_name == 'next' }} + continue-on-error: true + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install pnpm + uses: pnpm/action-setup@v4 + + - name: Install dependencies + working-directory: login + run: pnpm install --frozen-lockfile + + - name: Create Release Pull Request + uses: changesets/action@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + version: ${{ needs.version.outputs.version }} + cwd: packages + createGithubReleases: false + + login-repo: + runs-on: ubuntu-latest + needs: version + if: ${{ github.ref_name == 'next' }} + continue-on-error: true + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Push Subtree + run: make login_push LOGIN_REMOTE_BRANCH=mirror-zitadel-repo + - name: Create Pull Request + uses: peter-evans/create-pull-request@v7 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commit-message: 'chore: mirror zitadel repo' + branch: mirror-zitadel-repo + title: 'chore: mirror zitadel repo' + body: 'This PR updates the login repository with the latest changes from the zitadel repository.' + base: main + reviewers: | + @peintnermax + @eliobischof diff --git a/.gitignore b/.gitignore index 23469d4209..4c3c877a18 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ # Test binary, build with `go test -c` *.test +!/**/.env.test # Coverage coverage.txt @@ -52,7 +53,8 @@ console/src/app/proto/generated/ !pkg/grpc/protoc/v2/options.pb.go **.proto.mock.go **.pb.*.go -**.gen.go +pkg/**/**.connect.go +**.gen.go openapi/**/*.json /internal/api/assets/authz.go /internal/api/assets/router.go @@ -67,6 +69,7 @@ docs/docs/apis/proto /internal/api/ui/login/static/resources/themes/zitadel/css/zitadel.css /internal/api/ui/login/static/resources/themes/zitadel/css/zitadel.css.map zitadel-*-* +!apps/**/zitadel-*-* # local build/local/*.env @@ -83,7 +86,14 @@ go.work.sum .netlify load-test/node_modules -load-test/yarn-error.log +load-test/pnpm-debug.log load-test/dist load-test/output/* .vercel + +# Turbo +.turbo/ +**/.turbo/ + +# PNPM +.pnpm-store \ No newline at end of file diff --git a/.golangci.yaml b/.golangci.yaml index 1cae359605..712df7d33d 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -1,308 +1,148 @@ -issues: - new-from-rev: main - # Set to 0 to disable. - max-issues-per-linter: 0 - # Set to 0 to disable. - max-same-issues: 0 - exclude-dirs: - - .artifacts - - .backups - - .codecov - - .github - - .keys - - .vscode - - build - - console - - deploy - - docs - - guides - - internal/api/ui/login/static - - openapi - - proto - - tools - +version: "2" run: concurrency: 4 - timeout: 10m - go: '1.22' + go: "1.24" linters: enable: - # Simple linter to check that your code does not contain non-ASCII identifiers [fast: true, auto-fix: false] - asciicheck - # checks whether HTTP response body is closed successfully [fast: false, auto-fix: false] - bodyclose - # check the function whether use a non-inherited context [fast: false, auto-fix: false] - contextcheck - # Computes and checks the cognitive complexity of functions [fast: true, auto-fix: false] - - gocognit - # Checks Go code for unused constants, variables, functions and types [fast: false, auto-fix: false] - - unused - # Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases [fast: false, auto-fix: false] - - errcheck - # Checks that sentinel errors are prefixed with the `Err` and error types are suffixed with the `Error`. [fast: false, auto-fix: false] - errname - # errorlint is a linter for that can be used to find code that will cause problems with the error wrapping scheme introduced in Go 1.13. [fast: false, auto-fix: false] - errorlint - # check exhaustiveness of enum switch statements [fast: false, auto-fix: false] - exhaustive - # Gci controls golang package import order and makes it always deterministic. [fast: true, auto-fix: false] - - gci - # Provides diagnostics that check for bugs, performance and style issues. [fast: false, auto-fix: false] + - gocognit - gocritic - # Linter for Go source code that specializes in simplifying a code [fast: false, auto-fix: false] - - gosimple - # Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string [fast: false, auto-fix: false] - - govet - # Detects when assignments to existing variables are not used [fast: true, auto-fix: false] - - ineffassign - # Finds commonly misspelled English words in comments [fast: true, auto-fix: true] - misspell - # Finds naked returns in functions greater than a specified function length [fast: true, auto-fix: false] - nakedret - # Staticcheck is a go vet on steroids, applying a ton of static analysis checks [fast: false, auto-fix: false] - - staticcheck - # Like the front-end of a Go compiler, parses and type-checks Go code [fast: false, auto-fix: false] - - typecheck - # Reports ill-formed or insufficient nolint directives [fast: true, auto-fix: false] - nolintlint - # Checks for misuse of Sprintf to construct a host with port in a URL. - nosprintfhostport - # checks whether Err of rows is checked successfully in `sql.Rows` [fast: false, auto-fix: false] - rowserrcheck - # Checks that sql.Rows and sql.Stmt are closed. [fast: false, auto-fix: false] - sqlclosecheck - # Remove unnecessary type conversions [fast: false, auto-fix: false] - unconvert - disable: - # Checks for dangerous unicode character sequences [fast: true, auto-fix: false] - # not needed because github does that out of the box - - bidichk - # containedctx is a linter that detects struct contained context.Context field [fast: true, auto-fix: false] - # using contextcheck which looks more active - - containedctx - # checks function and package cyclomatic complexity [fast: false, auto-fix: false] - # not use because gocognit is used - - cyclop - # The owner seems to have abandoned the linter. Replaced by unused. - # deprecated, replaced by unused - - deadcode - # check declaration order and count of types, constants, variables and functions [fast: true, auto-fix: false] - # FUTURE: IMO it sometimes makes sense to declare consts or types after a func - - decorder - # Go linter that checks if package imports are in a list of acceptable packages [fast: false, auto-fix: false] - # not required because of dependabot - - depguard - # Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) [fast: true, auto-fix: false] - # FUTURE: old code is not compatible - - dogsled - # Tool for code clone detection [fast: true, auto-fix: false] - # FUTURE: old code is not compatible - - dupl - # checks for duplicate words in the source code - # not sure if it makes sense - - dupword - # check for two durations multiplied together [fast: false, auto-fix: false] - # FUTURE: checks for accident `1 * time.Second * time.Second` - - durationcheck - # Checks types passed to the json encoding functions. Reports unsupported types and optionally reports occations, where the check for the returned error can be omitted. [fast: false, auto-fix: false] - # FUTURE: use asap, because we use json alot. nice feature is possiblity to check if err check is required - - errchkjson - # execinquery is a linter about query string checker in Query function which reads your Go src files and warning it finds - # FUTURE: might find some errors in sql queries - - execinquery - # Checks if all struct's fields are initialized [fast: false, auto-fix: false] - # deprecated - - exhaustivestruct - # Checks if all structure fields are initialized - # Not all fields have to be initialized - - exhaustruct - # checks for pointers to enclosing loop variables [fast: false, auto-fix: false] - # FUTURE: finds bugs hard to find, could occur much later - - exportloopref - # Forbids identifiers [fast: true, auto-fix: false] - # see no reason. allows to define regexp which are not allowed to use - - forbidigo - # finds forced type assertions [fast: true, auto-fix: false] - # not used because we mostly use `_, _ = a.(int)` - - forcetypeassert - # Tool for detection of long functions [fast: true, auto-fix: false] - # not used because it ignores complexity - - funlen - # check that no global variables exist [fast: true, auto-fix: false] - # We use some global variables which is ok IMO - - gochecknoglobals - # Checks that no init functions are present in Go code [fast: true, auto-fix: false] - # we use inits for the database abstraction - - gochecknoinits - # Finds repeated strings that could be replaced by a constant [fast: true, auto-fix: false] - # FUTURE: might be cool to check - - goconst - # Computes and checks the cyclomatic complexity of functions [fast: true, auto-fix: false] - # not used because cyclop also checks complexity of package - - gocyclo - # Check if comments end in a period [fast: true, auto-fix: true] - # FUTURE: checks if comments are written as specified - - godot - # Tool for detection of FIXME, TODO and other comment keywords [fast: true, auto-fix: false] - # FUTURE: maybe makes sense later. IMO some view todos are ok for later tasks. - - godox - # Golang linter to check the errors handling expressions [fast: false, auto-fix: false] - # Not used in favore of errorlint - - goerr113 - # Gofmt checks whether code was gofmt-ed. By default this tool runs with -s option to check for code simplification [fast: true, auto-fix: true] - # ignored in favor of goimports - - gofmt - # Gofumpt checks whether code was gofumpt-ed. [fast: true, auto-fix: true] - # ignored in favor of goimports - - gofumpt - # Checks is file header matches to pattern [fast: true, auto-fix: false] - # ignored because we don't write licenses as headers - - goheader - # In addition to fixing imports, goimports also formats your code in the same style as gofmt. [fast: true, auto-fix: true] - # ignored in favor of gci - - goimports - #deprecated]: Golint differs from gofmt. Gofmt reformats Go source code, whereas golint prints out style mistakes [fast: false, auto-fix: false] - # ignored in favor of goimports - - golint - # An analyzer to detect magic numbers. [fast: true, auto-fix: false] - # FUTURE: not that critical at the moment - - gomnd - # Manage the use of 'replace', 'retract', and 'excludes' directives in go.mod. [fast: true, auto-fix: false] - # FUTURE: not a problem at the moment - - gomoddirectives - # Allow and block list linter for direct Go module dependencies. This is different from depguard where there are different block types for example version constraints and module recommendations. [fast: true, auto-fix: false] - # FUTURE: maybe interesting because of licenses - - gomodguard - # Checks that printf-like functions are named with `f` at the end [fast: true, auto-fix: false] - # FUTURE: not a problem at the moment - - goprintffuncname - # Inspects source code for security problems [fast: false, auto-fix: false] - # TODO: I think it would be more interesting to integrate into gh code scanning: https://github.com/securego/gosec#integrating-with-code-scanning - - gosec - # An analyzer to analyze expression groups. [fast: true, auto-fix: false] - # I think the groups (vars, consts, imports, ...) we have atm are ok - - grouper - # Checks that your code uses short syntax for if-statements whenever possible [fast: true, auto-fix: false] - # Dont't use its deprecated - - ifshort - # Enforces consistent import aliases [fast: false, auto-fix: false] - # FUTURE: aliasing of imports is more or less consistent - - importas - # A linter that checks the number of methods inside an interface. - # No need at the moment, repository abstraction was removed - - interfacebloat - # A linter that suggests interface types - # Don't use it's archived - - interfacer - # Accept Interfaces, Return Concrete Types [fast: false, auto-fix: false] - # FUTURE: check if no interface is returned - - ireturn - # Reports long lines [fast: true, auto-fix: false] - # FUTURE: would make code more readable - - lll - # Checks key valur pairs for common logger libraries (kitlog,klog,logr,zap). - # FUTURE: useable as soon as we switch logger library - - loggercheck - # maintidx measures the maintainability index of each function. [fast: true, auto-fix: false] - # not used because volume of halstead complexity feels strange as measurement https://en.wikipedia.org/wiki/Halstead_complexity_measures - - maintidx - # Finds slice declarations with non-zero initial length [fast: false, auto-fix: false] - # I would prefer to use https://github.com/alexkohler/prealloc - - makezero - # Reports deeply nested if statements [fast: true, auto-fix: false] - # focus only on if's - - nestif - # Finds the code that returns nil even if it checks that the error is not nil. [fast: false, auto-fix: false] - # FUTURE: check if it is allowed to return nil partially in error catch - - nilerr - # Checks that there is no simultaneous return of `nil` error and an invalid value. [fast: false, auto-fix: false] - # FUTURE: would reduce checks and panics + - errcheck + - govet + - ineffassign + - staticcheck + - unused + - nilnil + disable: + - bidichk + - containedctx + - cyclop + - decorder + - depguard + - dogsled + - dupl + - dupword + - durationcheck + - err113 + - errchkjson + - exhaustruct + - forbidigo + - forcetypeassert + - funlen + - gochecknoglobals + - gochecknoinits + - goconst + - gocyclo + - godot + - godox + - goheader + - gomoddirectives + - gomodguard + - goprintffuncname + - gosec + - grouper + - importas + - interfacebloat + - ireturn + - lll + - loggercheck + - maintidx + - makezero + - mnd + - nestif + - nilerr - nilnil - # nlreturn checks for a new line before return and branch statements to increase code clarity [fast: true, auto-fix: false] - # DISCUSS: IMO the readability of does not always increase using more empty lines - nlreturn - # noctx finds sending http request without context.Context [fast: false, auto-fix: false] - # only interesting if using http - noctx - # Reports all names returns - # Named returns are not allowed which IMO reduces readability of code - nonamedreturns - # detects snake case of variable naming and function name. - # has not been a problem in our code and deprecated - - nosnakecase - # paralleltest detects missing usage of t.Parallel() method in your Go test [fast: true, auto-fix: false] - # FUTURE: will break all of our tests - paralleltest - # Finds slice declarations that could potentially be preallocated [fast: true, auto-fix: false] - # FUTURE: would improve performance - prealloc - # find code that shadows one of Go's predeclared identifiers [fast: true, auto-fix: false] - # FUTURE: checks for overwrites - predeclared - # Check Prometheus metrics naming via promlint [fast: true, auto-fix: false] - # Not interesting at the moment - promlinter - # Checks that package variables are not reassigned - # FUTURE: checks if vars like Err's are reassigned which might break code - reassign - # Fast, configurable, extensible, flexible, and beautiful linter for Go. Drop-in replacement of golint. [fast: false, auto-fix: false] - # Linter aggregator, would allow to use less other linters - revive - # checks for unpinned variables in go programs - # deprecated - - scopelint - # Finds unused struct fields [fast: false, auto-fix: false] - # deprecated, replaced by unused - - structcheck - # Stylecheck is a replacement for golint [fast: false, auto-fix: false] - # we use goimports - - stylecheck - # Checks the struct tags. [fast: true, auto-fix: false] - # FUTURE: would help for new structs - tagliatelle - # tenv is analyzer that detects using os.Setenv instead of t.Setenv since Go1.17 [fast: false, auto-fix: false] - # FUTURE: currently are no env vars set - - tenv - # linter checks if examples are testable (have an expected output) - # FUTURE: as soon as examples are added - testableexamples - # linter that makes you use a separate _test package [fast: true, auto-fix: false] - # don't use because we test some unexported functions - testpackage - # thelper detects golang test helpers without t.Helper() call and checks the consistency of test helpers [fast: false, auto-fix: false] - # FUTURE: nice to improve test quality - thelper - # tparallel detects inappropriate usage of t.Parallel() method in your Go test codes [fast: false, auto-fix: false] - # FUTURE: nice to improve test quality - tparallel - # Reports unused function parameters [fast: false, auto-fix: false] - # DISCUSS: nice idea and would improve code quality, but how to handle false positives? - unparam - # A linter that detect the possibility to use variables/constants from the Go standard library. - # FUTURE: improves code quality - usestdlibvars - # Finds unused global variables and constants [fast: false, auto-fix: false] - # deprecated, replaced by unused - - varcheck - # checks that the length of a variable's name matches its scope [fast: false, auto-fix: false] - # I would not use it because it more or less checks if var lenght matches - varnamelen - # wastedassign finds wasted assignment statements. [fast: false, auto-fix: false] - # FUTURE: would improve code quality (maybe already checked by vet?) - wastedassign - # Tool for detection of leading and trailing whitespace [fast: true, auto-fix: true] - # Not sure if it improves code readability - whitespace - # Checks that errors returned from external packages are wrapped [fast: false, auto-fix: false] - # FUTURE: improves UX because all the errors will be ZITADEL errors - wrapcheck - # Whitespace Linter - Forces you to use empty lines! [fast: true, auto-fix: false] - # FUTURE: improves code quality by allowing and blocking line breaks - wsl -linters-settings: - gci: - sections: - - standard # Standard section: captures all standard packages. - - default # Default section: contains all imports that could not be matched to another section type. - - prefix(github.com/zitadel/zitadel) # Custom section: groups all imports with the specified Prefix. - custom-order: true + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + paths: + - .artifacts + - .backups + - .codecov + - .github + - .keys + - .vscode + - build + - console + - deploy + - docs + - guides + - internal/api/ui/login/static + - openapi + - proto + - tools + - third_party$ + - builtin$ + - examples$ +issues: + max-issues-per-linter: 0 + max-same-issues: 0 + new-from-rev: main +formatters: + enable: + - gci + settings: + gci: + sections: + - standard + - default + - prefix(github.com/zitadel/zitadel) + custom-order: true + exclusions: + generated: lax + paths: + - .artifacts + - .backups + - .codecov + - .github + - .keys + - .vscode + - build + - console + - deploy + - docs + - guides + - internal/api/ui/login/static + - openapi + - proto + - tools + - third_party$ + - builtin$ + - examples$ diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000000..7bd51097f9 --- /dev/null +++ b/.npmrc @@ -0,0 +1,2 @@ +auto-install-peers = true +ignore-scripts = "postman-code-generators" diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000000..0a47c855eb --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +lts/iron \ No newline at end of file diff --git a/ADOPTERS.md b/ADOPTERS.md index 0573099cf9..876d984347 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -23,6 +23,7 @@ If you are using Zitadel, please consider adding yourself as a user with a quick | OpenAIP | [@openaip](https://github.com/openAIP) | Using Zitadel Cloud for everything related to user authentication. | | Smat.io | [@smatio](https://github.com/smatio) - [@lukasver](https://github.com/lukasver) | Zitadel for authentication in cloud applications while offering B2B portfolio management solutions for professional investors | | roclub GmbH | [@holgerson97](https://github.com/holgerson97) | Roclub builds a telehealth application to enable remote MRI/CT examinations. | +| CEEX AG | [@cleanenergyexchange](https://github.com/cleanenergyexchange) | Using Zitadel cloud for our SaaS products that support the sustainabel energy transistion | | Organization Name | contact@example.com | Description of how they use Zitadel | | Individual Name | contact@example.com | Description of how they use Zitadel | diff --git a/API_DESIGN.md b/API_DESIGN.md index 7df13d6588..cdf43a71df 100644 --- a/API_DESIGN.md +++ b/API_DESIGN.md @@ -48,6 +48,52 @@ When creating a new service, start with version `2`, as version `1` is reserved Please check out the structure Buf style guide for more information about the folder and package structure: https://buf.build/docs/best-practices/style-guide/ +### Deprecations + +As a rule of thumb, redundant API methods are deprecated. + +- The proto option `grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation.deprecated` MUST be set to true. +- One or more links to recommended replacement methods MUST be added to the deprecation message as a proto comment above the rpc spec. +- Guidance for switching to the recommended methods for common use cases SHOULD be added as a proto comment above the rpc spec. + +#### Example + +```protobuf +// Delete the user phone +// +// Deprecated: [Update the user's phone field](apis/resources/user_service_v2/user-service-update-user.api.mdx) to remove the phone number. +// +// Delete the phone number of a user. +rpc RemovePhone(RemovePhoneRequest) returns (RemovePhoneResponse) { + option (google.api.http) = { + delete: "/v2/users/{user_id}/phone" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; + responses: { + key: "200" + value: { + description: "OK"; + } + }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } + }; +} +``` + ### Explicitness Make the handling of the API as explicit as possible. Do not make assumptions about the client's knowledge of the system or the API. @@ -56,6 +102,10 @@ Provide clear and concise documentation for the API. Do not rely on implicit fallbacks or defaults if the client does not provide certain parameters. Only use defaults if they are explicitly documented, such as returning a result set for the whole instance if no filter is provided. +Some API calls may create multiple resources such as in the case of `zitadel.org.v2.AddOrganization`, where you can create an organization AND multiple users as admin. +In such cases the response should include **ALL** created resources and their ids. This removes any ambiguity from the users perspective whether or not +the additional resources were created and it also helps in testing. + ### Naming Conventions Names of resources, fields and methods MUST be descriptive and consistent. @@ -69,6 +119,8 @@ For example, use `organization_id` instead of **org_id** or **resource_owner** f #### Resources and Fields +##### Context information in Requests + When a context is required for creating a resource, the context is added as a field to the resource. For example, when creating a new user, the organization's id is required. The `organization_id` is added as a field to the `CreateUserRequest`. @@ -83,9 +135,70 @@ message CreateUserRequest { ``` Only allow providing a context where it is required. The context MUST not be provided if not required. +If the context is required but deferrable, the context can be defaulted. +For example, creating an Authorization without an organization id will default the organization id to the projects resource owner. For example, when retrieving or updating a user, the `organization_id` is not required, since the user can be determined by the user's id. However, it is possible to provide the `organization_id` as a filter to retrieve a list of users of a specific organization. +##### Context information in Responses + +When the action of creation, update or deletion of a resource was successful, the returned response has to include the time of the operation and the generated identifiers. +This is achieved through the addition of a timestamp attribute with the operation as a prefix, and the generated information as separate attributes. + +```protobuf +message SetExecutionResponse { + // The timestamp of the execution set. + google.protobuf.Timestamp set_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; +} + +message CreateTargetResponse { + // The unique identifier of the newly created target. + string id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629012906488334\""; + } + ]; + // The timestamp of the target creation. + google.protobuf.Timestamp creation_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + // Key used to sign and check payload sent to the target. + string signing_key = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"98KmsU67\"" + } + ]; +} + +message UpdateProjectGrantResponse { + // The timestamp of the change of the project grant. + google.protobuf.Timestamp change_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message DeleteProjectGrantResponse { + // The timestamp of the deletion of the project grant. + // Note that the deletion date is only guaranteed to be set if the deletion was successful during the request. + // In case the deletion occurred in a previous request, the deletion date might be empty. + google.protobuf.Timestamp deletion_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} +``` + +##### Global messages + Prevent the creation of global messages that are used in multiple resources unless they always follow the same pattern. Use dedicated fields as described above or create a separate message for the specific context, that is only used in the boundary of the same resource. For example, settings might be set as a default on the instance level, but might be overridden on the organization level. @@ -95,6 +208,10 @@ The same applies to messages that are returned by multiple resources. For example, information about the `User` might be different when managing the user resource itself than when it's returned as part of an authorization or a manager role, where only limited information is needed. +On the other hand, types that always follow the same pattern and are used in multiple resources, such as `IDFilter`, `TimestampFilter` or `InIDsFilter` SHOULD be globalized and reused. + +##### Re-using messages + Prevent reusing messages for the creation and the retrieval of a resource. Returning messages might contain additional information that is not required or even not available for the creation of the resource. What might sound obvious when designing the CreateUserRequest for example, where only an `organization_id` but not the @@ -158,7 +275,7 @@ Additionally, state changes, specific actions or operations that do not fit into The API uses OAuth 2 for authorization. There are corresponding middlewares that check the access token for validity and automatically return an error if the token is invalid. -Permissions grated to the user might be organization specific and can therefore only be checked based on the queried resource. +Permissions granted to the user might be organization specific and can therefore only be checked based on the queried resource. In such case, the API does not check the permissions itself but relies on the checks of the functions that are called by the API. If the permission can be checked by the API itself, e.g. if the permission is instance wide, it can be annotated on the endpoint in the proto file (see below). In any case, the required permissions need to be documented in the [API documentation](#documentation). @@ -186,33 +303,54 @@ In case the permission cannot be checked by the API itself, but all requests nee }; ``` -## Pagination +## Listing resources The API uses pagination for listing resources. The client can specify a limit and an offset to retrieve a subset of the resources. Additionally, the client can specify sorting options to sort the resources by a specific field. -Most listing methods SHOULD provide use the `ListQuery` message to allow the client to specify the limit, offset, and sorting options. -```protobuf +### Pagination -// ListQuery is a general query object for lists to allow pagination and sorting. -message ListQuery { - uint64 offset = 1; - // limit is the maximum amount of objects returned. The default is set to 100 - // with a maximum of 1000 in the runtime configuration. - // If the limit exceeds the maximum configured ZITADEL will throw an error. - // If no limit is present the default is taken. - uint32 limit = 2; - // Asc is the sorting order. If true the list is sorted ascending, if false - // the list is sorted descending. The default is descending. - bool asc = 3; +Most listing methods SHOULD use the `PaginationRequest` message to allow the client to specify the limit, offset, and sorting options. +```protobuf +message ListTargetsRequest { + // List limitations and ordering. + optional zitadel.filter.v2beta.PaginationRequest pagination = 1; + // The field the result is sorted by. The default is the creation date. Beware that if you change this, your result pagination might be inconsistent. + optional TargetFieldName sorting_column = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"TARGET_FIELD_NAME_CREATION_DATE\"" + } + ]; + // Define the criteria to query for. + repeated TargetSearchFilter filters = 3; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + example: "{\"pagination\":{\"offset\":0,\"limit\":0,\"asc\":true},\"sortingColumn\":\"TARGET_FIELD_NAME_CREATION_DATE\",\"filters\":[{\"targetNameFilter\":{\"targetName\":\"ip_allow_list\",\"method\":\"TEXT_FILTER_METHOD_EQUALS\"}},{\"inTargetIdsFilter\":{\"targetIds\":[\"69629023906488334\",\"69622366012355662\"]}}]}"; + }; } ``` -On the corresponding responses the `ListDetails` can be used to return the total count of the resources + +On the corresponding responses the `PaginationResponse` can be used to return the total count of the resources and allow the user to handle their offset and limit accordingly. The API MUST enforce a reasonable maximum limit for the number of resources that can be retrieved and returned in a single request. The default limit is set to 100 and the maximum limit is set to 1000. If the client requests a limit that exceeds the maximum limit, an error is returned. +### Filter method + +All filters in List operations SHOULD provide a method if not already specified by the filters name. +```protobuf +message TargetNameFilter { + // Defines the name of the target to query for. + string target_name = 1 [ + (validate.rules).string = {max_len: 200} + ]; + // Defines which text comparison method used for the name query. + zitadel.filter.v2beta.TextFilterMethod method = 2 [ + (validate.rules).enum.defined_only = true + ]; +} +``` + ## Error Handling The API returns machine-readable errors in the response body. This includes a status code, an error code and possibly @@ -371,4 +509,4 @@ message VerifyEmailRequest{ ]; } -``` \ No newline at end of file +``` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ce8b9aff89..3d6e517a8a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,5 @@ -# Contributing to ZITADEL +# Contributing to Zitadel + ## Introduction @@ -12,24 +13,29 @@ If you want to give an answer or be part of discussions please be kind. Treat ot ## What can I contribute? -For people who are new to ZITADEL: We flag issues which are a good starting point to start contributing. You find them [here](https://github.com/zitadel/zitadel/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) +For people who are new to Zitadel: We flag issues which are a good starting point to start contributing. +You find them [here](https://github.com/zitadel/zitadel/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) +We add the label "good first issue" for problems we think are a good starting point to contribute to Zitadel. -Make ZITADEL more popular and give it a ⭐ +- [Issues for first time contributors](https://github.com/zitadel/zitadel/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) +- [All issues](https://github.com/zitadel/zitadel/issues) -Help shaping the future of ZITADEL: +Help shaping the future of Zitadel: - Join our [chat](https://zitadel.com/chat) and discuss with us or others. - Ask or answer questions in the [issues section](https://github.com/zitadel/zitadel/issues) - Share your thoughts and ideas in the [discussions section](https://github.com/zitadel/zitadel/discussions) +Make Zitadel more popular and give it a ⭐ + +Follow [@zitadel](https://twitter.com/zitadel) on twitter + [Contribute](#how-to-contribute) - [Contribute code](#contribute) -- If you found a mistake on our [docs page](https://zitadel.com/docs) or something is missing please read [the docs section](#contribute-docs) +- If you found a mistake on our [docs page](https://zitadel.com/docs) or something is missing please read [the docs section](contribute-docs) - [Translate](#contribute-internationalization) and improve texts -Follow [@zitadel](https://twitter.com/zitadel) on twitter - ## How to contribute We strongly recommend to [talk to us](https://zitadel.com/contact) before you start contributing to streamline our and your work. @@ -40,6 +46,21 @@ If you are unfamiliar with git have a look at Github's documentation on [creatin Please draft the pull request as soon as possible. Go through the following checklist before you submit the final pull request: +### Components + +The code consists of the following parts: + +| name | description | language | where to find | Development Guide | +| --------------- | -------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | --------------------------------------------------- | -------------------------------------------------- | +| backend | Service that serves the grpc(-web) and RESTful API | [go](https://go.dev) | [API implementation](./internal/api/grpc) | [Contribute to Backend](contribute-backend) | +| API definitions | Specifications of the API | [Protobuf](https://developers.google.com/protocol-buffers) | [./proto/zitadel](./proto/zitadel) | [Contribute to Backend](contribute-backend) | +| console | Frontend the user interacts with after log in | [Angular](https://angular.io), [Typescript](https://www.typescriptlang.org) | [./console](./console) | [Contribute to Frontend](contribute-frontend) | +| login | Modern authentication UI built with Next.js | [Next.js](https://nextjs.org), [React](https://reactjs.org), [TypeScript](https://www.typescriptlang.org) | [./login](./login) | [Contribute to Frontend](contribute-frontend) | +| docs | Project documentation made with docusaurus | [Docusaurus](https://docusaurus.io/) | [./docs](./docs) | [Contribute to Frontend](contribute-frontend) | +| translations | Internationalization files for default languages | YAML | [./console](./console) and [./internal](./internal) | [Contribute Translations](contribute-translations) | + +Please follow the guides to validate and test the code before you contribute. + ### Submit a pull request (PR) 1. [Fork](https://docs.github.com/en/get-started/quickstart/fork-a-repo) the [zitadel/zitadel](https://github.com/zitadel/zitadel) repository on GitHub @@ -104,25 +125,6 @@ Please make sure you cover your changes with tests before marking a Pull Request - [ ] Integration tests ensure that certain commands emit expected events that trigger notifications. - [ ] Integration tests ensure that certain events trigger expected notifications. -## Contribute - -The code consists of the following parts: - -| name | description | language | where to find | -| --------------- | ------------------------------------------------------------------ | --------------------------------------------------------------------------- | -------------------------------------------------- | -| backend | Service that serves the grpc(-web) and RESTful API | [go](https://go.dev) | [API implementation](./internal/api/grpc) | -| console | Frontend the user interacts with after log in | [Angular](https://angular.io), [Typescript](https://www.typescriptlang.org) | [./console](./console) | -| login | Server side rendered frontend the user interacts with during login | [go](https://go.dev), [go templates](https://pkg.go.dev/html/template) | [./internal/api/ui/login](./internal/api/ui/login) | -| API definitions | Specifications of the API | [Protobuf](https://developers.google.com/protocol-buffers) | [./proto/zitadel](./proto/zitadel) | -| docs | Project documentation made with docusaurus | [Docusaurus](https://docusaurus.io/) | [./docs](./docs) | - -Please validate and test the code before you contribute. - -We add the label "good first issue" for problems we think are a good starting point to contribute to ZITADEL. - -- [Issues for first time contributors](https://github.com/zitadel/zitadel/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) -- [All issues](https://github.com/zitadel/zitadel/issues) - ### General Guidelines #### Gender Neutrality and Inclusive Language @@ -143,34 +145,62 @@ Choose alternative words depending on the context. ### API -ZITADEL follows an API first approach. This means all features can not only be accessed via the UI but also via the API. +Zitadel follows an API first approach. This means all features can not only be accessed via the UI but also via the API. The API is designed to be used by different clients, such as web applications, mobile applications, and other services. Therefore, the API is designed to be easy to use, consistent, and reliable. Please check out the dedicated [API guidelines](./API_DESIGN.md) page when contributing to the API. -### Developing ZITADEL with Dev Containers -Follow the instructions provided by your code editor/IDE to initiate the development container. This typically involves opening the "Command Palette" or similar functionality and searching for commands related to "Dev Containers" or "Remote Containers". The quick start guide for VS Code can found [here](https://code.visualstudio.com/docs/devcontainers/containers#_quick-start-open-an-existing-folder-in-a-container) +#### Developing Zitadel with Dev Containers -When you are connected to the container run the following commands to start ZITADEL. +You can use dev containers if you'd like to make sure you have the same development environment like the corresponding GitHub PR checks use. +The following dev containers are available: + +- **.devcontainer/base/devcontainer.json**: Contains everything you need to run whatever you want. +- **.devcontainer/turbo-lint-unit/devcontainer.json**: Runs a dev container that executes frontent linting and unit tests and then exits. This is useful to reproduce the corresponding GitHub PR check. +- **.devcontainer/turbo-lint-unit-debug/devcontainer.json**: Runs a dev container that executes frontent linting and unit tests in watch mode. You can fix the errors right away and have immediate feedback. +- **.devcontainer/login-integration/devcontainer.json**: Runs a dev container that executes login integration tests and then exits. This is useful to reproduce the corresponding GitHub PR check. +- **.devcontainer/login-integration-debug/devcontainer.json**: Runs a dev container that spins up the login in a hot-reloading dev server and executes login integration tests interactively. You can fix the errors right away and have immediate feedback. + +You can also run the GitHub PR checks locally in dev containers without having to connect to a dev container. + + +The following pnpm commands use the [devcontainer CLI](https://github.com/devcontainers/cli/) and exit when the checks are done. +The minimal system requirements are having Docker and the devcontainers CLI installed. +If you don't have the node_modules installed already, you need to install the devcontainers CLI manually. Run `npm i -g @devcontainers/cli`. Alternatively, the [official Microsoft VS Code extension for Dev Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) offers a command `Dev Containers: Install devcontainer CLI` + + +```bash +npm run devcontainer:lint-unit +npm run devcontainer:integration:login +``` + +If you don't have NPM installed, copy and execute the scripts from the package.json directly. + +To connect to a dev container to have full IDE support, follow the instructions provided by your code editor/IDE to initiate the dev container. +This typically involves opening the "Command Palette" or similar functionality and searching for commands related to "Dev Containers" or "Remote Containers". +The quick start guide for VS Code can found [here](https://code.visualstudio.com/docs/devcontainers/containers#_quick-start-open-an-existing-folder-in-a-container) + +For example, to build and run the Zitadel binary in a dev container, connect your IDE to the dev container described in .devcontainer/base/devcontainer.json. +Run the following commands inside the container to start Zitadel. ```bash make compile && ./zitadel start-from-init --masterkey MasterkeyNeedsToHave32Characters --tlsMode disabled ``` -ZITADEL serves traffic as soon as you can see the following log line: +Zitadel serves traffic as soon as you can see the following log line: `INFO[0001] server is listening on [::]:8080` -### Backend/login +## Contribute Backend Code -By executing the commands from this section, you run everything you need to develop the ZITADEL backend locally. +By executing the commands from this section, you run everything you need to develop the Zitadel backend locally. Using [Docker Compose](https://docs.docker.com/compose/), you run a [PostgreSQL](https://www.postgresql.org/download/) on your local machine. -With [make](https://www.gnu.org/software/make/), you build a debuggable ZITADEL binary and run it using [delve](https://github.com/go-delve/delve). +With [make](https://www.gnu.org/software/make/), you build a debuggable Zitadel binary and run it using [delve](https://github.com/go-delve/delve). Then, you test your changes via the console your binary is serving at http://localhost:8080 and by verifying the database. Once you are happy with your changes, you run end-to-end tests and tear everything down. -ZITADEL uses [golangci-lint](https://golangci-lint.run) for code quality checks. Please use [this configuration](.golangci.yaml) when running `golangci-lint`. We recommend to set golangci-lint as linter in your IDE. +Zitadel uses [golangci-lint v2](https://golangci-lint.run) for code quality checks. Please use [this configuration](.golangci.yaml) when running `golangci-lint`. We recommend to set golangci-lint as linter in your IDE. The commands in this section are tested against the following software versions: @@ -199,10 +229,10 @@ make compile > Build the binary: `make compile` You can now run and debug the binary in .artifacts/zitadel/zitadel using your favourite IDE, for example GoLand. -You can test if ZITADEL does what you expect by using the UI at http://localhost:8080/ui/console. +You can test if Zitadel does what you expect by using the UI at http://localhost:8080/ui/console. Also, you can verify the data by running `psql "host=localhost dbname=zitadel sslmode=disable"` and running SQL queries. -#### Run Local Unit Tests +### Run Local Unit Tests To test the code without dependencies, run the unit tests: @@ -210,11 +240,11 @@ To test the code without dependencies, run the unit tests: make core_unit_test ``` -#### Run Local Integration Tests +### Run Local Integration Tests -Integration tests are run as gRPC clients against a running ZITADEL server binary. +Integration tests are run as gRPC clients against a running Zitadel server binary. The server binary is typically [build with coverage enabled](https://go.dev/doc/build-cover). -It is also possible to run a ZITADEL sever in a debugger and run the integrations tests like that. In order to run the server, a database is required. +It is also possible to run a Zitadel sever in a debugger and run the integrations tests like that. In order to run the server, a database is required. In order to prepare the local system, the following will bring up the database, builds a coverage binary, initializes the database and starts the sever. @@ -237,7 +267,7 @@ To run all available integration tests: make core_integration_test_packages ``` -When you change any ZITADEL server code, be sure to rebuild and restart the server before the next test run. +When you change any Zitadel server code, be sure to rebuild and restart the server before the next test run. ```bash make core_integration_server_stop core_integration_server_start @@ -251,69 +281,83 @@ make core_integration_server_stop core_integration_db_down The test binary has the race detector enabled. `core_core_integration_server_stop` checks for any race logs reported by Go and will print them along a `66` exit code when found. Note that the actual race condition may have happened anywhere during the server lifetime, including start, stop or serving gRPC requests during tests. -#### Run Local End-to-End Tests +### Run Local End-to-End Tests To test the whole system, including the console UI and the login UI, run the E2E tests. ```bash # Build the production docker image -export ZITADEL_IMAGE=zitadel:local GOOS=linux +export Zitadel_IMAGE=zitadel:local GOOS=linux make docker_image # If you made changes in the e2e directory, make sure you reformat the files -make console_lint +pnpm turbo lint:fix --filter=e2e # Run the tests -docker compose --file ./e2e/config/host.docker.internal/docker-compose.yaml run --service-ports e2e +docker compose --file ./e2e/docker-compose.yaml run --service-ports e2e ``` When you are happy with your changes, you can cleanup your environment. ```bash # Stop and remove the docker containers for zitadel and the database -docker compose --file ./e2e/config/host.docker.internal/docker-compose.yaml down +docker compose --file ./e2e/docker-compose.yaml down ``` -#### Run Local End-to-End Tests Against Your Dev Server Console +### Run Local End-to-End Tests Against Your Dev Server Console If you also make [changes to the console](#console), you can run the test suite against your locally built backend code and frontend server. -But you will have to install the relevant node dependencies. ```bash -# Install dependencies -(cd ./e2e && npm install) +# Install dependencies (from repository root) +pnpm install # Run the tests interactively -(cd ./e2e && npm run open:golangangular) +pnpm run open:golangangular # Run the tests non-interactively -(cd ./e2e && npm run e2e:golangangular) +pnpm run e2e:golangangular ``` When you are happy with your changes, you can cleanup your environment. ```bash # Stop and remove the docker containers for zitadel and the database -docker compose --file ./e2e/config/host.docker.internal/docker-compose.yaml down +docker compose --file ./e2e/docker-compose.yaml down ``` -### Console +## Contribute Frontend Code -By executing the commands from this section, you run everything you need to develop the console locally. -Using [Docker Compose](https://docs.docker.com/compose/), you run [PostgreSQL](https://www.postgresql.org/download/) and the [latest release of ZITADEL](https://github.com/zitadel/zitadel/releases/latest) on your local machine. -You use the ZITADEL container as backend for your console. -The console is run in your [Node](https://nodejs.org/en/about/) environment using [a local development server for Angular](https://angular.io/cli/serve#ng-serve), so you have fast feedback about your changes. +This repository uses **pnpm** as package manager and **Turbo** for build orchestration. +All frontend packages are managed as a monorepo with shared dependencies and optimized builds: -We use angular-eslint/Prettier for linting/formatting, so please run `yarn lint:fix` before committing. (VSCode users, check out [this ESLint extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) and [this Prettier extension](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) to fix lint and formatting issues in development) +- [apps/login](contribute-login) (depends on packages/zitadel-client and packages/zitadel-proto) +- apps/login/integration +- apps/login/acceptance +- [console](contribute-console) (depends on packages/zitadel-client) +- packages/zitadel-client +- packages/zitadel-proto +- [docs](contribute-docs) -Once you are happy with your changes, you run end-to-end tests and tear everything down. +### Frontend Development Requirements + +The frontend components are run in a [Node](https://nodejs.org/en/about/) environment and are managed using the pnpm package manager and the Turborepo orchestrator. + +> [!INFO] +> Some [dev containers are available](dev-containers) for remote development with docker and pipeline debugging in isolated environments. +> If you don't want to use one of the dev containers, you can develop the frontend components directly on your local machine. +> To do so, proceed with installing the necessary dependencies. + +We use **pnpm** as package manager and **Turbo** for build orchestration. Use angular-eslint/Prettier for linting/formatting. +VSCode users, check out [this ESLint extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) and [this Prettier extension](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) to fix lint and formatting issues during development. The commands in this section are tested against the following software versions: - [Docker version 20.10.17](https://docs.docker.com/engine/install/) -- [Node version v16.17.0](https://nodejs.org/en/download/) -- [npm version 8.18.0](https://docs.npmjs.com/try-the-latest-stable-version-of-npm) -- [Cypress runtime dependencies](https://docs.cypress.io/guides/continuous-integration/introduction#Dependencies) +- [Node version v20.x](https://nodejs.org/en/download/) +- [pnpm version 9.x](https://pnpm.io/installation) + +To run tests with Cypress, ensure you have installed the required [Cypress runtime dependencies](https://docs.cypress.io/guides/continuous-integration/introduction#Dependencies)
Note for WSL2 on Windows 10 @@ -325,97 +369,165 @@ The commands in this section are tested against the following software versions: 4. When starting XLaunch, make sure to disable access control
+### Contribute to Login + +The Login UI is a Next.js application that provides the user interface for authentication flows. +It's located in the `apps/login` directory and uses pnpm and Turbo for development. + +To start developing the login, make sure your system has the [required system dependencies](frontend-dev-requirements) installed. + +#### Development Setup + +```bash +# Start from the root of the repository +# Start the database and Zitadel backend +docker compose --file ./apps/login/acceptance/docker-compose.yaml up --detach zitadel + +# Install dependencies +pnpm install + +# Option 1: Run login development server with Turbo (recommended) +pnpm turbo dev --filter=@zitadel/login + +# Option 2: Build and serve login (production build) +pnpm turbo build --filter=@zitadel/login +cd ./login && pnpm start +``` + +The login UI is available at http://localhost:3000. + +#### Login Architecture + +The login application consists of multiple packages: + +- `@zitadel/login` - Main Next.js application +- `@zitadel/client` - TypeScript client library for Zitadel APIs +- `@zitadel/proto` - Protocol buffer definitions and generated code + +The build process uses Turbo to orchestrate dependencies: + +1. Proto generation (`@zitadel/proto#generate`) +2. Client library build (`@zitadel/client#build`) +3. Login application build (`@zitadel/login#build`) + +#### Pass Quality Checks + +Reproduce the pipelines linting and testing for the login. + +```bash +pnpm turbo quality --filter=./apps/login/* --filter=./packages/* +``` + +Fix the [quality checks](troubleshoot-frontend), add new checks that cover your changes and mark your pull request as ready for review when the pipeline checks pass. + +### Contribute to Console + +To start developing the console, make sure your system has the [required system dependencies](frontend-dev-requirements) installed. +Then, you need to decide which Zitadel instance you would like to target. +- The easiest starting point is to [configure your environment](console-dev-existing-zitadel) to use a [Zitadel cloud](https://zitadel.com) instance. +- Alternatively, you can [start a local Zitadel instance from scratch and develop against it](console-dev-local-zitadel). + +#### Develop against an already running Zitadel instance + +By default, `pnpm dev --filter=console` targets a Zitadel API running at http://localhost:8080. +To change this, export the link to your environment.json in your environment variables. + +```bash +export ENVIRONMENT_JSON_URL=https://my-cloud-instance-abcdef.us1.zitadel.cloud/ui/console/assets/environment.json +``` + +Proceed [with configuring your console redirect URIs](console-redirect). + +#### Develop against a local Zitadel instance from scratch + +By executing the commands from this section, you run everything you need to develop the console locally. +Using [Docker Compose](https://docs.docker.com/compose/), you run [PostgreSQL](https://www.postgresql.org/download/) and the [latest release of Zitadel](https://github.com/zitadel/zitadel/releases/latest) on your local machine. +You use the Zitadel container as backend for your console. + Run the database and the latest backend locally. ```bash -# Change to the console directory -cd ./console - +# Start from the root of the repository # You just need the db and the zitadel services to develop the console against. -docker compose --file ../e2e/docker-compose.yaml up --detach zitadel +docker compose --file ./e2e/docker-compose.yaml up --detach zitadel ``` -When the backend is ready, you have the latest zitadel exposed at http://localhost:8080. -You can now run a local development server with live code reloading at http://localhost:4200. -To allow console access via http://localhost:4200, you have to configure the ZITADEL backend. +When Zitadel accepts traffic, navigate to http://localhost:8080/ui/console/projects?login_hint=zitadel-admin@zitadel.localhost and log in with _Password1!_. -1. Navigate to . -2. When prompted, login with _zitadel-admin@zitadel.localhost_ and _Password1!_ -3. Select the _ZITADEL_ project. +Proceed [with configuring your console redirect URIs](console-redirect). + +#### Configure Console redirect URI + +To allow console access via http://localhost:4200, you have to configure the Zitadel backend. + +1. Navigate to /ui/console/projects in your target Zitadel instance. +3. Select the _Zitadel_ project. 4. Select the _Console_ application. 5. Select _Redirect Settings_ 6. Add _http://localhost:4200/auth/callback_ to the _Redirect URIs_ 7. Add _http://localhost:4200/signedout_ to the _Post Logout URIs_ 8. Select the _Save_ button -You can run the local console development server now. +#### Develop + +Run the local console development server. ```bash -# Install npm dependencies -yarn install +# Install dependencies (from repository root) +pnpm install -# Generate source files from Protos -yarn generate +# Option 1: Run console development server with live reloading and dependency rebuilds +pnpm turbo dev --filter=console -# Start the server -yarn start - -# If you don't want to develop against http://localhost:8080, you can use another environment -ENVIRONMENT_JSON_URL=https://my-cloud-instance-abcdef.zitadel.cloud/ui/console/assets/environment.json yarn start +# Option 2: Build and serve console (production build) +pnpm turbo build --filter=console +pnpm turbo serve --filter=console ``` Navigate to http://localhost:4200/. Make some changes to the source code and see how the browser is automatically updated. -After making changes to the code, you should run the end-to-end-tests. -Open another shell. + +#### Pass Quality Checks + +Reproduce the pipelines linting and testing for the console. ```bash -# Reformat your console code -yarn lint:fix - -# Change to the e2e directory -cd .. && cd e2e/ - -# If you made changes in the e2e directory, make sure you reformat the files here too -npm run lint:fix - -# Install npm dependencies -npm install - -# Run all e2e tests -npm run e2e:angular -- --headed +pnpm turbo quality --filter=console --filter=e2e ``` -You can also open the test suite interactively for fast feedback on specific tests. +Fix the [quality checks](troubleshoot-frontend), add new checks that cover your changes and mark your pull request as ready for review when the pipeline checks pass. + +### Contribute to Docs + +Project documentation is made with Docusaurus and is located under [./docs](./docs). The documentation uses **pnpm** and **Turbo** for development and build processes. + +#### Local Development ```bash -# Run tests interactively -npm run open:angular +# Install dependencies (from repository root) +pnpm install + +# Option 1: Run docs development server with Turbo (recommended) +pnpm turbo dev --filter=zitadel-docs + +# Option 2: Build and serve docs (production build) +pnpm turbo build --filter=zitadel-docs +cd ./docs && pnpm serve ``` -If you also make [changes to the backend code](#backend--login), you can run the test against your locally built backend code and frontend server +The docs build process automatically: -```bash -npm run open:golangangular -npm run e2e:golangangular -``` +1. Downloads required protoc plugins +2. Generates gRPC documentation from proto files +3. Generates API documentation from OpenAPI specs +4. Copies configuration files +5. Builds the Docusaurus site -When you are happy with your changes, you can format your code and cleanup your environment +#### Local testing -```bash -# Stop and remove the docker containers for zitadel and the database -docker compose down -``` +The documentation server will be available at http://localhost:3000 with live reload for fast development feedback. -## Contribute docs - -Project documentation is made with docusaurus and is located under [./docs](./docs). - -### Local testing - -Please refer to the [README](./docs/README.md) for more information and local testing. - -### Style guide +#### Style guide - **Code with variables**: Make sure that code snippets can be used by setting environment variables, instead of manually replacing a placeholder. - **Embedded files**: When embedding mdx files, make sure the template ist prefixed by "\_" (lowdash). The content will be rendered inside the parent page, but is not accessible individually (eg, by search). @@ -431,14 +543,54 @@ The style guide covers a lot of material, so their [highlights](https://develope - Use active voice: make clear who's performing the action. - Use descriptive link text. -### Docs pull request +#### Docs pull request When making a pull request use `docs(): ` as title for the semantic release. Scope can be left empty (omit the brackets) or refer to the top navigation sections. -## Contribute internationalization +#### Pass Quality Checks -ZITADEL loads translations from four files: +Reproduce the pipelines linting checks for the docs. + +```bash +pnpm turbo quality --filter=docs +``` + +Fix the [quality checks](troubleshoot-frontend), add new checks that cover your changes and mark your pull request as ready for review when the pipeline checks pass. + +### Troubleshoot Frontend Quality Checks + +To debug and fix failing tasks, execute them individually using the `--filter` flag. + +We recommend to use [one of the dev containers](dev-containers) to reproduce pipeline issues. + +```bash +# to reproduce linting error in the console: +pnpm lint --filter=console +# To fix them: +pnpm lint:fix --filter=console +``` + +More tasks that are runnable on-demand. +Some tasks have variants like `pnpm test:e2e:angulargolang`, +others support arguments and flags like `pnpm test:integration run --spec apps/login/integration/integration/login.cy.ts`. +For the turbo commands, check your options with `pnpm turbo --help` + +| Command | Description | Example | +| ------------------------- | -------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `pnpm turbo run generate` | Generate stubs from Proto files | Generate API docs: `pnpm turbo run generate --filter zitadel-docs` | +| `pnpm turbo build` | Build runnable JavaScript code | Regenerate the proto stubs and build the @zitadel/client package: `pnpm turbo build --filter @zitadel/client` | +| `pnpm turbo quality` | Reproduce the pipeline quality checks | Run login-related quality checks `pnpm turbo quality --filter './apps/login/*' --filter './packages/*'` | +| `pnpm turbo lint` | Check linting issues | Check login-related linting issues for differences with main `pnpm turbo lint --filter=[main...HEAD] --filter .'/apps/login/**/*' --filter './packages/*'` | +| `pnpm turbo lint:fix` | Fix linting issues | Fix console-relevant linting issues `pnpm turbo lint:fix --filter console --filter './packages/*' --filter zitadel-e2e` | +| `pnpm turbo test:unit` | Run unit tests. Rerun on file changes | Run unit tests in all packages in and watch for file changes `pnpm turbo watch test:unit` | +| `pnpm turbo test:e2e` | Run the Cypress CLI for console e2e tests | Test interactively against the console in a local dev server and Zitadel in a container: `pnpm turbo test:e2e:angular open` | +| `pnpm turbo down` | Remove containers and volumes | Shut down containers from the integration test setup `pnpm turbo down` | +| `pnpm turbo clean` | Remove downloaded dependencies and other generated files | Remove generated docs `pnpm turbo clean --filter zitadel-docs` | + +## Contribute Translations + +Zitadel loads translations from four files: - [Console texts](./console/src/assets/i18n) - [Login interface](./internal/api/ui/login/static/i18n) @@ -449,13 +601,14 @@ You may edit the texts in these files or create a new file for additional langua Please make sure that the languages within the files remain in their own language, e.g. German must always be `Deutsch. If you have added support for a new language, please also ensure that it is added in the list of languages in all the other language files. -You also have to add some changes to the following files: +You also have to add some changes to the following files: + - [Register Local File](./console/src/app/app.module.ts) - [Add Supported Language](./console/src/app/utils/language.ts) - [Customized Text Docs](./docs/docs/guides/manage/customize/texts.md) - [Add language option](./internal/api/ui/login/static/templates/external_not_found_option.html) -## Want to start ZITADEL? +## Want to start Zitadel? You can find an installation guide for all the different environments here: [https://zitadel.com/docs/self-hosting/deploy/overview](https://zitadel.com/docs/self-hosting/deploy/overview) @@ -466,14 +619,14 @@ You can find an installation guide for all the different environments here: ## Product management -The ZITADEL Team works with an agile product management methodology. +The Zitadel Team works with an agile product management methodology. You can find all the issues prioritized and ordered in the [product board](https://github.com/orgs/zitadel/projects/2/views/1). ### Sprint We want to deliver a new release every second week. So we plan everything in two-week sprints. Each Tuesday we estimate new issues and on Wednesday the last sprint will be reviewed and the next one will be planned. -After a sprint ends a new version of ZITADEL will be released, and publish to [ZITADEL Cloud](https://zitadel.cloud) the following Monday. +After a sprint ends a new version of Zitadel will be released, and publish to [Zitadel Cloud](https://zitadel.cloud) the following Monday. If there are some critical or urgent issues we will have a look at it earlier, than the two weeks. To show the community the needed information, each issue gets attributes and labels. @@ -493,15 +646,16 @@ The state should reflect the progress of the issue and what is going on right no - **🔖 Ready**: The issue is ready to take into a sprint. Difference to "prioritized..." is that the complexity is defined by the team. - **📋 Sprint backlog**: The issue is scheduled for the current sprint. - **🏗 In progress**: Someone is working on this issue right now. The issue will get an assignee as soon as it is in progress. +- **❌ Blocked**: The issue is blocked until another issue is resolved/done. - **👀 In review**: The issue is in review. Please add someone to review your issue or let us know that it is ready to review with a comment on your pull request. - **✅ Done**: The issue is implemented and merged to main. #### Priority -Priority shows you the priority the ZITADEL team has given this issue. In general the higher the demand from customers and community for the feature, the higher the priority. +Priority shows you the priority the Zitadel team has given this issue. In general the higher the demand from customers and community for the feature, the higher the priority. - **🌋 Critical**: This is a security issue or something that has to be fixed urgently, because the software is not usable or highly vulnerable. -- **🏔 High**: These are the issues the ZITADEL team is currently focusing on and will be implemented as soon as possible. +- **🏔 High**: These are the issues the Zitadel team is currently focusing on and will be implemented as soon as possible. - **🏕 Medium**: After all the high issues are done these will be next. - **🏝 Low**: This is low in priority and will probably not be implemented in the next time or just if someone has some time in between. @@ -516,18 +670,18 @@ Everything that is higher than 8 should be split in smaller parts. There are a few general labels that don't belong to a specific category. -- **good first issue**: This label shows contributors, that it is an easy entry point to start developing on ZITADEL. -- **help wanted**: The author is seeking help on this topic, this may be from an internal ZITADEL team member or external contributors. +- **good first issue**: This label shows contributors, that it is an easy entry point to start developing on Zitadel. +- **help wanted**: The author is seeking help on this topic, this may be from an internal Zitadel team member or external contributors. #### Category -The category shows which part of ZITADEL is affected. +The category shows which part of Zitadel is affected. - **category: backend**: The backend includes the APIs, event store, command and query side. This is developed in golang. - **category: ci**: ci is all about continues integration and pipelines. -- **category: design**: All about the ux/ui of ZITADEL +- **category: design**: All about the ux/ui of Zitadel - **category: docs**: Adjustments or new documentations, this can be found in the docs folder. -- **category: frontend**: The frontend concerns on the one hand the ZITADEL management console (Angular) and on the other hand the login (gohtml) +- **category: frontend**: The frontend concerns on the one hand the Zitadel management console (Angular) and on the other hand the login (gohtml) - **category: infra**: Infrastructure does include many different parts. E.g Terraform-provider, docker, metrics, etc. - **category: translation**: Everything concerning translations or new languages diff --git a/LICENSING.md b/LICENSING.md index 9cad2082f8..ca4717afa5 100644 --- a/LICENSING.md +++ b/LICENSING.md @@ -18,6 +18,14 @@ The following files and directories, including their subdirectories, are license proto/ ``` + +The following files and directories, including their subdirectories, are licensed under the [MIT License](https://opensource.org/license/mit/): + +``` +login/ +clients/ +``` + ## Community Contributions To maintain a clear licensing structure and facilitate community contributions, all contributions must be licensed under the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0) to be accepted. By submitting a contribution, you agree to this licensing. diff --git a/Makefile b/Makefile index b5145cef3d..ad561ab725 100644 --- a/Makefile +++ b/Makefile @@ -12,12 +12,22 @@ ZITADEL_MASTERKEY ?= MasterkeyNeedsToHave32Characters export GOCOVERDIR ZITADEL_MASTERKEY +LOGIN_REMOTE_NAME := login +LOGIN_REMOTE_URL ?= https://github.com/zitadel/typescript.git +LOGIN_REMOTE_BRANCH ?= main + .PHONY: compile compile: core_build console_build compile_pipeline .PHONY: docker_image -docker_image: compile - DOCKER_BUILDKIT=1 docker build -f build/Dockerfile -t $(ZITADEL_IMAGE) . +docker_image: + @if [ ! -f ./zitadel ]; then \ + echo "Compiling zitadel binary"; \ + $(MAKE) compile; \ + else \ + echo "Reusing precompiled zitadel binary"; \ + fi + DOCKER_BUILDKIT=1 docker build -f build/zitadel/Dockerfile -t $(ZITADEL_IMAGE) . .PHONY: compile_pipeline compile_pipeline: console_move @@ -68,12 +78,13 @@ core_grpc_dependencies: go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@v2.22.0 # https://pkg.go.dev/github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2?tab=versions go install github.com/envoyproxy/protoc-gen-validate@v1.1.0 # https://pkg.go.dev/github.com/envoyproxy/protoc-gen-validate?tab=versions go install github.com/bufbuild/buf/cmd/buf@v1.45.0 # https://pkg.go.dev/github.com/bufbuild/buf/cmd/buf?tab=versions + go install connectrpc.com/connect/cmd/protoc-gen-connect-go@v1.18.1 # https://pkg.go.dev/connectrpc.com/connect/cmd/protoc-gen-connect-go?tab=versions .PHONY: core_api core_api: core_api_generator core_grpc_dependencies buf generate mkdir -p pkg/grpc - cp -r .artifacts/grpc/github.com/zitadel/zitadel/pkg/grpc/* pkg/grpc/ + cp -r .artifacts/grpc/github.com/zitadel/zitadel/pkg/grpc/** pkg/grpc/ mkdir -p openapi/v2/zitadel cp -r .artifacts/grpc/zitadel/ openapi/v2/zitadel @@ -86,18 +97,11 @@ console_move: .PHONY: console_dependencies console_dependencies: - cd console && \ - yarn install --immutable - -.PHONY: console_client -console_client: - cd console && \ - yarn generate + npx pnpm install --frozen-lockfile --filter=./console .PHONY: console_build -console_build: console_dependencies console_client - cd console && \ - yarn build +console_build: console_dependencies + npx pnpm turbo build --filter=./console .PHONY: clean clean: @@ -112,7 +116,7 @@ core_unit_test: .PHONY: core_integration_db_up core_integration_db_up: - docker compose -f internal/integration/config/docker-compose.yaml up --pull always --wait cache + docker compose -f internal/integration/config/docker-compose.yaml up --pull always --wait cache postgres .PHONY: core_integration_db_down core_integration_db_down: @@ -155,8 +159,7 @@ core_integration_test: core_integration_server_start core_integration_test_packa .PHONY: console_lint console_lint: - cd console && \ - yarn lint + npx pnpm turbo lint --filter=./console .PHONY: core_lint core_lint: @@ -165,3 +168,29 @@ core_lint: --config ./.golangci.yaml \ --out-format=github-actions \ --concurrency=$$(getconf _NPROCESSORS_ONLN) + +.PHONY: login_pull +login_pull: login_ensure_remote + @echo "Pulling changes from the 'apps/login' subtree on remote $(LOGIN_REMOTE_NAME) branch $(LOGIN_REMOTE_BRANCH)" + git fetch $(LOGIN_REMOTE_NAME) $(LOGIN_REMOTE_BRANCH) + git merge -s ours --allow-unrelated-histories $(LOGIN_REMOTE_NAME)/$(LOGIN_REMOTE_BRANCH) -m "Synthetic merge to align histories" + git push + +.PHONY: login_push +login_push: login_ensure_remote + @echo "Pushing changes to the 'apps/login' subtree on remote $(LOGIN_REMOTE_NAME) branch $(LOGIN_REMOTE_BRANCH)" + git subtree split --prefix=apps/login -b login-sync-tmp + git checkout login-sync-tmp + git fetch $(LOGIN_REMOTE_NAME) main + git merge -s ours --allow-unrelated-histories $(LOGIN_REMOTE_NAME)/main -m "Synthetic merge to align histories" + git push $(LOGIN_REMOTE_NAME) login-sync-tmp:$(LOGIN_REMOTE_BRANCH) + git checkout - + git branch -D login-sync-tmp + +login_ensure_remote: + @if ! git remote get-url $(LOGIN_REMOTE_NAME) > /dev/null 2>&1; then \ + echo "Adding remote $(LOGIN_REMOTE_NAME)"; \ + git remote add $(LOGIN_REMOTE_NAME) $(LOGIN_REMOTE_URL); \ + else \ + echo "Remote $(LOGIN_REMOTE_NAME) already exists."; \ + fi diff --git a/README.md b/README.md index 285e50964c..3d33e20e57 100644 --- a/README.md +++ b/README.md @@ -193,7 +193,7 @@ Use [Console](https://zitadel.com/docs/guides/manage/console/overview) or our [A ### Login V2 Check out our new Login V2 version in our [typescript repository](https://github.com/zitadel/typescript) or in our [documentation](https://zitadel.com/docs/guides/integrate/login/hosted-login#hosted-login-version-2-beta) -[![New Login Showcase](https://github.com/user-attachments/assets/cb5c5212-128b-4dc9-b11d-cabfd3f73e26)] +![New Login Showcase](https://github.com/user-attachments/assets/cb5c5212-128b-4dc9-b11d-cabfd3f73e26) ## Security diff --git a/apps/login/.dockerignore b/apps/login/.dockerignore new file mode 100644 index 0000000000..4e64fd495f --- /dev/null +++ b/apps/login/.dockerignore @@ -0,0 +1,21 @@ +* + +!constants +!scripts +!src +!public +!locales +!next.config.mjs +!next-env-vars.d.ts +!next-env.d.ts +!tailwind.config.js +!tsconfig.json +!package.json +!pnpm-lock.yaml + +**/*.md +**/*.png +**/node_modules +**/.turbo +**/*.test.ts +**/*.test.tsx \ No newline at end of file diff --git a/apps/login/.env.test b/apps/login/.env.test new file mode 100644 index 0000000000..134fdb5669 --- /dev/null +++ b/apps/login/.env.test @@ -0,0 +1,5 @@ +NEXT_PUBLIC_BASE_PATH="/ui/v2/login" +ZITADEL_API_URL=http://mock-zitadel:22222 +ZITADEL_SERVICE_USER_TOKEN="yolo" +EMAIL_VERIFICATION=true +DEBUG=true diff --git a/apps/login/.eslintrc.cjs b/apps/login/.eslintrc.cjs new file mode 100644 index 0000000000..d704a7f0c3 --- /dev/null +++ b/apps/login/.eslintrc.cjs @@ -0,0 +1,24 @@ +module.exports = { + parser: "@typescript-eslint/parser", + extends: ["next", "prettier"], + plugins: ["@typescript-eslint"], + rules: { + "@next/next/no-html-link-for-pages": "off", + "@next/next/no-img-element": "off", + "react/no-unescaped-entities": "off", + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": ["error", { + argsIgnorePattern: "^_" , + varsIgnorePattern: "^_" , + }], + "no-undef": "off", + }, + parserOptions: { + ecmaVersion: "latest", + sourceType: "module", + ecmaFeatures: { + jsx: true, + }, + project: "./tsconfig.json", + }, +}; diff --git a/apps/login/.github/ISSUE_TEMPLATE/bug.yaml b/apps/login/.github/ISSUE_TEMPLATE/bug.yaml new file mode 100644 index 0000000000..2764c1a365 --- /dev/null +++ b/apps/login/.github/ISSUE_TEMPLATE/bug.yaml @@ -0,0 +1,63 @@ +name: 🐛 Bug Report +description: "Create a bug report to help us improve ZITADEL Typescript Library." +title: "[Bug]: " +labels: ["bug"] +body: +- type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! +- type: checkboxes + id: preflight + attributes: + label: Preflight Checklist + options: + - label: + I could not find a solution in the documentation, the existing issues or discussions + required: true + - label: + I have joined the [ZITADEL chat](https://zitadel.com/chat) + validations: + required: true +- type: input + id: version + attributes: + label: Version + description: Which version of ZITADEL Typescript Library are you using. +- type: textarea + id: impact + attributes: + label: Describe the problem caused by this bug + description: A clear and concise description of the problem you have and what the bug is. + validations: + required: true +- type: textarea + id: reproduce + attributes: + label: To reproduce + description: Steps to reproduce the behaviour + placeholder: | + Steps to reproduce the behavior: + 1. ... + validations: + required: true +- type: textarea + id: screenshots + attributes: + label: Screenshots + description: If applicable, add screenshots to help explain your problem. +- type: textarea + id: expected + attributes: + label: Expected behavior + description: A clear and concise description of what you expected to happen. +- type: textarea + id: config + attributes: + label: Relevant Configuration + description: Add any relevant configurations that could help us. Make sure to redact any sensitive information. +- type: textarea + id: additional + attributes: + label: Additional Context + description: Please add any other infos that could be useful. diff --git a/apps/login/.github/ISSUE_TEMPLATE/config.yml b/apps/login/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..7e690b9344 --- /dev/null +++ b/apps/login/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,4 @@ +blank_issues_enabled: true +contact_links: + - name: 💬 ZITADEL Community Chat + url: https://zitadel.com/chat diff --git a/apps/login/.github/ISSUE_TEMPLATE/docs.yaml b/apps/login/.github/ISSUE_TEMPLATE/docs.yaml new file mode 100644 index 0000000000..04c1c0cdb1 --- /dev/null +++ b/apps/login/.github/ISSUE_TEMPLATE/docs.yaml @@ -0,0 +1,30 @@ +name: 📄 Documentation +description: Create an issue for missing or wrong documentation. +labels: ["docs"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this issue. + - type: checkboxes + id: preflight + attributes: + label: Preflight Checklist + options: + - label: + I could not find a solution in the existing issues, docs, nor discussions + required: true + - label: + I have joined the [ZITADEL chat](https://zitadel.com/chat) + - type: textarea + id: docs + attributes: + label: Describe the docs your are missing or that are wrong + placeholder: As a [type of user], I want [some goal] so that [some reason]. + validations: + required: true + - type: textarea + id: additional + attributes: + label: Additional Context + description: Please add any other infos that could be useful. diff --git a/apps/login/.github/ISSUE_TEMPLATE/improvement.yaml b/apps/login/.github/ISSUE_TEMPLATE/improvement.yaml new file mode 100644 index 0000000000..cfe79d407b --- /dev/null +++ b/apps/login/.github/ISSUE_TEMPLATE/improvement.yaml @@ -0,0 +1,54 @@ +name: 🛠️ Improvement +description: "Create an new issue for an improvment in ZITADEL" +labels: ["improvement"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this improvement request + - type: checkboxes + id: preflight + attributes: + label: Preflight Checklist + options: + - label: + I could not find a solution in the existing issues, docs, nor discussions + required: true + - label: + I have joined the [ZITADEL chat](https://zitadel.com/chat) + - type: textarea + id: problem + attributes: + label: Describe your problem + description: Please describe your problem this improvement is supposed to solve. + placeholder: Describe the problem you have + validations: + required: true + - type: textarea + id: solution + attributes: + label: Describe your ideal solution + description: Which solution do you propose? + placeholder: As a [type of user], I want [some goal] so that [some reason]. + validations: + required: true + - type: input + id: version + attributes: + label: Version + description: Which version of the typescript library are you using. + - type: dropdown + id: environment + attributes: + label: Environment + description: How do you use ZITADEL? + options: + - ZITADEL Cloud + - Self-hosted + validations: + required: true + - type: textarea + id: additional + attributes: + label: Additional Context + description: Please add any other infos that could be useful. diff --git a/apps/login/.github/ISSUE_TEMPLATE/proposal.yaml b/apps/login/.github/ISSUE_TEMPLATE/proposal.yaml new file mode 100644 index 0000000000..cd9ff66972 --- /dev/null +++ b/apps/login/.github/ISSUE_TEMPLATE/proposal.yaml @@ -0,0 +1,54 @@ +name: 💡 Proposal / Feature request +description: "Create an issue for a feature request/proposal." +labels: ["enhancement"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this proposal / feature reqeust + - type: checkboxes + id: preflight + attributes: + label: Preflight Checklist + options: + - label: + I could not find a solution in the existing issues, docs, nor discussions + required: true + - label: + I have joined the [ZITADEL chat](https://zitadel.com/chat) + - type: textarea + id: problem + attributes: + label: Describe your problem + description: Please describe your problem this proposal / feature is supposed to solve. + placeholder: Describe the problem you have. + validations: + required: true + - type: textarea + id: solution + attributes: + label: Describe your ideal solution + description: Which solution do you propose? + placeholder: As a [type of user], I want [some goal] so that [some reason]. + validations: + required: true + - type: input + id: version + attributes: + label: Version + description: Which version of the Typescript Library are you using. + - type: dropdown + id: environment + attributes: + label: Environment + description: How do you use ZITADEL? + options: + - ZITADEL Cloud + - Self-hosted + validations: + required: true + - type: textarea + id: additional + attributes: + label: Additional Context + description: Please add any other infos that could be useful. diff --git a/apps/login/.github/custom-i18n.png b/apps/login/.github/custom-i18n.png new file mode 100644 index 0000000000..2306e62f87 Binary files /dev/null and b/apps/login/.github/custom-i18n.png differ diff --git a/apps/login/.github/dependabot.example.yml b/apps/login/.github/dependabot.example.yml new file mode 100644 index 0000000000..8f3906c179 --- /dev/null +++ b/apps/login/.github/dependabot.example.yml @@ -0,0 +1,21 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: '/' + open-pull-requests-limit: 1 + schedule: + interval: 'daily' + + - package-ecosystem: npm + directory: '/' + open-pull-requests-limit: 3 + schedule: + interval: daily + groups: + prod: + dependency-type: production + dev: + dependency-type: development + ignore: + - dependency-name: "eslint" + versions: [ "9.x" ] diff --git a/apps/login/.github/pull_request_template.md b/apps/login/.github/pull_request_template.md new file mode 100644 index 0000000000..138d4919af --- /dev/null +++ b/apps/login/.github/pull_request_template.md @@ -0,0 +1,13 @@ +### Definition of Ready + +- [ ] I am happy with the code +- [ ] Short description of the feature/issue is added in the pr description +- [ ] PR is linked to the corresponding user story +- [ ] Acceptance criteria are met +- [ ] All open todos and follow ups are defined in a new ticket and justified +- [ ] Deviations from the acceptance criteria and design are agreed with the PO and documented. +- [ ] Vitest unit tests ensure that components produce expected outputs on different inputs. +- [ ] Cypress integration tests ensure that login app pages work as expected on good and bad user inputs, ZITADEL responses or IDP redirects. The ZITADEL API is mocked, IDP redirects are simulated. +- [ ] Playwright acceptances tests ensure that the happy paths of common user journeys work as expected. The ZITADEL API is not mocked but IDP redirects are simulated. +- [ ] No debug or dead code +- [ ] My code has no repetitions diff --git a/apps/login/.github/workflows/close_pr.yml b/apps/login/.github/workflows/close_pr.yml new file mode 100644 index 0000000000..90f92eff55 --- /dev/null +++ b/apps/login/.github/workflows/close_pr.yml @@ -0,0 +1,39 @@ +name: Auto-close PRs and guide to correct repo + +on: + workflow_dispatch: + pull_request_target: + types: [opened] + +jobs: + auto-close: + runs-on: ubuntu-latest + if: github.repository_id == '622995060' && github.ref_name != 'mirror-zitadel-repo' + steps: + - name: Comment and close PR + uses: actions/github-script@v7 + with: + script: | + const message = ` + 👋 **Thanks for your contribution @${{ github.event.pull_request.user.login }}!** + + This repository \`${{ github.repository }}\` is a read-only mirror of the git subtree at [\`zitadel/zitadel/login\`](https://github.com/zitadel/zitadel). + Therefore, we close this pull request automatically. + + Your changes are not lost. Submitting them to the main repository is easy: + 1. [Fork zitadel/zitadel](https://github.com/zitadel/zitadel/fork) + 2. Clone your Zitadel fork \`git clone https://github.com//zitadel.git\` + 3. Change directory to your Zitadel forks root. + 4. Pull your changes into the Zitadel fork by running \`make login_pull LOGIN_REMOTE_URL=https://github.com//typescript.git LOGIN_REMOTE_BRANCH=\`. + 5. Push your changes and [open a pull request to zitadel/zitadel](https://github.com/zitadel/zitadel/compare) + `.trim(); + await github.rest.issues.createComment({ + ...context.repo, + issue_number: context.issue.number, + body: message + }); + await github.rest.pulls.update({ + ...context.repo, + pull_number: context.issue.number, + state: "closed" + }); diff --git a/apps/login/.github/workflows/issues.yml b/apps/login/.github/workflows/issues.yml new file mode 100644 index 0000000000..ff12b8fe04 --- /dev/null +++ b/apps/login/.github/workflows/issues.yml @@ -0,0 +1,41 @@ +name: Add new issues to product management project + +on: + issues: + types: + - opened + +jobs: + add-to-project: + name: Add issue and community pr to project + runs-on: ubuntu-latest + if: github.repository_id == '622995060' + steps: + - name: add issue + uses: actions/add-to-project@v1.0.2 + if: ${{ github.event_name == 'issues' }} + with: + # You can target a repository in a different organization + # to the issue + project-url: https://github.com/orgs/zitadel/projects/2 + github-token: ${{ secrets.ADD_TO_PROJECT_PAT }} + - uses: tspascoal/get-user-teams-membership@v3 + id: checkUserMember + if: github.actor != 'dependabot[bot]' + with: + username: ${{ github.actor }} + GITHUB_TOKEN: ${{ secrets.ADD_TO_PROJECT_PAT }} + - name: add pr + uses: actions/add-to-project@v1.0.2 + if: ${{ github.event_name == 'pull_request_target' && github.actor != 'dependabot[bot]' && !contains(steps.checkUserMember.outputs.teams, 'engineers')}} + with: + # You can target a repository in a different organization + # to the issue + project-url: https://github.com/orgs/zitadel/projects/2 + github-token: ${{ secrets.ADD_TO_PROJECT_PAT }} + - uses: actions-ecosystem/action-add-labels@v1.1.3 + if: ${{ github.event_name == 'pull_request_target' && github.actor != 'dependabot[bot]' && !contains(steps.checkUserMember.outputs.teams, 'staff')}} + with: + github_token: ${{ secrets.ADD_TO_PROJECT_PAT }} + labels: | + os-contribution diff --git a/apps/login/.github/workflows/release.yml b/apps/login/.github/workflows/release.yml new file mode 100644 index 0000000000..2508627d1b --- /dev/null +++ b/apps/login/.github/workflows/release.yml @@ -0,0 +1,32 @@ +name: Release + +on: + push: + branches: + - main + +concurrency: ${{ github.workflow }}-${{ github.ref }} + +jobs: + release: + runs-on: ubuntu-latest + if: github.repository_id != '622995060' + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install pnpm + uses: pnpm/action-setup@v4 + + - name: Install dependencies + run: pnpm install + + - name: Create Release Pull Request + uses: changesets/action@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/apps/login/.github/workflows/test.yml b/apps/login/.github/workflows/test.yml new file mode 100644 index 0000000000..7b4721dbee --- /dev/null +++ b/apps/login/.github/workflows/test.yml @@ -0,0 +1,67 @@ +name: Quality +on: + pull_request: + workflow_dispatch: + inputs: + ignore-run-cache: + description: 'Whether to ignore the run cache' + required: false + default: true + ref-tag: + description: 'overwrite the DOCKER_METADATA_OUTPUT_VERSION environment variable used by the make file' + required: false + default: '' +jobs: + quality: + name: Ensure Quality + if: github.event_name == 'workflow_dispatch' || + (github.event_name == 'pull_request' && github.repository_id != '622995060') + runs-on: ubuntu-22.04 + timeout-minutes: 30 + permissions: + contents: read # We only need read access to the repository contents + actions: write # We need write access to the actions cache + env: + CACHE_DIR: /tmp/login-run-caches + # Only run this job on workflow_dispatch or pushes to forks + steps: + - uses: actions/checkout@v4 + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: | + ghcr.io/zitadel/login + tags: | + type=raw,value=latest,enable={{is_default_branch}} + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + - name: Set up Buildx + uses: docker/setup-buildx-action@v3 + # Only with correctly restored build cache layers, the run caches work as expected. + # To restore docker build layer caches, extend the docker-bake.hcl to use the cache-from and cache-to options. + # https://docs.docker.com/build/ci/github-actions/cache/ + # Alternatively, you can use a self-hosted runner or a third-party builder that restores build layer caches out-of-the-box, like https://depot.dev/ + - name: Restore Run Caches + uses: actions/cache/restore@v4 + id: run-caches-restore + with: + path: ${{ env.CACHE_DIR }} + key: ${{ runner.os }}-login-run-caches-${{github.ref_name}}-${{ github.sha }}-${{github.run_attempt}} + restore-keys: | + ${{ runner.os }}-login-run-caches-${{github.ref_name}}-${{ github.sha }}- + ${{ runner.os }}-login-run-caches-${{github.ref_name}}- + ${{ runner.os }}-login-run-caches- + - run: make login_quality + env: + IGNORE_RUN_CACHE: ${{ github.event.inputs.ignore-run-cache == 'true' }} + DOCKER_METADATA_OUTPUT_VERSION: ${{ github.event.inputs.ref-tag || env.DOCKER_METADATA_OUTPUT_VERSION || steps.meta.outputs.version }} + - name: Save Run Caches + uses: actions/cache/save@v4 + with: + path: ${{ env.CACHE_DIR }} + key: ${{ steps.run-caches-restore.outputs.cache-primary-key }} + if: always() diff --git a/apps/login/.gitignore b/apps/login/.gitignore new file mode 100644 index 0000000000..17a18bf973 --- /dev/null +++ b/apps/login/.gitignore @@ -0,0 +1,16 @@ +custom-config.js +.env*.local +standalone +tsconfig.tsbuildinfo + +.DS_Store +node_modules +.turbo +*.log +.next +dist +dist-ssr +*.local +.env +.vscode +/blob-report/ diff --git a/apps/login/.prettierignore b/apps/login/.prettierignore new file mode 100644 index 0000000000..413c4b52e0 --- /dev/null +++ b/apps/login/.prettierignore @@ -0,0 +1,5 @@ +* +!constants +!src +!locales +!scripts/healthcheck.js \ No newline at end of file diff --git a/apps/login/.prettierrc b/apps/login/.prettierrc new file mode 100644 index 0000000000..ba42405b03 --- /dev/null +++ b/apps/login/.prettierrc @@ -0,0 +1,6 @@ +{ + "printWidth": 125, + "trailingComma": "all", + "plugins": ["prettier-plugin-organize-imports"], + "filepath": "" +} diff --git a/apps/login/CODE_OF_CONDUCT.md b/apps/login/CODE_OF_CONDUCT.md new file mode 100644 index 0000000000..ac3f129652 --- /dev/null +++ b/apps/login/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +- Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +- The use of sexualized language or imagery, and sexual attention or + advances of any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email + address, without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +legal@zitadel.com. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/apps/login/CONTRIBUTING.md b/apps/login/CONTRIBUTING.md new file mode 100644 index 0000000000..50ca3172d1 --- /dev/null +++ b/apps/login/CONTRIBUTING.md @@ -0,0 +1,218 @@ +# Contributing + +:attention: In this CONTRIBUTING.md you read about contributing to this very repository. +If you want to develop your own login UI, please refer [to the README.md](./README.md). + +## Introduction + +Thank you for your interest about how to contribute! + +:attention: If you notice a possible **security vulnerability**, please don't hesitate to disclose any concern by contacting [security@zitadel.com](mailto:security@zitadel.com). +You don't have to be perfectly sure about the nature of the vulnerability. +We will give them a high priority and figure them out. + +We also appreciate all your other ideas, thoughts and feedback and will take care of them as soon as possible. +We love to discuss in an open space using [GitHub issues](https://github.com/zitadel/typescript/issues), +[GitHub discussions in the core repo](https://github.com/zitadel/zitadel/discussions) +or in our [chat on Discord](https://zitadel.com/chat). +For private discussions, +you have [more contact options on our Website](https://zitadel.com/contact). + +## Pull Requests + +The repository zitadel/typescript is a read-only mirror of the git subtree at zitadel/zitadel/login. +To submit changes, please open a Pull Request [in the zitadel/zitadel repository](https://github.com/zitadel/zitadel/compare). + +If you already made changes based on the zitadel/typescript repository, these changes are not lost. +Submitting them to the main repository is easy: + +1. [Fork zitadel/zitadel](https://github.com/zitadel/zitadel/fork) +1. Clone your Zitadel fork git clone https://github.com//zitadel.git +1. Change directory to your Zitadel forks root. +1. Pull your changes into the Zitadel fork by running make login_pull LOGIN_REMOTE_URL=https://github.com//typescript.git LOGIN_REMOTE_BRANCH=. +1. Push your changes and [open a pull request to zitadel/zitadel](https://github.com/zitadel/zitadel/compare) + +Please consider the following guidelines when creating a pull request. + +- The latest changes are always in `main`, so please make your pull request against that branch. +- pull requests should be raised for any change +- Pull requests need approval of a Zitadel core engineer @zitadel/engineers before merging +- We use ESLint/Prettier for linting/formatting, so please run `pnpm lint:fix` before committing to make resolving conflicts easier (VSCode users, check out [this ESLint extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) and [this Prettier extension](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) to fix lint and formatting issues in development) +- If you add new functionality, please provide the corresponding documentation as well and make it part of the pull request + +### Setting up local environment + +```sh +# Install dependencies. Developing requires Node.js v20 +pnpm install + +# Generate gRPC stubs +pnpm generate + +# Start a local development server for the login and manually configure apps/login/.env.local +pnpm dev +``` + +The application is now available at `http://localhost:3000` + +Configure apps/login/.env.local to target the Zitadel instance of your choice. +The login app live-reloads on changes, so you can start developing right away. + +### Developing Against A Local Latest Zitadel Release + +The following command uses Docker to run a local Zitadel instance and the login application in live-reloading dev mode. +Additionally, it runs a Traefik reverse proxy that exposes the login with a self-signed certificate at https://127.0.0.1.sslip.io +127.0.0.1.sslip.io is a special domain that resolves to your localhost, so it's safe to allow your browser to proceed with loading the page. + +```sh +# Install dependencies. Developing requires Node.js v20 +pnpm install + +# Generate gRPC stubs +pnpm generate + +# Start a local development server and have apps/login/.env.test.local configured for you to target the local Zitadel instance. +pnpm dev:local +``` + +Log in at https://127.0.0.1.sslip.io/ui/v2/login/loginname and use the following credentials: +**Loginname**: *zitadel-admin@zitadel.127.0.0.1.sslip.io* +**Password**: _Password1!_. + +The login app live-reloads on changes, so you can start developing right away. + +### Developing Against A Locally Compiled Zitadel + +To develop against a locally compiled version of Zitadel, you need to build the Zitadel docker image first. +Clone the [Zitadel repository](https://github.com/zitadel/zitadel.git) and run the following command from its root: + +```sh +# This compiles a Zitadel binary if it does not exist at ./zitadel already and copies it into a Docker image. +# If you want to recompile the binary, run `make compile` first +make login_dev +``` + +Open another terminal session at zitadel/zitadel/login and run the following commands to start the dev server. + +```bash +# Install dependencies. Developing requires Node.js v20 +pnpm install + +# Start a local development server and have apps/login/.env.test.local configured for you to target the local Zitadel instance. +NODE_ENV=test pnpm dev +``` + +Log in at https://127.0.0.1.sslip.io/ui/v2/login/loginname and use the following credentials: +**Loginname**: *zitadel-admin@zitadel.127.0.0.1.sslip.io* +**Password**: _Password1!_. + +The login app live-reloads on changes, so you can start developing right away. + +### Quality Assurance + +Use `make` commands to test the quality of your code against a production build without installing any dependencies besides Docker. +Using `make` commands, you can reproduce and debug the CI pipelines locally. + +```sh +# Reproduce the whole CI pipeline in docker +make login_quality +# Show other options with make +make help +``` + +Use `pnpm` commands to run the tests in dev mode with live reloading and debugging capabilities. + +#### Linting and formatting + +Check the formatting and linting of the code in docker + +```sh +make login_lint +``` + +Check the linting of the code using pnpm + +```sh +pnpm lint +pnpm format +``` + +Fix the linting of your code + +```sh +pnpm lint:fix +pnpm format:fix +``` + +#### Running Unit Tests + +Run the tests in docker + +```sh +make login_test_unit +``` + +Run unit tests with live-reloading + +```sh +pnpm test:unit +``` + +#### Running Integration Tests + +Run the test in docker + +```sh +make login_test_integration +``` + +Alternatively, run a live-reloading development server with an interactive Cypress test suite. +First, set up your local test environment. + +```sh +# Install dependencies. Developing requires Node.js v20 +pnpm install + +# Generate gRPC stubs +pnpm generate + +# Start a local development server and use apps/login/.env.test to use the locally mocked Zitadel API. +pnpm test:integration:setup +``` + +Now, in another terminal session, open the interactive Cypress integration test suite. + +```sh +pnpm test:integration open +``` + +Show more options with Cypress + +```sh +pnpm test:integration help +``` + +#### Running Acceptance Tests + +To run the tests in docker against the latest release of Zitadel, use the following command: + +:warning: The acceptance tests are not reliable at the moment :construction: + +```sh +make login_test_acceptance +``` + +Alternatively, run can use a live-reloading development server with an interactive Playwright test suite. +Set up your local environment by running the commands either for [developing against a local latest Zitadel release](latest) or for [developing against a locally compiled Zitadel](compiled). + +Now, in another terminal session, open the interactive Playwright acceptance test suite. + +```sh +pnpm test:acceptance open +``` + +Show more options with Playwright + +```sh +pnpm test:acceptance help +``` diff --git a/apps/login/Dockerfile b/apps/login/Dockerfile new file mode 100644 index 0000000000..7e3d8668d2 --- /dev/null +++ b/apps/login/Dockerfile @@ -0,0 +1,36 @@ +FROM node:20-alpine AS base + +FROM base AS build +ENV PNPM_HOME="/pnpm" +ENV PATH="$PNPM_HOME:$PATH" +RUN corepack enable && COREPACK_ENABLE_DOWNLOAD_PROMPT=0 corepack prepare pnpm@9.1.2 --activate && \ + apk update && apk add --no-cache && \ + rm -rf /var/cache/apk/* +WORKDIR /app +COPY pnpm-lock.yaml ./ +RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store pnpm fetch --frozen-lockfile +COPY package.json ./ +RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store pnpm install --frozen-lockfile --prod +COPY . . +RUN pnpm build:login:standalone + +FROM scratch AS build-out +COPY --from=build /app/.next/standalone / +COPY --from=build /app/.next/static /.next/static +COPY --from=build /app/public /public + +FROM base AS login-standalone +WORKDIR /runtime +RUN addgroup --system --gid 1001 nodejs && \ + adduser --system --uid 1001 nextjs +# If /.env-file/.env is mounted into the container, its variables are made available to the server before it starts up. +RUN mkdir -p /.env-file && touch /.env-file/.env && chown -R nextjs:nodejs /.env-file +COPY ./scripts/ ./ +COPY --chown=nextjs:nodejs --from=build-out / ./ +USER nextjs +ENV HOSTNAME="0.0.0.0" +ENV PORT=3000 +# TODO: Check healthy, not ready +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ +CMD ["/bin/sh", "-c", "node ./healthcheck.js http://localhost:${PORT}/ui/v2/login/healthy"] +ENTRYPOINT ["./entrypoint.sh"] diff --git a/apps/login/LICENSE b/apps/login/LICENSE new file mode 100644 index 0000000000..89f750f2ab --- /dev/null +++ b/apps/login/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 ZITADEL + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/apps/login/acceptance/.eslintrc.cjs b/apps/login/acceptance/.eslintrc.cjs new file mode 100644 index 0000000000..84711c5881 --- /dev/null +++ b/apps/login/acceptance/.eslintrc.cjs @@ -0,0 +1,10 @@ +module.exports = { + root: true, + // Use basic ESLint config since the login app has its own detailed config + extends: ["eslint:recommended"], + settings: { + next: { + rootDir: ["apps/*/"], + }, + }, +}; diff --git a/apps/login/acceptance/.gitignore b/apps/login/acceptance/.gitignore new file mode 100644 index 0000000000..6a7425e885 --- /dev/null +++ b/apps/login/acceptance/.gitignore @@ -0,0 +1 @@ +go-command diff --git a/apps/login/acceptance/go-command.Dockerfile b/apps/login/acceptance/go-command.Dockerfile new file mode 100644 index 0000000000..fafebd6f4d --- /dev/null +++ b/apps/login/acceptance/go-command.Dockerfile @@ -0,0 +1,11 @@ +ARG LOGIN_TEST_ACCEPTANCE_GOLANG_TAG="golang:1.24-alpine" + +FROM ${LOGIN_TEST_ACCEPTANCE_GOLANG_TAG} +RUN apk add curl jq +COPY go.mod go.sum ./ +RUN go mod download +COPY . . +RUN go build -o /go-command . +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s \ + CMD curl -f http://localhost:${PORT}/healthy || exit 1 +ENTRYPOINT [ "/go-command" ] diff --git a/apps/login/acceptance/idp/oidc/go.mod b/apps/login/acceptance/idp/oidc/go.mod new file mode 100644 index 0000000000..bc43390218 --- /dev/null +++ b/apps/login/acceptance/idp/oidc/go.mod @@ -0,0 +1,28 @@ +module github.com/zitadel/typescript/acceptance/idp/oidc + +go 1.24.1 + +require github.com/zitadel/oidc/v3 v3.37.0 + +require ( + github.com/bmatcuk/doublestar/v4 v4.8.1 // indirect + github.com/go-chi/chi/v5 v5.2.2 // indirect + github.com/go-jose/go-jose/v4 v4.0.5 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/securecookie v1.1.2 // indirect + github.com/muhlemmer/gu v0.3.1 // indirect + github.com/muhlemmer/httpforwarded v0.1.0 // indirect + github.com/rs/cors v1.11.1 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/zitadel/logging v0.6.2 // indirect + github.com/zitadel/schema v1.3.1 // indirect + go.opentelemetry.io/otel v1.29.0 // indirect + go.opentelemetry.io/otel/metric v1.29.0 // indirect + go.opentelemetry.io/otel/trace v1.29.0 // indirect + golang.org/x/crypto v0.35.0 // indirect + golang.org/x/oauth2 v0.28.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/text v0.23.0 // indirect +) diff --git a/apps/login/acceptance/idp/oidc/go.sum b/apps/login/acceptance/idp/oidc/go.sum new file mode 100644 index 0000000000..23fd2b3384 --- /dev/null +++ b/apps/login/acceptance/idp/oidc/go.sum @@ -0,0 +1,71 @@ +github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38= +github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618= +github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= +github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= +github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +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/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= +github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/muhlemmer/gu v0.3.1 h1:7EAqmFrW7n3hETvuAdmFmn4hS8W+z3LgKtrnow+YzNM= +github.com/muhlemmer/gu v0.3.1/go.mod h1:YHtHR+gxM+bKEIIs7Hmi9sPT3ZDUvTN/i88wQpZkrdM= +github.com/muhlemmer/httpforwarded v0.1.0 h1:x4DLrzXdliq8mprgUMR0olDvHGkou5BJsK/vWUetyzY= +github.com/muhlemmer/httpforwarded v0.1.0/go.mod h1:yo9czKedo2pdZhoXe+yDkGVbU0TJ0q9oQ90BVoDEtw0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= +github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +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/zitadel/logging v0.6.2 h1:MW2kDDR0ieQynPZ0KIZPrh9ote2WkxfBif5QoARDQcU= +github.com/zitadel/logging v0.6.2/go.mod h1:z6VWLWUkJpnNVDSLzrPSQSQyttysKZ6bCRongw0ROK4= +github.com/zitadel/oidc/v3 v3.37.0 h1:nYATWlnP7f18XiAbw6upUruBaqfB1kUrXrSTf1EYGO8= +github.com/zitadel/oidc/v3 v3.37.0/go.mod h1:/xDan4OUQhguJ4Ur73OOJrtugvR164OMnidXP9xfVNw= +github.com/zitadel/schema v1.3.1 h1:QT3kwiRIRXXLVAs6gCK/u044WmUVh6IlbLXUsn6yRQU= +github.com/zitadel/schema v1.3.1/go.mod h1:071u7D2LQacy1HAN+YnMd/mx1qVE2isb0Mjeqg46xnU= +go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= +go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= +go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= +go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= +go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= +go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= +golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= +golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= +golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= +golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/apps/login/acceptance/idp/oidc/main.go b/apps/login/acceptance/idp/oidc/main.go new file mode 100644 index 0000000000..b04ac94234 --- /dev/null +++ b/apps/login/acceptance/idp/oidc/main.go @@ -0,0 +1,186 @@ +package main + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "log" + "log/slog" + "net/http" + "os" + "os/signal" + "strings" + "syscall" + "time" + + "github.com/zitadel/oidc/v3/example/server/exampleop" + "github.com/zitadel/oidc/v3/example/server/storage" +) + +func main() { + apiURL := os.Getenv("API_URL") + pat := readPAT(os.Getenv("PAT_FILE")) + domain := os.Getenv("API_DOMAIN") + schema := os.Getenv("SCHEMA") + host := os.Getenv("HOST") + port := os.Getenv("PORT") + + logger := slog.New( + slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{ + AddSource: true, + Level: slog.LevelDebug, + }), + ) + + issuer := fmt.Sprintf("%s://%s:%s/", schema, host, port) + redirectURI := fmt.Sprintf("%s/idps/callback", apiURL) + + clientID := "web" + clientSecret := "secret" + storage.RegisterClients( + storage.WebClient(clientID, clientSecret, redirectURI), + ) + + storage := storage.NewStorage(storage.NewUserStore(issuer)) + router := exampleop.SetupServer(issuer, storage, logger, false) + + server := &http.Server{ + Addr: ":" + port, + Handler: router, + } + go func() { + if err := server.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) { + log.Fatalf("HTTP server error: %v", err) + } + log.Println("Stopped serving new connections.") + }() + + createZitadelResources(apiURL, pat, domain, issuer, clientID, clientSecret) + + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + <-sigChan + + shutdownCtx, shutdownRelease := context.WithTimeout(context.Background(), 10*time.Second) + defer shutdownRelease() + + if err := server.Shutdown(shutdownCtx); err != nil { + log.Fatalf("HTTP shutdown error: %v", err) + } +} + +func readPAT(path string) string { + f, err := os.Open(path) + if err != nil { + panic(err) + } + pat, err := io.ReadAll(f) + if err != nil { + panic(err) + } + return strings.Trim(string(pat), "\n") +} + +func createZitadelResources(apiURL, pat, domain, issuer, clientID, clientSecret string) error { + idpID, err := CreateIDP(apiURL, pat, domain, issuer, clientID, clientSecret) + if err != nil { + return err + } + return ActivateIDP(apiURL, pat, domain, idpID) +} + +type createIDP struct { + Name string `json:"name"` + Issuer string `json:"issuer"` + ClientId string `json:"clientId"` + ClientSecret string `json:"clientSecret"` + Scopes []string `json:"scopes"` + ProviderOptions providerOptions `json:"providerOptions"` + IsIdTokenMapping bool `json:"isIdTokenMapping"` + UsePkce bool `json:"usePkce"` +} + +type providerOptions struct { + IsLinkingAllowed bool `json:"isLinkingAllowed"` + IsCreationAllowed bool `json:"isCreationAllowed"` + IsAutoCreation bool `json:"isAutoCreation"` + IsAutoUpdate bool `json:"isAutoUpdate"` + AutoLinking string `json:"autoLinking"` +} + +type idp struct { + ID string `json:"id"` +} + +func CreateIDP(apiURL, pat, domain string, issuer, clientID, clientSecret string) (string, error) { + createIDP := &createIDP{ + Name: "OIDC", + Issuer: issuer, + ClientId: clientID, + ClientSecret: clientSecret, + Scopes: []string{"openid", "profile", "email"}, + ProviderOptions: providerOptions{ + IsLinkingAllowed: true, + IsCreationAllowed: true, + IsAutoCreation: true, + IsAutoUpdate: true, + AutoLinking: "AUTO_LINKING_OPTION_USERNAME", + }, + IsIdTokenMapping: false, + UsePkce: false, + } + + resp, err := doRequestWithHeaders(apiURL+"/admin/v1/idps/generic_oidc", pat, domain, createIDP) + if err != nil { + return "", err + } + data, err := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + defer resp.Body.Close() + + idp := new(idp) + if err := json.Unmarshal(data, idp); err != nil { + return "", err + } + return idp.ID, nil +} + +type activateIDP struct { + IdpId string `json:"idpId"` +} + +func ActivateIDP(apiURL, pat, domain string, idpID string) error { + activateIDP := &activateIDP{ + IdpId: idpID, + } + _, err := doRequestWithHeaders(apiURL+"/admin/v1/policies/login/idps", pat, domain, activateIDP) + return err +} + +func doRequestWithHeaders(apiURL, pat, domain string, body any) (*http.Response, error) { + data, err := json.Marshal(body) + if err != nil { + return nil, err + } + + req, err := http.NewRequest(http.MethodPost, apiURL, io.NopCloser(bytes.NewReader(data))) + if err != nil { + return nil, err + } + values := http.Header{} + values.Add("Authorization", "Bearer "+pat) + values.Add("x-forwarded-host", domain) + values.Add("Content-Type", "application/json") + req.Header = values + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + return resp, nil +} diff --git a/apps/login/acceptance/idp/saml/go.mod b/apps/login/acceptance/idp/saml/go.mod new file mode 100644 index 0000000000..e73b4feb3b --- /dev/null +++ b/apps/login/acceptance/idp/saml/go.mod @@ -0,0 +1,16 @@ +module github.com/zitadel/typescript/acceptance/idp/saml + +go 1.24.1 + +require ( + github.com/crewjam/saml v0.4.14 + github.com/mattermost/xml-roundtrip-validator v0.1.0 + github.com/zenazn/goji v1.0.1 + golang.org/x/crypto v0.36.0 +) + +require ( + github.com/beevik/etree v1.1.0 // indirect + github.com/jonboulle/clockwork v0.2.2 // indirect + github.com/russellhaering/goxmldsig v1.3.0 // indirect +) diff --git a/apps/login/acceptance/idp/saml/go.sum b/apps/login/acceptance/idp/saml/go.sum new file mode 100644 index 0000000000..1208550f6e --- /dev/null +++ b/apps/login/acceptance/idp/saml/go.sum @@ -0,0 +1,49 @@ +github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs= +github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/crewjam/saml v0.4.14 h1:g9FBNx62osKusnFzs3QTN5L9CVA/Egfgm+stJShzw/c= +github.com/crewjam/saml v0.4.14/go.mod h1:UVSZCf18jJkk6GpWNVqcyQJMD5HsRugBPf4I1nl2mME= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/golang-jwt/jwt/v4 v4.4.3 h1:Hxl6lhQFj4AnOX6MLrsCb/+7tCj7DxP7VA+2rDIq5AU= +github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ= +github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mattermost/xml-roundtrip-validator v0.1.0 h1:RXbVD2UAl7A7nOTR4u7E3ILa4IbtvKBHw64LDsmu9hU= +github.com/mattermost/xml-roundtrip-validator v0.1.0/go.mod h1:qccnGMcpgwcNaBnxqpJpWWUiPNr5H3O8eDgGV9gT5To= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/russellhaering/goxmldsig v1.3.0 h1:DllIWUgMy0cRUMfGiASiYEa35nsieyD3cigIwLonTPM= +github.com/russellhaering/goxmldsig v1.3.0/go.mod h1:gM4MDENBQf7M+V824SGfyIUVFWydB7n0KkEubVJl+Tw= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/zenazn/goji v1.0.1 h1:4lbD8Mx2h7IvloP7r2C0D6ltZP6Ufip8Hn0wmSK5LR8= +github.com/zenazn/goji v1.0.1/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +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-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= diff --git a/apps/login/acceptance/idp/saml/main.go b/apps/login/acceptance/idp/saml/main.go new file mode 100644 index 0000000000..059eab79e2 --- /dev/null +++ b/apps/login/acceptance/idp/saml/main.go @@ -0,0 +1,328 @@ +package main + +import ( + "bytes" + "crypto" + "crypto/x509" + "encoding/base64" + "encoding/json" + "encoding/pem" + "encoding/xml" + "errors" + "io" + "log" + "net/http" + "net/http/httptest" + "net/url" + "os" + "os/signal" + "strings" + "syscall" + + "github.com/crewjam/saml" + "github.com/crewjam/saml/logger" + "github.com/crewjam/saml/samlidp" + xrv "github.com/mattermost/xml-roundtrip-validator" + "github.com/zenazn/goji" + "github.com/zenazn/goji/bind" + "github.com/zenazn/goji/web" + "golang.org/x/crypto/bcrypt" +) + +var key = func() crypto.PrivateKey { + b, _ := pem.Decode([]byte(`-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA0OhbMuizgtbFOfwbK7aURuXhZx6VRuAs3nNibiuifwCGz6u9 +yy7bOR0P+zqN0YkjxaokqFgra7rXKCdeABmoLqCC0U+cGmLNwPOOA0PaD5q5xKhQ +4Me3rt/R9C4Ca6k3/OnkxnKwnogcsmdgs2l8liT3qVHP04Oc7Uymq2v09bGb6nPu +fOrkXS9F6mSClxHG/q59AGOWsXK1xzIRV1eu8W2SNdyeFVU1JHiQe444xLoPul5t +InWasKayFsPlJfWNc8EoU8COjNhfo/GovFTHVjh9oUR/gwEFVwifIHihRE0Hazn2 +EQSLaOr2LM0TsRsQroFjmwSGgI+X2bfbMTqWOQIDAQABAoIBAFWZwDTeESBdrLcT +zHZe++cJLxE4AObn2LrWANEv5AeySYsyzjRBYObIN9IzrgTb8uJ900N/zVr5VkxH +xUa5PKbOcowd2NMfBTw5EEnaNbILLm+coHdanrNzVu59I9TFpAFoPavrNt/e2hNo +NMGPSdOkFi81LLl4xoadz/WR6O/7N2famM+0u7C2uBe+TrVwHyuqboYoidJDhO8M +w4WlY9QgAUhkPyzZqrl+VfF1aDTGVf4LJgaVevfFCas8Ws6DQX5q4QdIoV6/0vXi +B1M+aTnWjHuiIzjBMWhcYW2+I5zfwNWRXaxdlrYXRukGSdnyO+DH/FhHePJgmlkj +NInADDkCgYEA6MEQFOFSCc/ELXYWgStsrtIlJUcsLdLBsy1ocyQa2lkVUw58TouW +RciE6TjW9rp31pfQUnO2l6zOUC6LT9Jvlb9PSsyW+rvjtKB5PjJI6W0hjX41wEO6 +fshFELMJd9W+Ezao2AsP2hZJ8McCF8no9e00+G4xTAyxHsNI2AFTCQcCgYEA5cWZ +JwNb4t7YeEajPt9xuYNUOQpjvQn1aGOV7KcwTx5ELP/Hzi723BxHs7GSdrLkkDmi +Gpb+mfL4wxCt0fK0i8GFQsRn5eusyq9hLqP/bmjpHoXe/1uajFbE1fZQR+2LX05N +3ATlKaH2hdfCJedFa4wf43+cl6Yhp6ZA0Yet1r8CgYEAwiu1j8W9G+RRA5/8/DtO +yrUTOfsbFws4fpLGDTA0mq0whf6Soy/96C90+d9qLaC3srUpnG9eB0CpSOjbXXbv +kdxseLkexwOR3bD2FHX8r4dUM2bzznZyEaxfOaQypN8SV5ME3l60Fbr8ajqLO288 +wlTmGM5Mn+YCqOg/T7wjGmcCgYBpzNfdl/VafOROVbBbhgXWtzsz3K3aYNiIjbp+ +MunStIwN8GUvcn6nEbqOaoiXcX4/TtpuxfJMLw4OvAJdtxUdeSmEee2heCijV6g3 +ErrOOy6EqH3rNWHvlxChuP50cFQJuYOueO6QggyCyruSOnDDuc0BM0SGq6+5g5s7 +H++S/wKBgQDIkqBtFr9UEf8d6JpkxS0RXDlhSMjkXmkQeKGFzdoJcYVFIwq8jTNB +nJrVIGs3GcBkqGic+i7rTO1YPkquv4dUuiIn+vKZVoO6b54f+oPBXd4S0BnuEqFE +rdKNuCZhiaE2XD9L/O9KP1fh5bfEcKwazQ23EvpJHBMm8BGC+/YZNw== +-----END RSA PRIVATE KEY-----`)) + k, _ := x509.ParsePKCS1PrivateKey(b.Bytes) + return k +}() + +var cert = func() *x509.Certificate { + b, _ := pem.Decode([]byte(`-----BEGIN CERTIFICATE----- +MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNV +BAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5 +NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8A +hs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+a +ucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWx +m+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6 +D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURN +B2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0O +BBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56 +zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5 +pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uv +NONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEf +y/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL +/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsb +GFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTL +UzreO96WzlBBMtY= +-----END CERTIFICATE-----`)) + c, _ := x509.ParseCertificate(b.Bytes) + return c +}() + +// Example from https://github.com/crewjam/saml/blob/main/example/idp/idp.go +func main() { + apiURL := os.Getenv("API_URL") + pat := readPAT(os.Getenv("PAT_FILE")) + domain := os.Getenv("API_DOMAIN") + schema := os.Getenv("SCHEMA") + host := os.Getenv("HOST") + port := os.Getenv("PORT") + + baseURL, err := url.Parse(schema + "://" + host + ":" + port) + if err != nil { + + panic(err) + } + + idpServer, err := samlidp.New(samlidp.Options{ + URL: *baseURL, + Logger: logger.DefaultLogger, + Key: key, + Certificate: cert, + Store: &samlidp.MemoryStore{}, + }) + if err != nil { + + panic(err) + } + + metadata, err := xml.MarshalIndent(idpServer.IDP.Metadata(), "", " ") + if err != nil { + panic(err) + } + idpID, err := createZitadelResources(apiURL, pat, domain, metadata) + if err != nil { + panic(err) + } + + lis := bind.Socket(":" + baseURL.Port()) + goji.Handle("/*", idpServer) + + go func() { + goji.ServeListener(lis) + }() + + addService(idpServer, apiURL+"/idps/"+idpID+"/saml/metadata") + addUsers(idpServer) + + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + <-sigChan + + if err := lis.Close(); err != nil { + log.Fatalf("HTTP shutdown error: %v", err) + } +} + +func readPAT(path string) string { + f, err := os.Open(path) + if err != nil { + panic(err) + } + pat, err := io.ReadAll(f) + if err != nil { + panic(err) + } + return strings.Trim(string(pat), "\n") +} + +func addService(idpServer *samlidp.Server, spURLStr string) { + metadataResp, err := http.Get(spURLStr) + if err != nil { + panic(err) + } + defer metadataResp.Body.Close() + + idpServer.HandlePutService( + web.C{URLParams: map[string]string{"id": spURLStr}}, + httptest.NewRecorder(), + httptest.NewRequest(http.MethodPost, spURLStr, metadataResp.Body), + ) +} + +func getSPMetadata(r io.Reader) (spMetadata *saml.EntityDescriptor, err error) { + var data []byte + if data, err = io.ReadAll(r); err != nil { + return nil, err + } + + spMetadata = &saml.EntityDescriptor{} + if err := xrv.Validate(bytes.NewBuffer(data)); err != nil { + return nil, err + } + + if err := xml.Unmarshal(data, &spMetadata); err != nil { + if err.Error() == "expected element type but have " { + entities := &saml.EntitiesDescriptor{} + if err := xml.Unmarshal(data, &entities); err != nil { + return nil, err + } + + for _, e := range entities.EntityDescriptors { + if len(e.SPSSODescriptors) > 0 { + return &e, nil + } + } + + // there were no SPSSODescriptors in the response + return nil, errors.New("metadata contained no service provider metadata") + } + + return nil, err + } + + return spMetadata, nil +} + +func addUsers(idpServer *samlidp.Server) { + hashedPassword, _ := bcrypt.GenerateFromPassword([]byte("hunter2"), bcrypt.DefaultCost) + err := idpServer.Store.Put("/users/alice", samlidp.User{Name: "alice", + HashedPassword: hashedPassword, + Groups: []string{"Administrators", "Users"}, + Email: "alice@example.com", + CommonName: "Alice Smith", + Surname: "Smith", + GivenName: "Alice", + }) + if err != nil { + panic(err) + } + + err = idpServer.Store.Put("/users/bob", samlidp.User{ + Name: "bob", + HashedPassword: hashedPassword, + Groups: []string{"Users"}, + Email: "bob@example.com", + CommonName: "Bob Smith", + Surname: "Smith", + GivenName: "Bob", + }) + if err != nil { + panic(err) + } +} + +func createZitadelResources(apiURL, pat, domain string, metadata []byte) (string, error) { + idpID, err := CreateIDP(apiURL, pat, domain, metadata) + if err != nil { + return "", err + } + return idpID, ActivateIDP(apiURL, pat, domain, idpID) +} + +type createIDP struct { + Name string `json:"name"` + MetadataXml string `json:"metadataXml"` + Binding string `json:"binding"` + WithSignedRequest bool `json:"withSignedRequest"` + ProviderOptions providerOptions `json:"providerOptions"` + NameIdFormat string `json:"nameIdFormat"` +} +type providerOptions struct { + IsLinkingAllowed bool `json:"isLinkingAllowed"` + IsCreationAllowed bool `json:"isCreationAllowed"` + IsAutoCreation bool `json:"isAutoCreation"` + IsAutoUpdate bool `json:"isAutoUpdate"` + AutoLinking string `json:"autoLinking"` +} + +type idp struct { + ID string `json:"id"` +} + +func CreateIDP(apiURL, pat, domain string, idpMetadata []byte) (string, error) { + encoded := make([]byte, base64.URLEncoding.EncodedLen(len(idpMetadata))) + base64.URLEncoding.Encode(encoded, idpMetadata) + + createIDP := &createIDP{ + Name: "CREWJAM", + MetadataXml: string(encoded), + Binding: "SAML_BINDING_REDIRECT", + WithSignedRequest: false, + ProviderOptions: providerOptions{ + IsLinkingAllowed: true, + IsCreationAllowed: true, + IsAutoCreation: true, + IsAutoUpdate: true, + AutoLinking: "AUTO_LINKING_OPTION_USERNAME", + }, + NameIdFormat: "SAML_NAME_ID_FORMAT_PERSISTENT", + } + + resp, err := doRequestWithHeaders(apiURL+"/admin/v1/idps/saml", pat, domain, createIDP) + if err != nil { + return "", err + } + data, err := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + defer resp.Body.Close() + + idp := new(idp) + if err := json.Unmarshal(data, idp); err != nil { + return "", err + } + return idp.ID, nil +} + +type activateIDP struct { + IdpId string `json:"idpId"` +} + +func ActivateIDP(apiURL, pat, domain string, idpID string) error { + activateIDP := &activateIDP{ + IdpId: idpID, + } + _, err := doRequestWithHeaders(apiURL+"/admin/v1/policies/login/idps", pat, domain, activateIDP) + return err +} + +func doRequestWithHeaders(apiURL, pat, domain string, body any) (*http.Response, error) { + data, err := json.Marshal(body) + if err != nil { + return nil, err + } + + req, err := http.NewRequest(http.MethodPost, apiURL, io.NopCloser(bytes.NewReader(data))) + if err != nil { + return nil, err + } + values := http.Header{} + values.Add("Authorization", "Bearer "+pat) + values.Add("x-forwarded-host", domain) + values.Add("Content-Type", "application/json") + req.Header = values + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + return resp, nil +} diff --git a/apps/login/acceptance/oidcrp/go.mod b/apps/login/acceptance/oidcrp/go.mod new file mode 100644 index 0000000000..f2cda3058e --- /dev/null +++ b/apps/login/acceptance/oidcrp/go.mod @@ -0,0 +1,26 @@ +module github.com/zitadel/typescript/acceptance/oidc + +go 1.24.1 + +require ( + github.com/google/uuid v1.6.0 + github.com/sirupsen/logrus v1.9.3 + github.com/zitadel/logging v0.6.1 + github.com/zitadel/oidc/v3 v3.36.1 +) + +require ( + github.com/go-jose/go-jose/v4 v4.0.5 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/gorilla/securecookie v1.1.2 // indirect + github.com/muhlemmer/gu v0.3.1 // indirect + github.com/zitadel/schema v1.3.0 // indirect + go.opentelemetry.io/otel v1.29.0 // indirect + go.opentelemetry.io/otel/metric v1.29.0 // indirect + go.opentelemetry.io/otel/trace v1.29.0 // indirect + golang.org/x/crypto v0.35.0 // indirect + golang.org/x/oauth2 v0.28.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/text v0.22.0 // indirect +) diff --git a/apps/login/acceptance/oidcrp/go.sum b/apps/login/acceptance/oidcrp/go.sum new file mode 100644 index 0000000000..33244ea6eb --- /dev/null +++ b/apps/login/acceptance/oidcrp/go.sum @@ -0,0 +1,67 @@ +github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38= +github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8= +github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= +github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= +github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +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/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= +github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= +github.com/jeremija/gosubmit v0.2.8 h1:mmSITBz9JxVtu8eqbN+zmmwX7Ij2RidQxhcwRVI4wqA= +github.com/jeremija/gosubmit v0.2.8/go.mod h1:Ui+HS073lCFREXBbdfrJzMB57OI/bdxTiLtrDHHhFPI= +github.com/muhlemmer/gu v0.3.1 h1:7EAqmFrW7n3hETvuAdmFmn4hS8W+z3LgKtrnow+YzNM= +github.com/muhlemmer/gu v0.3.1/go.mod h1:YHtHR+gxM+bKEIIs7Hmi9sPT3ZDUvTN/i88wQpZkrdM= +github.com/muhlemmer/httpforwarded v0.1.0 h1:x4DLrzXdliq8mprgUMR0olDvHGkou5BJsK/vWUetyzY= +github.com/muhlemmer/httpforwarded v0.1.0/go.mod h1:yo9czKedo2pdZhoXe+yDkGVbU0TJ0q9oQ90BVoDEtw0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= +github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +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/zitadel/logging v0.6.1 h1:Vyzk1rl9Kq9RCevcpX6ujUaTYFX43aa4LkvV1TvUk+Y= +github.com/zitadel/logging v0.6.1/go.mod h1:Y4CyAXHpl3Mig6JOszcV5Rqqsojj+3n7y2F591Mp/ow= +github.com/zitadel/oidc/v3 v3.36.1 h1:1AT1NqKKEqAwx4GmKJZ9fYkWH2WIn/VKMfQ46nBtRf0= +github.com/zitadel/oidc/v3 v3.36.1/go.mod h1:dApGZLvWZTHRuxmcbQlW5d2XVjVYR3vGOdq536igmTs= +github.com/zitadel/schema v1.3.0 h1:kQ9W9tvIwZICCKWcMvCEweXET1OcOyGEuFbHs4o5kg0= +github.com/zitadel/schema v1.3.0/go.mod h1:NptN6mkBDFvERUCvZHlvWmmME+gmZ44xzwRXwhzsbtc= +go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= +go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= +go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= +go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= +go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= +go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= +golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= +golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= +golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/apps/login/acceptance/oidcrp/main.go b/apps/login/acceptance/oidcrp/main.go new file mode 100644 index 0000000000..72ae5f57e9 --- /dev/null +++ b/apps/login/acceptance/oidcrp/main.go @@ -0,0 +1,322 @@ +package main + +import ( + "bytes" + "context" + "crypto/tls" + "encoding/json" + "errors" + "fmt" + "io" + "log" + "log/slog" + "net/http" + "os" + "os/signal" + "strings" + "sync/atomic" + "syscall" + "time" + + "github.com/google/uuid" + "github.com/sirupsen/logrus" + + "github.com/zitadel/logging" + "github.com/zitadel/oidc/v3/pkg/client/rp" + httphelper "github.com/zitadel/oidc/v3/pkg/http" + "github.com/zitadel/oidc/v3/pkg/oidc" +) + +var ( + callbackPath = "/auth/callback" + key = []byte("test1234test1234") +) + +func main() { + apiURL := os.Getenv("API_URL") + pat := readPAT(os.Getenv("PAT_FILE")) + domain := os.Getenv("API_DOMAIN") + loginURL := os.Getenv("LOGIN_URL") + issuer := os.Getenv("ISSUER") + port := os.Getenv("PORT") + scopeList := strings.Split(os.Getenv("SCOPES"), " ") + + redirectURI := fmt.Sprintf("%s%s", issuer, callbackPath) + cookieHandler := httphelper.NewCookieHandler(key, key, httphelper.WithUnsecure()) + + clientID, clientSecret, err := createZitadelResources(apiURL, pat, domain, redirectURI, loginURL) + if err != nil { + panic(err) + } + + logger := slog.New( + slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{ + AddSource: true, + Level: slog.LevelDebug, + }), + ) + client := &http.Client{ + Timeout: time.Minute, + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + }, + } + // enable outgoing request logging + logging.EnableHTTPClient(client, + logging.WithClientGroup("client"), + ) + + options := []rp.Option{ + rp.WithCookieHandler(cookieHandler), + rp.WithVerifierOpts(rp.WithIssuedAtOffset(5 * time.Second)), + rp.WithHTTPClient(client), + rp.WithLogger(logger), + rp.WithSigningAlgsFromDiscovery(), + rp.WithCustomDiscoveryUrl(issuer + "/.well-known/openid-configuration"), + } + if clientSecret == "" { + options = append(options, rp.WithPKCE(cookieHandler)) + } + + // One can add a logger to the context, + // pre-defining log attributes as required. + ctx := logging.ToContext(context.TODO(), logger) + provider, err := rp.NewRelyingPartyOIDC(ctx, issuer, clientID, clientSecret, redirectURI, scopeList, options...) + if err != nil { + logrus.Fatalf("error creating provider %s", err.Error()) + } + + // generate some state (representing the state of the user in your application, + // e.g. the page where he was before sending him to login + state := func() string { + return uuid.New().String() + } + + urlOptions := []rp.URLParamOpt{ + rp.WithPromptURLParam("Welcome back!"), + } + + // register the AuthURLHandler at your preferred path. + // the AuthURLHandler creates the auth request and redirects the user to the auth server. + // including state handling with secure cookie and the possibility to use PKCE. + // Prompts can optionally be set to inform the server of + // any messages that need to be prompted back to the user. + http.Handle("/login", rp.AuthURLHandler( + state, + provider, + urlOptions..., + )) + + // for demonstration purposes the returned userinfo response is written as JSON object onto response + marshalUserinfo := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[*oidc.IDTokenClaims], state string, rp rp.RelyingParty, info *oidc.UserInfo) { + fmt.Println("access token", tokens.AccessToken) + fmt.Println("refresh token", tokens.RefreshToken) + fmt.Println("id token", tokens.IDToken) + + data, err := json.Marshal(info) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Header().Set("content-type", "application/json") + w.Write(data) + } + + // register the CodeExchangeHandler at the callbackPath + // the CodeExchangeHandler handles the auth response, creates the token request and calls the callback function + // with the returned tokens from the token endpoint + // in this example the callback function itself is wrapped by the UserinfoCallback which + // will call the Userinfo endpoint, check the sub and pass the info into the callback function + http.Handle(callbackPath, rp.CodeExchangeHandler(rp.UserinfoCallback(marshalUserinfo), provider)) + + // if you would use the callback without calling the userinfo endpoint, simply switch the callback handler for: + // + // http.Handle(callbackPath, rp.CodeExchangeHandler(marshalToken, provider)) + + // simple counter for request IDs + var counter atomic.Int64 + // enable incomming request logging + mw := logging.Middleware( + logging.WithLogger(logger), + logging.WithGroup("server"), + logging.WithIDFunc(func() slog.Attr { + return slog.Int64("id", counter.Add(1)) + }), + ) + + http.Handle("/healthy", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return })) + fmt.Println("/healthy returns 200 OK") + + server := &http.Server{ + Addr: ":" + port, + Handler: mw(http.DefaultServeMux), + } + go func() { + if err := server.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) { + log.Fatalf("HTTP server error: %v", err) + } + log.Println("Stopped serving new connections.") + }() + + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + <-sigChan + + shutdownCtx, shutdownRelease := context.WithTimeout(context.Background(), 10*time.Second) + defer shutdownRelease() + + if err := server.Shutdown(shutdownCtx); err != nil { + log.Fatalf("HTTP shutdown error: %v", err) + } +} + +func readPAT(path string) string { + f, err := os.Open(path) + if err != nil { + panic(err) + } + pat, err := io.ReadAll(f) + if err != nil { + panic(err) + } + return strings.Trim(string(pat), "\n") +} + +func createZitadelResources(apiURL, pat, domain, redirectURI, loginURL string) (string, string, error) { + projectID, err := CreateProject(apiURL, pat, domain) + if err != nil { + return "", "", err + } + return CreateApp(apiURL, pat, domain, projectID, redirectURI, loginURL) +} + +type project struct { + ID string `json:"id"` +} +type createProject struct { + Name string `json:"name"` + ProjectRoleAssertion bool `json:"projectRoleAssertion"` + ProjectRoleCheck bool `json:"projectRoleCheck"` + HasProjectCheck bool `json:"hasProjectCheck"` + PrivateLabelingSetting string `json:"privateLabelingSetting"` +} + +func CreateProject(apiURL, pat, domain string) (string, error) { + createProject := &createProject{ + Name: "OIDC", + ProjectRoleAssertion: false, + ProjectRoleCheck: false, + HasProjectCheck: false, + PrivateLabelingSetting: "PRIVATE_LABELING_SETTING_UNSPECIFIED", + } + resp, err := doRequestWithHeaders(apiURL+"/management/v1/projects", pat, domain, createProject) + if err != nil { + return "", err + } + data, err := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + defer resp.Body.Close() + + p := new(project) + if err := json.Unmarshal(data, p); err != nil { + return "", err + } + fmt.Printf("projectID: %+v\n", p.ID) + return p.ID, nil +} + +type createApp struct { + Name string `json:"name"` + RedirectUris []string `json:"redirectUris"` + ResponseTypes []string `json:"responseTypes"` + GrantTypes []string `json:"grantTypes"` + AppType string `json:"appType"` + AuthMethodType string `json:"authMethodType"` + PostLogoutRedirectUris []string `json:"postLogoutRedirectUris"` + Version string `json:"version"` + DevMode bool `json:"devMode"` + AccessTokenType string `json:"accessTokenType"` + AccessTokenRoleAssertion bool `json:"accessTokenRoleAssertion"` + IdTokenRoleAssertion bool `json:"idTokenRoleAssertion"` + IdTokenUserinfoAssertion bool `json:"idTokenUserinfoAssertion"` + ClockSkew string `json:"clockSkew"` + AdditionalOrigins []string `json:"additionalOrigins"` + SkipNativeAppSuccessPage bool `json:"skipNativeAppSuccessPage"` + BackChannelLogoutUri []string `json:"backChannelLogoutUri"` + LoginVersion version `json:"loginVersion"` +} + +type version struct { + LoginV2 loginV2 `json:"loginV2"` +} +type loginV2 struct { + BaseUri string `json:"baseUri"` +} + +type app struct { + ClientID string `json:"clientId"` + ClientSecret string `json:"clientSecret"` +} + +func CreateApp(apiURL, pat, domain, projectID string, redirectURI, loginURL string) (string, string, error) { + createApp := &createApp{ + Name: "OIDC", + RedirectUris: []string{redirectURI}, + ResponseTypes: []string{"OIDC_RESPONSE_TYPE_CODE"}, + GrantTypes: []string{"OIDC_GRANT_TYPE_AUTHORIZATION_CODE"}, + AppType: "OIDC_APP_TYPE_WEB", + AuthMethodType: "OIDC_AUTH_METHOD_TYPE_BASIC", + Version: "OIDC_VERSION_1_0", + DevMode: true, + AccessTokenType: "OIDC_TOKEN_TYPE_BEARER", + AccessTokenRoleAssertion: true, + IdTokenRoleAssertion: true, + IdTokenUserinfoAssertion: true, + ClockSkew: "1s", + SkipNativeAppSuccessPage: true, + LoginVersion: version{ + LoginV2: loginV2{ + BaseUri: loginURL, + }, + }, + } + + resp, err := doRequestWithHeaders(apiURL+"/management/v1/projects/"+projectID+"/apps/oidc", pat, domain, createApp) + data, err := io.ReadAll(resp.Body) + if err != nil { + return "", "", err + } + defer resp.Body.Close() + + a := new(app) + if err := json.Unmarshal(data, a); err != nil { + return "", "", err + } + return a.ClientID, a.ClientSecret, err +} + +func doRequestWithHeaders(apiURL, pat, domain string, body any) (*http.Response, error) { + data, err := json.Marshal(body) + if err != nil { + return nil, err + } + req, err := http.NewRequest(http.MethodPost, apiURL, io.NopCloser(bytes.NewReader(data))) + if err != nil { + return nil, err + } + values := http.Header{} + values.Add("Authorization", "Bearer "+pat) + values.Add("x-forwarded-host", domain) + values.Add("Content-Type", "application/json") + req.Header = values + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + return resp, nil +} diff --git a/apps/login/acceptance/package.json b/apps/login/acceptance/package.json new file mode 100644 index 0000000000..87cbe9a7e1 --- /dev/null +++ b/apps/login/acceptance/package.json @@ -0,0 +1,20 @@ +{ + "name": "login-test-acceptance", + "private": true, + "scripts": { + "test:acceptance": "dotenv -e ../login/.env.test.local playwright", + "test:acceptance:setup": "cd ../.. && make login_test_acceptance_setup_env && NODE_ENV=test turbo run test:acceptance:setup:dev", + "test:acceptance:setup:dev": "cd ../.. && make login_test_acceptance_setup_dev", + "clean": "rm -rf .turbo node_modules" + }, + "devDependencies": { + "@faker-js/faker": "^9.7.0", + "@otplib/core": "^12.0.0", + "@otplib/plugin-crypto": "^12.0.0", + "@otplib/plugin-thirty-two": "^12.0.0", + "@playwright/test": "^1.52.0", + "dotenv-cli": "^8.0.0", + "gaxios": "^7.1.0", + "typescript": "^5.8.3" + } +} diff --git a/apps/login/acceptance/pat/.gitignore b/apps/login/acceptance/pat/.gitignore new file mode 100644 index 0000000000..bf27f3114d --- /dev/null +++ b/apps/login/acceptance/pat/.gitignore @@ -0,0 +1,3 @@ +* +!.gitignore +!.gitkeep diff --git a/console/src/assets/.gitkeep b/apps/login/acceptance/pat/.gitkeep similarity index 100% rename from console/src/assets/.gitkeep rename to apps/login/acceptance/pat/.gitkeep diff --git a/apps/login/acceptance/playwright-report/.gitignore b/apps/login/acceptance/playwright-report/.gitignore new file mode 100644 index 0000000000..bf27f3114d --- /dev/null +++ b/apps/login/acceptance/playwright-report/.gitignore @@ -0,0 +1,3 @@ +* +!.gitignore +!.gitkeep diff --git a/build/zitadel/generate-grpc.sh b/apps/login/acceptance/playwright-report/.gitkeep old mode 100755 new mode 100644 similarity index 100% rename from build/zitadel/generate-grpc.sh rename to apps/login/acceptance/playwright-report/.gitkeep diff --git a/apps/login/acceptance/playwright.config.ts b/apps/login/acceptance/playwright.config.ts new file mode 100644 index 0000000000..8025db3238 --- /dev/null +++ b/apps/login/acceptance/playwright.config.ts @@ -0,0 +1,78 @@ +import { defineConfig, devices } from "@playwright/test"; +import dotenv from "dotenv"; +import path from "path"; + +dotenv.config({ path: path.resolve(__dirname, "../login/.env.test.local") }); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: "./tests", + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + expect: { + timeout: 10_000, // 10 seconds + }, + timeout: 300 * 1000, // 5 minutes + globalTimeout: 30 * 60_000, // 30 minutes + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: [ + ["line"], + ["html", { open: process.env.CI ? "never" : "on-failure", host: "0.0.0.0", outputFolder: "./playwright-report/html" }], + ], + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: process.env.LOGIN_BASE_URL || "http://127.0.0.1:3000", + trace: "retain-on-failure", + headless: true, + screenshot: "only-on-failure", + video: "retain-on-failure", + ignoreHTTPSErrors: true, + }, + outputDir: "test-results/results", + + /* Configure projects for major browsers */ + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + /* + { + name: "firefox", + use: { ...devices["Desktop Firefox"] }, + }, + TODO: webkit fails. Is this a bug? + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + }, + */ + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], +}); diff --git a/apps/login/acceptance/samlsp/go.mod b/apps/login/acceptance/samlsp/go.mod new file mode 100644 index 0000000000..9986149bfb --- /dev/null +++ b/apps/login/acceptance/samlsp/go.mod @@ -0,0 +1,18 @@ +module github.com/zitadel/typescript/acceptance/saml + +go 1.24.0 + +require github.com/crewjam/saml v0.4.14 + +require ( + github.com/beevik/etree v1.5.0 // indirect + github.com/crewjam/httperr v0.2.0 // indirect + github.com/golang-jwt/jwt/v4 v4.5.2 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/jonboulle/clockwork v0.5.0 // indirect + github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/russellhaering/goxmldsig v1.5.0 // indirect + github.com/stretchr/testify v1.10.0 // indirect + golang.org/x/crypto v0.36.0 // indirect +) diff --git a/apps/login/acceptance/samlsp/go.sum b/apps/login/acceptance/samlsp/go.sum new file mode 100644 index 0000000000..3394a39410 --- /dev/null +++ b/apps/login/acceptance/samlsp/go.sum @@ -0,0 +1,38 @@ +github.com/beevik/etree v1.5.0 h1:iaQZFSDS+3kYZiGoc9uKeOkUY3nYMXOKLl6KIJxiJWs= +github.com/beevik/etree v1.5.0/go.mod h1:gPNJNaBGVZ9AwsidazFZyygnd+0pAU38N4D+WemwKNs= +github.com/crewjam/httperr v0.2.0 h1:b2BfXR8U3AlIHwNeFFvZ+BV1LFvKLlzMjzaTnZMybNo= +github.com/crewjam/httperr v0.2.0/go.mod h1:Jlz+Sg/XqBQhyMjdDiC+GNNRzZTD7x39Gu3pglZ5oH4= +github.com/crewjam/saml v0.4.14 h1:g9FBNx62osKusnFzs3QTN5L9CVA/Egfgm+stJShzw/c= +github.com/crewjam/saml v0.4.14/go.mod h1:UVSZCf18jJkk6GpWNVqcyQJMD5HsRugBPf4I1nl2mME= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I= +github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60= +github.com/mattermost/xml-roundtrip-validator v0.1.0 h1:RXbVD2UAl7A7nOTR4u7E3ILa4IbtvKBHw64LDsmu9hU= +github.com/mattermost/xml-roundtrip-validator v0.1.0/go.mod h1:qccnGMcpgwcNaBnxqpJpWWUiPNr5H3O8eDgGV9gT5To= +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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russellhaering/goxmldsig v1.5.0 h1:AU2UkkYIUOTyZRbe08XMThaOCelArgvNfYapcmSjBNw= +github.com/russellhaering/goxmldsig v1.5.0/go.mod h1:x98CjQNFJcWfMxeOrMnMKg70lvDP6tE0nTaeUnjXDmk= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= diff --git a/apps/login/acceptance/samlsp/main.go b/apps/login/acceptance/samlsp/main.go new file mode 100644 index 0000000000..9dcfd13796 --- /dev/null +++ b/apps/login/acceptance/samlsp/main.go @@ -0,0 +1,271 @@ +package main + +import ( + "bytes" + "context" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "encoding/base64" + "encoding/json" + "encoding/xml" + "errors" + "fmt" + "io" + "log" + "net/http" + "net/url" + "os" + "os/signal" + "strings" + "syscall" + "time" + + "github.com/crewjam/saml/samlsp" +) + +var keyPair = func() tls.Certificate { + cert := []byte(`-----BEGIN CERTIFICATE----- +MIIDITCCAgmgAwIBAgIUKjAUmxsHO44X+/TKBNciPgNl1GEwDQYJKoZIhvcNAQEL +BQAwIDEeMBwGA1UEAwwVbXlzZXJ2aWNlLmV4YW1wbGUuY29tMB4XDTI0MTIxOTEz +Mzc1MVoXDTI1MTIxOTEzMzc1MVowIDEeMBwGA1UEAwwVbXlzZXJ2aWNlLmV4YW1w +bGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0QYuJsayILRI +hVT7G1DlitVSXnt1iw3gEXJZfe81Egz06fUbvXF6Yo1LJmwYpqe/rm+hf4FNUb8e +2O+LH2FieA9FkVe4P2gKOzw87A/KxvpV8stgNgl4LlqRCokbc1AzeE/NiLr5TcTD +RXm3DUcYxXxinprtDu2jftFysaOZmNAukvE/iL6qS3X6ggVEDDM7tY9n5FV2eJ4E +p0ImKfypi2aZYROxOK+v5x9ryFRMl4y07lMDvmtcV45uXYmfGNCgG9PNf91Kk/mh +JxEQbxycJwFoSi9XWljR8ahPdO11LXG7Dsj/RVbY8k2LdKNstl6Ae3aCpbe9u2Pj +vxYs1bVJuQIDAQABo1MwUTAdBgNVHQ4EFgQU+mRVN5HYJWgnpopReaLhf2cMcoYw +HwYDVR0jBBgwFoAU+mRVN5HYJWgnpopReaLhf2cMcoYwDwYDVR0TAQH/BAUwAwEB +/zANBgkqhkiG9w0BAQsFAAOCAQEABJpHVuc9tGhD04infRVlofvqXIUizTlOrjZX +vozW9pIhSWEHX8o+sJP8AMZLnrsdq+bm0HE0HvgYrw7Lb8pd4FpR46TkFHjeukoj +izqfgckjIBl2nwPGlynbKA0/U/rTCSxVt7XiAn+lgYUGIpOzNdk06/hRMitrMNB7 +t2C97NseVC4b1ZgyFrozsefCfUmD8IJF0+XJ4Wzmsh0jRrI8koCtVmPYnKn6vw1b +cZprg/97CWHYrsavd406wOB60CMtYl83Q16ucOF1dretDFqJC5kY+aFLvuqfag2+ +kIaoPV1MnGsxveQyyHdOsEatS5XOv/1OWcmnvePDPxcvb9jCcw== +-----END CERTIFICATE----- +`) + key := []byte(`-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDRBi4mxrIgtEiF +VPsbUOWK1VJee3WLDeARcll97zUSDPTp9Ru9cXpijUsmbBimp7+ub6F/gU1Rvx7Y +74sfYWJ4D0WRV7g/aAo7PDzsD8rG+lXyy2A2CXguWpEKiRtzUDN4T82IuvlNxMNF +ebcNRxjFfGKemu0O7aN+0XKxo5mY0C6S8T+IvqpLdfqCBUQMMzu1j2fkVXZ4ngSn +QiYp/KmLZplhE7E4r6/nH2vIVEyXjLTuUwO+a1xXjm5diZ8Y0KAb081/3UqT+aEn +ERBvHJwnAWhKL1daWNHxqE907XUtcbsOyP9FVtjyTYt0o2y2XoB7doKlt727Y+O/ +FizVtUm5AgMBAAECggEACak+l5f6Onj+u5vrjc4JyAaXW6ra6loSM9g8Uu3sHukW +plwoA7Pzp0u20CAxrP1Gpqw984/hSCCcb0Q2ItWMWLaC/YZni5W2WFnOyo3pzlPa +hmH4UNMT+ReCSfF/oW8w69QLcNEMjhfEu0i2iWBygIlA4SoRwC2Db6yEX7nLMwUB +6AICid9hfeACNRz/nq5ytdcHdmcB7Ptgb9jLiXr6RZw26g5AsRPHU3LdcyZAOXjP +aUHriHuHQFKAVkoEUxslvCB6ePCTCpB0bSAuzQbeGoY8fmvmNSCvJ1vrH5hiSUYp +Axtl5iNgFl5o9obb0eBYlY9x3pMSz0twdbCwfR7HAQKBgQDtWhmFm0NaJALoY+tq +lIIC0EOMSrcRIlgeXr6+g8womuDOMi5m/Nr5Mqt4mPOdP4HytrQb+a/ZmEm17KHh +mQb1vwH8ffirCBHbPNC1vwSNoxDKv9E6OysWlKiOzxPFSVZr3dKl2EMX6qi17n0l +LBrGXXaNPgYiHSmwBA5CZvvouQKBgQDhclGJfZfuoubQkUuz8yOA2uxalh/iUmQ/ +G8ac6/w7dmnL9pXehqCWh06SeC3ZvW7yrf7IIGx4sTJji2FzQ+8Ta6pPELMyBEXr +1VirIFrlNVMlMQEbZcbzdzEhchM1RUpZJtl3b4amvH21UcRB69d9klcDRisKoFRm +k0P9QLHpAQKBgQDh5J9nphZa4u0ViYtTW1XFIbs3+R/0IbCl7tww67TRbF3KQL4i +7EHna88ALumkXf3qJvKRsXgoaqS0jSqgUAjst8ZHLQkOldaQxneIkezedDSWEisp +9YgTrJYjnHefiyXB8VL63jE0wPOiewEF8Mzmv6sFz+L8cq7rQ2Di16qmmQKBgQDH +bvCwVxkrMpJK2O2GH8U9fOzu6bUE6eviY/jb4mp8U7EdjGJhuuieoM2iBoxQ/SID +rmYftYcfcWlo4+juJZ99p5W+YcCTs3IDQPUyVOnzr6uA0Avxp6RKxhsBQj+5tTUj +Dpn77P3JzB7MYqvhwPcdD3LH46+5s8FWCFpx02RPAQKBgARbngtggfifatcsMC7n +lSv/FVLH7LYQAHdoW/EH5Be7FeeP+eQvGXwh1dgl+u0VZO8FvI8RwFganpBRR2Nc +ZSBRIb0fSUlTvIsckSWjpEvUJUomJXyi4PIZAfNvd9/u1uLInQiCDtObwb6hnLTU +FHHEZ+dR4eMaJp6PhNm8hu2O +-----END PRIVATE KEY----- +`) + + kp, err := tls.X509KeyPair(cert, key) + if err != nil { + panic(err) + } + kp.Leaf, err = x509.ParseCertificate(kp.Certificate[0]) + if err != nil { + panic(err) + } + return kp +}() + +func hello(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "Hello, %s!", samlsp.AttributeFromContext(r.Context(), "UserName")) +} + +func main() { + apiURL := os.Getenv("API_URL") + pat := readPAT(os.Getenv("PAT_FILE")) + domain := os.Getenv("API_DOMAIN") + loginURL := os.Getenv("LOGIN_URL") + idpURL := os.Getenv("IDP_URL") + host := os.Getenv("HOST") + port := os.Getenv("PORT") + + idpMetadataURL, err := url.Parse(idpURL) + if err != nil { + panic(err) + } + idpMetadata, err := samlsp.FetchMetadata(context.Background(), http.DefaultClient, + *idpMetadataURL) + if err != nil { + panic(fmt.Errorf("failed to fetch IDP metadata from %s: %w", idpURL, err)) + } + fmt.Printf("idpMetadata: %+v\n", idpMetadata) + rootURL, err := url.Parse(host + ":" + port) + if err != nil { + panic(err) + } + + samlSP, err := samlsp.New(samlsp.Options{ + URL: *rootURL, + Key: keyPair.PrivateKey.(*rsa.PrivateKey), + Certificate: keyPair.Leaf, + IDPMetadata: idpMetadata, + }) + if err != nil { + panic(err) + } + + server := &http.Server{ + Addr: ":" + port, + } + app := http.HandlerFunc(hello) + http.Handle("/hello", samlSP.RequireAccount(app)) + http.Handle("/saml/", samlSP) + go func() { + if err := server.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) { + log.Fatalf("HTTP server error: %v", err) + } + log.Println("Stopped serving new connections.") + }() + + metadata, err := xml.MarshalIndent(samlSP.ServiceProvider.Metadata(), "", " ") + if err != nil { + panic(err) + } + if err := createZitadelResources(apiURL, pat, domain, metadata, loginURL); err != nil { + panic(err) + } + + http.Handle("/healthy", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return })) + fmt.Println("/healthy returns 200 OK") + + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + <-sigChan + + shutdownCtx, shutdownRelease := context.WithTimeout(context.Background(), 10*time.Second) + defer shutdownRelease() + + if err := server.Shutdown(shutdownCtx); err != nil { + log.Fatalf("HTTP shutdown error: %v", err) + } +} + +func readPAT(path string) string { + f, err := os.Open(path) + if err != nil { + panic(err) + } + pat, err := io.ReadAll(f) + if err != nil { + panic(err) + } + return strings.Trim(string(pat), "\n") +} + +func createZitadelResources(apiURL, pat, domain string, metadata []byte, loginURL string) error { + projectID, err := CreateProject(apiURL, pat, domain) + if err != nil { + return err + } + return CreateApp(apiURL, pat, domain, projectID, metadata, loginURL) +} + +type project struct { + ID string `json:"id"` +} +type createProject struct { + Name string `json:"name"` + ProjectRoleAssertion bool `json:"projectRoleAssertion"` + ProjectRoleCheck bool `json:"projectRoleCheck"` + HasProjectCheck bool `json:"hasProjectCheck"` + PrivateLabelingSetting string `json:"privateLabelingSetting"` +} + +func CreateProject(apiURL, pat, domain string) (string, error) { + createProject := &createProject{ + Name: "SAML", + ProjectRoleAssertion: false, + ProjectRoleCheck: false, + HasProjectCheck: false, + PrivateLabelingSetting: "PRIVATE_LABELING_SETTING_UNSPECIFIED", + } + resp, err := doRequestWithHeaders(apiURL+"/management/v1/projects", pat, domain, createProject) + if err != nil { + return "", err + } + data, err := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + defer resp.Body.Close() + + p := new(project) + if err := json.Unmarshal(data, p); err != nil { + return "", err + } + return p.ID, nil +} + +type createApp struct { + Name string `json:"name"` + MetadataXml string `json:"metadataXml"` + LoginVersion version `json:"loginVersion"` +} +type version struct { + LoginV2 loginV2 `json:"loginV2"` +} +type loginV2 struct { + BaseUri string `json:"baseUri"` +} + +func CreateApp(apiURL, pat, domain, projectID string, spMetadata []byte, loginURL string) error { + encoded := make([]byte, base64.URLEncoding.EncodedLen(len(spMetadata))) + base64.URLEncoding.Encode(encoded, spMetadata) + + createApp := &createApp{ + Name: "SAML", + MetadataXml: string(encoded), + LoginVersion: version{ + LoginV2: loginV2{ + BaseUri: loginURL, + }, + }, + } + _, err := doRequestWithHeaders(apiURL+"/management/v1/projects/"+projectID+"/apps/saml", pat, domain, createApp) + if err != nil { + return fmt.Errorf("error creating saml app with request %+v: %v", *createApp, err) + } + return err +} + +func doRequestWithHeaders(apiURL, pat, domain string, body any) (*http.Response, error) { + data, err := json.Marshal(body) + if err != nil { + return nil, err + } + req, err := http.NewRequest(http.MethodPost, apiURL, io.NopCloser(bytes.NewReader(data))) + if err != nil { + return nil, err + } + values := http.Header{} + values.Add("Authorization", "Bearer "+pat) + values.Add("x-forwarded-host", domain) + values.Add("Content-Type", "application/json") + req.Header = values + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + return resp, nil +} diff --git a/apps/login/acceptance/setup/go.mod b/apps/login/acceptance/setup/go.mod new file mode 100644 index 0000000000..7be166ef9b --- /dev/null +++ b/apps/login/acceptance/setup/go.mod @@ -0,0 +1,3 @@ +module github.com/zitadel/typescript/apps/login-test-acceptance/setup + +go 1.23.3 diff --git a/apps/login/acceptance/setup/go.sum b/apps/login/acceptance/setup/go.sum new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/login/acceptance/setup/main.go b/apps/login/acceptance/setup/main.go new file mode 100644 index 0000000000..38dd16da61 --- /dev/null +++ b/apps/login/acceptance/setup/main.go @@ -0,0 +1,3 @@ +package main + +func main() {} diff --git a/apps/login/acceptance/setup/setup.sh b/apps/login/acceptance/setup/setup.sh new file mode 100755 index 0000000000..9d1a04e18f --- /dev/null +++ b/apps/login/acceptance/setup/setup.sh @@ -0,0 +1,139 @@ +#!/bin/sh + +set -e pipefail + +PAT_FILE=${PAT_FILE:-./pat/zitadel-admin-sa.pat} +LOGIN_BASE_URL=${LOGIN_BASE_URL:-"http://localhost:3000"} +ZITADEL_API_PROTOCOL="${ZITADEL_API_PROTOCOL:-http}" +ZITADEL_API_DOMAIN="${ZITADEL_API_DOMAIN:-localhost}" +ZITADEL_API_PORT="${ZITADEL_API_PORT:-8080}" +ZITADEL_API_URL="${ZITADEL_API_URL:-${ZITADEL_API_PROTOCOL}://${ZITADEL_API_DOMAIN}:${ZITADEL_API_PORT}}" +ZITADEL_API_INTERNAL_URL="${ZITADEL_API_INTERNAL_URL:-${ZITADEL_API_URL}}" +SINK_EMAIL_INTERNAL_URL="${SINK_EMAIL_INTERNAL_URL:-"http://sink:3333/email"}" +SINK_SMS_INTERNAL_URL="${SINK_SMS_INTERNAL_URL:-"http://sink:3333/sms"}" +SINK_NOTIFICATION_URL="${SINK_NOTIFICATION_URL:-"http://localhost:3333/notification"}" +WRITE_ENVIRONMENT_FILE=${WRITE_ENVIRONMENT_FILE:-$(dirname "$0")/../apps/login/.env.test.local} + +if [ -z "${PAT}" ]; then + echo "Reading PAT from file ${PAT_FILE}" + PAT=$(cat ${PAT_FILE}) +fi + +################################################################# +# ServiceAccount as Login Client +################################################################# + +SERVICEACCOUNT_RESPONSE=$(curl -s --request POST \ + --url "${ZITADEL_API_INTERNAL_URL}/management/v1/users/machine" \ + --header "Authorization: Bearer ${PAT}" \ + --header "Host: ${ZITADEL_API_DOMAIN}" \ + --header "Content-Type: application/json" \ + -d "{\"userName\": \"login\", \"name\": \"Login v2\", \"description\": \"Serviceaccount for Login v2\", \"accessTokenType\": \"ACCESS_TOKEN_TYPE_BEARER\"}") +echo "Received ServiceAccount response: ${SERVICEACCOUNT_RESPONSE}" + +SERVICEACCOUNT_ID=$(echo ${SERVICEACCOUNT_RESPONSE} | jq -r '. | .userId') +echo "Received ServiceAccount ID: ${SERVICEACCOUNT_ID}" + +MEMBER_RESPONSE=$(curl -s --request POST \ + --url "${ZITADEL_API_INTERNAL_URL}/admin/v1/members" \ + --header "Authorization: Bearer ${PAT}" \ + --header "Host: ${ZITADEL_API_DOMAIN}" \ + --header "Content-Type: application/json" \ + -d "{\"userId\": \"${SERVICEACCOUNT_ID}\", \"roles\": [\"IAM_LOGIN_CLIENT\"]}") +echo "Received Member response: ${MEMBER_RESPONSE}" + +SA_PAT_RESPONSE=$(curl -s --request POST \ + --url "${ZITADEL_API_INTERNAL_URL}/management/v1/users/${SERVICEACCOUNT_ID}/pats" \ + --header "Authorization: Bearer ${PAT}" \ + --header "Host: ${ZITADEL_API_DOMAIN}" \ + --header "Content-Type: application/json" \ + -d "{\"expirationDate\": \"2519-04-01T08:45:00.000000Z\"}") +echo "Received Member response: ${MEMBER_RESPONSE}" + +SA_PAT=$(echo ${SA_PAT_RESPONSE} | jq -r '. | .token') +echo "Received ServiceAccount Token: ${SA_PAT}" + +################################################################# +# Environment files +################################################################# + +echo "Writing environment file ${WRITE_ENVIRONMENT_FILE}." + +echo "ZITADEL_API_URL=${ZITADEL_API_URL} +ZITADEL_SERVICE_USER_TOKEN=${SA_PAT} +ZITADEL_ADMIN_TOKEN=${PAT} +SINK_NOTIFICATION_URL=${SINK_NOTIFICATION_URL} +EMAIL_VERIFICATION=true +DEBUG=false +LOGIN_BASE_URL=${LOGIN_BASE_URL} +NODE_TLS_REJECT_UNAUTHORIZED=0 +ZITADEL_ADMIN_USER=${ZITADEL_ADMIN_USER:-"zitadel-admin@zitadel.localhost"} +NEXT_PUBLIC_BASE_PATH=/ui/v2/login +" > ${WRITE_ENVIRONMENT_FILE} + +echo "Wrote environment file ${WRITE_ENVIRONMENT_FILE}" +cat ${WRITE_ENVIRONMENT_FILE} + +################################################################# +# SMS provider with HTTP +################################################################# + +SMSHTTP_RESPONSE=$(curl -s --request POST \ + --url "${ZITADEL_API_INTERNAL_URL}/admin/v1/sms/http" \ + --header "Authorization: Bearer ${PAT}" \ + --header "Host: ${ZITADEL_API_DOMAIN}" \ + --header "Content-Type: application/json" \ + -d "{\"endpoint\": \"${SINK_SMS_INTERNAL_URL}\", \"description\": \"test\"}") +echo "Received SMS HTTP response: ${SMSHTTP_RESPONSE}" + +SMSHTTP_ID=$(echo ${SMSHTTP_RESPONSE} | jq -r '. | .id') +echo "Received SMS HTTP ID: ${SMSHTTP_ID}" + +SMS_ACTIVE_RESPONSE=$(curl -s --request POST \ + --url "${ZITADEL_API_INTERNAL_URL}/admin/v1/sms/${SMSHTTP_ID}/_activate" \ + --header "Authorization: Bearer ${PAT}" \ + --header "Host: ${ZITADEL_API_DOMAIN}" \ + --header "Content-Type: application/json") +echo "Received SMS active response: ${SMS_ACTIVE_RESPONSE}" + +################################################################# +# Email provider with HTTP +################################################################# + +EMAILHTTP_RESPONSE=$(curl -s --request POST \ + --url "${ZITADEL_API_INTERNAL_URL}/admin/v1/email/http" \ + --header "Authorization: Bearer ${PAT}" \ + --header "Host: ${ZITADEL_API_DOMAIN}" \ + --header "Content-Type: application/json" \ + -d "{\"endpoint\": \"${SINK_EMAIL_INTERNAL_URL}\", \"description\": \"test\"}") +echo "Received Email HTTP response: ${EMAILHTTP_RESPONSE}" + +EMAILHTTP_ID=$(echo ${EMAILHTTP_RESPONSE} | jq -r '. | .id') +echo "Received Email HTTP ID: ${EMAILHTTP_ID}" + +EMAIL_ACTIVE_RESPONSE=$(curl -s --request POST \ + --url "${ZITADEL_API_INTERNAL_URL}/admin/v1/email/${EMAILHTTP_ID}/_activate" \ + --header "Authorization: Bearer ${PAT}" \ + --header "Host: ${ZITADEL_API_DOMAIN}" \ + --header "Content-Type: application/json") +echo "Received Email active response: ${EMAIL_ACTIVE_RESPONSE}" + +################################################################# +# Wait for projection of default organization in ZITADEL +################################################################# + +DEFAULTORG_RESPONSE_RESULTS=0 +# waiting for default organization +until [ ${DEFAULTORG_RESPONSE_RESULTS} -eq 1 ] +do + DEFAULTORG_RESPONSE=$(curl -s --request POST \ + --url "${ZITADEL_API_INTERNAL_URL}/v2/organizations/_search" \ + --header "Authorization: Bearer ${PAT}" \ + --header "Host: ${ZITADEL_API_DOMAIN}" \ + --header "Content-Type: application/json" \ + -d "{\"queries\": [{\"defaultQuery\":{}}]}" ) + echo "Received default organization response: ${DEFAULTORG_RESPONSE}" + DEFAULTORG_RESPONSE_RESULTS=$(echo $DEFAULTORG_RESPONSE | jq -r '.result | length') + echo "Received default organization response result: ${DEFAULTORG_RESPONSE_RESULTS}" +done + diff --git a/apps/login/acceptance/sink/go.mod b/apps/login/acceptance/sink/go.mod new file mode 100644 index 0000000000..1da7622b58 --- /dev/null +++ b/apps/login/acceptance/sink/go.mod @@ -0,0 +1,3 @@ +module github.com/zitadel/typescript/acceptance/sink + +go 1.24.0 diff --git a/apps/login/acceptance/sink/go.sum b/apps/login/acceptance/sink/go.sum new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/login/acceptance/sink/main.go b/apps/login/acceptance/sink/main.go new file mode 100644 index 0000000000..f3795ba0d0 --- /dev/null +++ b/apps/login/acceptance/sink/main.go @@ -0,0 +1,111 @@ +package main + +import ( + "encoding/json" + "flag" + "fmt" + "io" + "net/http" +) + +type serializableData struct { + ContextInfo map[string]interface{} `json:"contextInfo,omitempty"` + Args map[string]interface{} `json:"args,omitempty"` +} + +type response struct { + Recipient string `json:"recipient,omitempty"` +} + +func main() { + port := flag.String("port", "3333", "used port for the sink") + email := flag.String("email", "/email", "path for a sent email") + emailKey := flag.String("email-key", "recipientEmailAddress", "value in the sent context info of the email used as key to retrieve the notification") + sms := flag.String("sms", "/sms", "path for a sent sms") + smsKey := flag.String("sms-key", "recipientPhoneNumber", "value in the sent context info of the sms used as key to retrieve the notification") + notification := flag.String("notification", "/notification", "path to receive the notification") + flag.Parse() + + messages := make(map[string]serializableData) + + http.HandleFunc(*email, func(w http.ResponseWriter, r *http.Request) { + data, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + serializableData := serializableData{} + if err := json.Unmarshal(data, &serializableData); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + email, ok := serializableData.ContextInfo[*emailKey].(string) + if !ok { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + fmt.Println(email + ": " + string(data)) + messages[email] = serializableData + io.WriteString(w, "Email!\n") + }) + + http.HandleFunc(*sms, func(w http.ResponseWriter, r *http.Request) { + data, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + serializableData := serializableData{} + if err := json.Unmarshal(data, &serializableData); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + phone, ok := serializableData.ContextInfo[*smsKey].(string) + if !ok { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + fmt.Println(phone + ": " + string(data)) + messages[phone] = serializableData + io.WriteString(w, "SMS!\n") + }) + + http.HandleFunc(*notification, func(w http.ResponseWriter, r *http.Request) { + data, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + response := response{} + if err := json.Unmarshal(data, &response); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + msg, ok := messages[response.Recipient] + if !ok { + http.Error(w, "No messages found for recipient: "+response.Recipient, http.StatusNotFound) + return + } + serializableData, err := json.Marshal(msg) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") + io.WriteString(w, string(serializableData)) + }) + + fmt.Println("Starting server on", *port) + fmt.Println(*email, " for email handling") + fmt.Println(*sms, " for sms handling") + fmt.Println(*notification, " for retrieving notifications") + http.Handle("/healthy", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return })) + fmt.Println("/healthy returns 200 OK") + err := http.ListenAndServe(":"+*port, nil) + if err != nil { + panic("Server could not be started: " + err.Error()) + } +} diff --git a/apps/login/acceptance/test-results/.gitignore b/apps/login/acceptance/test-results/.gitignore new file mode 100644 index 0000000000..bf27f3114d --- /dev/null +++ b/apps/login/acceptance/test-results/.gitignore @@ -0,0 +1,3 @@ +* +!.gitignore +!.gitkeep diff --git a/apps/login/acceptance/test-results/.gitkeep b/apps/login/acceptance/test-results/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/login/acceptance/tests/admin.spec.ts b/apps/login/acceptance/tests/admin.spec.ts new file mode 100644 index 0000000000..13b748fc63 --- /dev/null +++ b/apps/login/acceptance/tests/admin.spec.ts @@ -0,0 +1,7 @@ +import { test } from "@playwright/test"; +import { loginScreenExpect, loginWithPassword } from "./login"; + +test("admin login", async ({ page }) => { + await loginWithPassword(page, process.env["ZITADEL_ADMIN_USER"], "Password1!"); + await loginScreenExpect(page, "ZITADEL Admin"); +}); diff --git a/apps/login/acceptance/tests/code-screen.ts b/apps/login/acceptance/tests/code-screen.ts new file mode 100644 index 0000000000..3ab9dad26d --- /dev/null +++ b/apps/login/acceptance/tests/code-screen.ts @@ -0,0 +1,12 @@ +import { expect, Page } from "@playwright/test"; + +const codeTextInput = "code-text-input"; + +export async function codeScreen(page: Page, code: string) { + await page.getByTestId(codeTextInput).pressSequentially(code); +} + +export async function codeScreenExpect(page: Page, code: string) { + await expect(page.getByTestId(codeTextInput)).toHaveValue(code); + await expect(page.getByTestId("error").locator("div")).toContainText("Could not verify OTP code"); +} diff --git a/apps/login/acceptance/tests/code.ts b/apps/login/acceptance/tests/code.ts new file mode 100644 index 0000000000..e27d1f6150 --- /dev/null +++ b/apps/login/acceptance/tests/code.ts @@ -0,0 +1,17 @@ +import { Page } from "@playwright/test"; +import { codeScreen } from "./code-screen"; +import { getOtpFromSink } from "./sink"; + +export async function otpFromSink(page: Page, key: string) { + const c = await getOtpFromSink(key); + await code(page, c); +} + +export async function code(page: Page, code: string) { + await codeScreen(page, code); + await page.getByTestId("submit-button").click(); +} + +export async function codeResend(page: Page) { + await page.getByTestId("resend-button").click(); +} diff --git a/apps/login/acceptance/tests/email-verify-screen.ts b/apps/login/acceptance/tests/email-verify-screen.ts new file mode 100644 index 0000000000..b077ecb424 --- /dev/null +++ b/apps/login/acceptance/tests/email-verify-screen.ts @@ -0,0 +1,12 @@ +import { expect, Page } from "@playwright/test"; + +const codeTextInput = "code-text-input"; + +export async function emailVerifyScreen(page: Page, code: string) { + await page.getByTestId(codeTextInput).pressSequentially(code); +} + +export async function emailVerifyScreenExpect(page: Page, code: string) { + await expect(page.getByTestId(codeTextInput)).toHaveValue(code); + await expect(page.getByTestId("error").locator("div")).toContainText("Could not verify email"); +} diff --git a/apps/login/acceptance/tests/email-verify.spec.ts b/apps/login/acceptance/tests/email-verify.spec.ts new file mode 100644 index 0000000000..2c546b8eee --- /dev/null +++ b/apps/login/acceptance/tests/email-verify.spec.ts @@ -0,0 +1,69 @@ +import { faker } from "@faker-js/faker"; +import { test as base } from "@playwright/test"; +import dotenv from "dotenv"; +import path from "path"; +import { emailVerify, emailVerifyResend } from "./email-verify"; +import { emailVerifyScreenExpect } from "./email-verify-screen"; +import { loginScreenExpect, loginWithPassword } from "./login"; +import { getCodeFromSink } from "./sink"; +import { PasswordUser } from "./user"; + +// Read from ".env" file. +dotenv.config({ path: path.resolve(__dirname, "../../login/.env.test.local") }); + +const test = base.extend<{ user: PasswordUser }>({ + user: async ({ page }, use) => { + const user = new PasswordUser({ + email: faker.internet.email(), + isEmailVerified: false, + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + organization: "", + phone: faker.phone.number(), + isPhoneVerified: false, + password: "Password1!", + passwordChangeRequired: false, + }); + await user.ensure(page); + await use(user); + await user.cleanup(); + }, +}); + +test("user email not verified, verify", async ({ user, page }) => { + await loginWithPassword(page, user.getUsername(), user.getPassword()); + const c = await getCodeFromSink(user.getUsername()); + await emailVerify(page, c); + // wait for resend of the code + await page.waitForTimeout(2000); + await loginScreenExpect(page, user.getFullName()); +}); + +test("user email not verified, resend, verify", async ({ user, page }) => { + await loginWithPassword(page, user.getUsername(), user.getPassword()); + // auto-redirect on /verify + await emailVerifyResend(page); + const c = await getCodeFromSink(user.getUsername()); + // wait for resend of the code + await page.waitForTimeout(2000); + await emailVerify(page, c); + await loginScreenExpect(page, user.getFullName()); +}); + +test("user email not verified, resend, old code", async ({ user, page }) => { + await loginWithPassword(page, user.getUsername(), user.getPassword()); + const c = await getCodeFromSink(user.getUsername()); + await emailVerifyResend(page); + // wait for resend of the code + await page.waitForTimeout(2000); + await emailVerify(page, c); + await emailVerifyScreenExpect(page, c); +}); + +test("user email not verified, wrong code", async ({ user, page }) => { + await loginWithPassword(page, user.getUsername(), user.getPassword()); + // auto-redirect on /verify + const code = "wrong"; + await emailVerify(page, code); + await emailVerifyScreenExpect(page, code); +}); diff --git a/apps/login/acceptance/tests/email-verify.ts b/apps/login/acceptance/tests/email-verify.ts new file mode 100644 index 0000000000..5275e82bfe --- /dev/null +++ b/apps/login/acceptance/tests/email-verify.ts @@ -0,0 +1,15 @@ +import { Page } from "@playwright/test"; +import { emailVerifyScreen } from "./email-verify-screen"; + +export async function startEmailVerify(page: Page, loginname: string) { + await page.goto("./verify"); +} + +export async function emailVerify(page: Page, code: string) { + await emailVerifyScreen(page, code); + await page.getByTestId("submit-button").click(); +} + +export async function emailVerifyResend(page: Page) { + await page.getByTestId("resend-button").click(); +} diff --git a/apps/login/acceptance/tests/idp-apple.spec.ts b/apps/login/acceptance/tests/idp-apple.spec.ts new file mode 100644 index 0000000000..32d3adba6b --- /dev/null +++ b/apps/login/acceptance/tests/idp-apple.spec.ts @@ -0,0 +1,102 @@ +// Note for all tests, in case Apple doesn't deliver all relevant information per default +// We should add an action in the needed cases + +import test from "@playwright/test"; + +test("login with Apple IDP", async ({ page }) => { + test.skip(); + // Given an Apple IDP is configured on the organization + // Given the user has an Apple added as auth method + // User authenticates with Apple + // User is redirected back to login + // User is redirected to the app +}); + +test("login with Apple IDP - error", async ({ page }) => { + test.skip(); + // Given an Apple IDP is configured on the organization + // Given the user has an Apple added as auth method + // User is redirected to Apple + // User authenticates with Apple and gets an error + // User is redirect back to login + // An error is shown to the user "Something went wrong in Apple Login" +}); + +test("login with Apple IDP, no user existing - auto register", async ({ page }) => { + test.skip(); + // Given idp Apple is configure on the organization as only authencation method + // Given idp Apple is configure with account creation alloweed, and automatic creation enabled + // Given no user exists yet + // User is automatically redirected to Apple + // User authenticates in Apple + // User is redirect to ZITADEL login + // User is created in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with Apple IDP, no user existing - auto register not possible", async ({ page }) => { + test.skip(); + // Given idp Apple is configure on the organization as only authencation method + // Given idp Apple is configure with account creation alloweed, and automatic creation enabled + // Given no user exists yet + // User is automatically redirected to Apple + // User authenticates in Apple + // User is redirect to ZITADEL login + // Because of missing informaiton on the user auto creation is not possible + // User will see the registration page with pre filled user information + // User fills missing information + // User clicks register button + // User is created in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with Apple IDP, no user existing - auto register enabled - manual creation disabled, creation not possible", async ({ + page, +}) => { + test.skip(); + // Given idp Apple is configure on the organization as only authencation method + // Given idp Apple is configure with account creation not allowed, and automatic creation enabled + // Given no user exists yet + // User is automatically redirected to Apple + // User authenticates in Apple + // User is redirect to ZITADEL login + // Because of missing informaiton on the user auto creation is not possible + // Error message is shown, that registration of the user was not possible due to missing information +}); + +test("login with Apple IDP, no user linked - auto link", async ({ page }) => { + test.skip(); + // Given idp Apple is configure on the organization as only authencation method + // Given idp Apple is configure with account linking allowed, and linking set to existing email + // Given user with email address user@zitadel.com exists + // User is automatically redirected to Apple + // User authenticates in Apple with user@zitadel.com + // User is redirect to ZITADEL login + // User is linked with existing user in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with Apple IDP, no user linked, linking not possible", async ({ page }) => { + test.skip(); + // Given idp Apple is configure on the organization as only authencation method + // Given idp Apple is configure with manually account linking not allowed, and linking set to existing email + // Given user with email address user@zitadel.com doesn't exists + // User is automatically redirected to Apple + // User authenticates in Apple with user@zitadel.com + // User is redirect to ZITADEL login + // User with email address user@zitadel.com can not be found + // User will get an error message that account linking wasn't possible +}); + +test("login with Apple IDP, no user linked, user link successful", async ({ page }) => { + test.skip(); + // Given idp Apple is configure on the organization as only authencation method + // Given idp Apple is configure with manually account linking allowed, and linking set to existing email + // Given user with email address user@zitadel.com doesn't exists + // User is automatically redirected to Apple + // User authenticates in Apple with user@zitadel.com + // User is redirect to ZITADEL login + // User with email address user@zitadel.com can not be found + // User is prompted to link the account manually + // User is redirected to the app (default redirect url) +}); diff --git a/apps/login/acceptance/tests/idp-generic-jwt.spec.ts b/apps/login/acceptance/tests/idp-generic-jwt.spec.ts new file mode 100644 index 0000000000..d68475a226 --- /dev/null +++ b/apps/login/acceptance/tests/idp-generic-jwt.spec.ts @@ -0,0 +1,99 @@ +import test from "@playwright/test"; + +test("login with Generic JWT IDP", async ({ page }) => { + test.skip(); + // Given a Generic JWT IDP is configured on the organization + // Given the user has Generic JWT IDP added as auth method + // User authenticates with the Generic JWT IDP + // User is redirected back to login + // User is redirected to the app +}); + +test("login with Generic JWT IDP - error", async ({ page }) => { + test.skip(); + // Given the Generic JWT IDP is configured on the organization + // Given the user has Generic JWT IDP added as auth method + // User is redirected to the Generic JWT IDP + // User authenticates with the Generic JWT IDP and gets an error + // User is redirected back to login + // An error is shown to the user "Something went wrong" +}); + +test("login with Generic JWT IDP, no user existing - auto register", async ({ page }) => { + test.skip(); + // Given idp Generic JWT is configure on the organization as only authencation method + // Given idp Generic JWT is configure with account creation alloweed, and automatic creation enabled + // Given no user exists yet + // User is automatically redirected to Generic JWT + // User authenticates in Generic JWT + // User is redirect to ZITADEL login + // User is created in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with Generic JWT IDP, no user existing - auto register not possible", async ({ page }) => { + test.skip(); + // Given idp Generic JWT is configure on the organization as only authencation method + // Given idp Generic JWT is configure with account creation alloweed, and automatic creation enabled + // Given no user exists yet + // User is automatically redirected to Generic JWT + // User authenticates in Generic JWT + // User is redirect to ZITADEL login + // Because of missing informaiton on the user auto creation is not possible + // User will see the registration page with pre filled user information + // User fills missing information + // User clicks register button + // User is created in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with Generic JWT IDP, no user existing - auto register enabled - manual creation disabled, creation not possible", async ({ + page, +}) => { + test.skip(); + // Given idp Generic JWT is configure on the organization as only authencation method + // Given idp Generic JWT is configure with account creation not allowed, and automatic creation enabled + // Given no user exists yet + // User is automatically redirected to Generic JWT + // User authenticates in Generic JWT + // User is redirect to ZITADEL login + // Because of missing informaiton on the user auto creation is not possible + // Error message is shown, that registration of the user was not possible due to missing information +}); + +test("login with Generic JWT IDP, no user linked - auto link", async ({ page }) => { + test.skip(); + // Given idp Generic JWT is configure on the organization as only authencation method + // Given idp Generic JWT is configure with account linking allowed, and linking set to existing email + // Given user with email address user@zitadel.com exists + // User is automatically redirected to Generic JWT + // User authenticates in Generic JWT with user@zitadel.com + // User is redirect to ZITADEL login + // User is linked with existing user in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with Generic JWT IDP, no user linked, linking not possible", async ({ page }) => { + test.skip(); + // Given idp Generic JWT is configure on the organization as only authencation method + // Given idp Generic JWT is configure with manually account linking not allowed, and linking set to existing email + // Given user with email address user@zitadel.com doesn't exists + // User is automatically redirected to Generic JWT + // User authenticates in Generic JWT with user@zitadel.com + // User is redirect to ZITADEL login + // User with email address user@zitadel.com can not be found + // User will get an error message that account linking wasn't possible +}); + +test("login with Generic JWT IDP, no user linked, linking successful", async ({ page }) => { + test.skip(); + // Given idp Generic JWT is configure on the organization as only authencation method + // Given idp Generic JWT is configure with manually account linking allowed, and linking set to existing email + // Given user with email address user@zitadel.com doesn't exists + // User is automatically redirected to Generic JWT + // User authenticates in Generic JWT with user@zitadel.com + // User is redirect to ZITADEL login + // User with email address user@zitadel.com can not be found + // User is prompted to link the account manually + // User is redirected to the app (default redirect url) +}); diff --git a/apps/login/acceptance/tests/idp-generic-oauth.spec.ts b/apps/login/acceptance/tests/idp-generic-oauth.spec.ts new file mode 100644 index 0000000000..24c25d0005 --- /dev/null +++ b/apps/login/acceptance/tests/idp-generic-oauth.spec.ts @@ -0,0 +1,99 @@ +import test from "@playwright/test"; + +test("login with Generic OAuth IDP", async ({ page }) => { + test.skip(); + // Given a Generic OAuth IDP is configured on the organization + // Given the user has Generic OAuth IDP added as auth method + // User authenticates with the Generic OAuth IDP + // User is redirected back to login + // User is redirected to the app +}); + +test("login with Generic OAuth IDP - error", async ({ page }) => { + test.skip(); + // Given the Generic OAuth IDP is configured on the organization + // Given the user has Generic OAuth IDP added as auth method + // User is redirected to the Generic OAuth IDP + // User authenticates with the Generic OAuth IDP and gets an error + // User is redirected back to login + // An error is shown to the user "Something went wrong" +}); + +test("login with Generic OAuth IDP, no user existing - auto register", async ({ page }) => { + test.skip(); + // Given idp Generic OAuth is configure on the organization as only authencation method + // Given idp Generic OAuth is configure with account creation alloweed, and automatic creation enabled + // Given no user exists yet + // User is automatically redirected to Generic OAuth + // User authenticates in Generic OAuth + // User is redirect to ZITADEL login + // User is created in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with Generic OAuth IDP, no user existing - auto register not possible", async ({ page }) => { + test.skip(); + // Given idp Generic OAuth is configure on the organization as only authencation method + // Given idp Generic OAuth is configure with account creation alloweed, and automatic creation enabled + // Given no user exists yet + // User is automatically redirected to Generic OAuth + // User authenticates in Generic OAuth + // User is redirect to ZITADEL login + // Because of missing informaiton on the user auto creation is not possible + // User will see the registration page with pre filled user information + // User fills missing information + // User clicks register button + // User is created in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with Generic OAuth IDP, no user existing - auto register enabled - manual creation disabled, creation not possible", async ({ + page, +}) => { + test.skip(); + // Given idp Generic OAuth is configure on the organization as only authencation method + // Given idp Generic OAuth is configure with account creation not allowed, and automatic creation enabled + // Given no user exists yet + // User is automatically redirected to Generic OAuth + // User authenticates in Generic OAuth + // User is redirect to ZITADEL login + // Because of missing informaiton on the user auto creation is not possible + // Error message is shown, that registration of the user was not possible due to missing information +}); + +test("login with Generic OAuth IDP, no user linked - auto link", async ({ page }) => { + test.skip(); + // Given idp Generic OAuth is configure on the organization as only authencation method + // Given idp Generic OAuth is configure with account linking allowed, and linking set to existing email + // Given user with email address user@zitadel.com exists + // User is automatically redirected to Generic OAuth + // User authenticates in Generic OAuth with user@zitadel.com + // User is redirect to ZITADEL login + // User is linked with existing user in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with Generic OAuth IDP, no user linked, linking not possible", async ({ page }) => { + test.skip(); + // Given idp Generic OAuth is configure on the organization as only authencation method + // Given idp Generic OAuth is configure with manually account linking not allowed, and linking set to existing email + // Given user with email address user@zitadel.com doesn't exists + // User is automatically redirected to Generic OAuth + // User authenticates in Generic OAuth with user@zitadel.com + // User is redirect to ZITADEL login + // User with email address user@zitadel.com can not be found + // User will get an error message that account linking wasn't possible +}); + +test("login with Generic OAuth IDP, no user linked, linking successful", async ({ page }) => { + test.skip(); + // Given idp Generic OAuth is configure on the organization as only authencation method + // Given idp Generic OAuth is configure with manually account linking allowed, and linking set to existing email + // Given user with email address user@zitadel.com doesn't exists + // User is automatically redirected to Generic OAuth + // User authenticates in Generic OAuth with user@zitadel.com + // User is redirect to ZITADEL login + // User with email address user@zitadel.com can not be found + // User is prompted to link the account manually + // User is redirected to the app (default redirect url) +}); diff --git a/apps/login/acceptance/tests/idp-generic-oidc.spec.ts b/apps/login/acceptance/tests/idp-generic-oidc.spec.ts new file mode 100644 index 0000000000..391481f99d --- /dev/null +++ b/apps/login/acceptance/tests/idp-generic-oidc.spec.ts @@ -0,0 +1,101 @@ +// Note, we should use a provider such as Google to test this, where we know OIDC standard is properly implemented + +import test from "@playwright/test"; + +test("login with Generic OIDC IDP", async ({ page }) => { + test.skip(); + // Given a Generic OIDC IDP is configured on the organization + // Given the user has Generic OIDC IDP added as auth method + // User authenticates with the Generic OIDC IDP + // User is redirected back to login + // User is redirected to the app +}); + +test("login with Generic OIDC IDP - error", async ({ page }) => { + test.skip(); + // Given the Generic OIDC IDP is configured on the organization + // Given the user has Generic OIDC IDP added as auth method + // User is redirected to the Generic OIDC IDP + // User authenticates with the Generic OIDC IDP and gets an error + // User is redirected back to login + // An error is shown to the user "Something went wrong" +}); + +test("login with Generic OIDC IDP, no user existing - auto register", async ({ page }) => { + test.skip(); + // Given idp Generic OIDC is configure on the organization as only authencation method + // Given idp Generic OIDC is configure with account creation alloweed, and automatic creation enabled + // Given no user exists yet + // User is automatically redirected to Generic OIDC + // User authenticates in Generic OIDC + // User is redirect to ZITADEL login + // User is created in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with Generic OIDC IDP, no user existing - auto register not possible", async ({ page }) => { + test.skip(); + // Given idp Generic OIDC is configure on the organization as only authencation method + // Given idp Generic OIDC is configure with account creation alloweed, and automatic creation enabled + // Given no user exists yet + // User is automatically redirected to Generic OIDC + // User authenticates in Generic OIDC + // User is redirect to ZITADEL login + // Because of missing informaiton on the user auto creation is not possible + // User will see the registration page with pre filled user information + // User fills missing information + // User clicks register button + // User is created in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with Generic OIDC IDP, no user existing - auto register enabled - manual creation disabled, creation not possible", async ({ + page, +}) => { + test.skip(); + // Given idp Generic OIDC is configure on the organization as only authencation method + // Given idp Generic OIDC is configure with account creation not allowed, and automatic creation enabled + // Given no user exists yet + // User is automatically redirected to Generic OIDC + // User authenticates in Generic OIDC + // User is redirect to ZITADEL login + // Because of missing informaiton on the user auto creation is not possible + // Error message is shown, that registration of the user was not possible due to missing information +}); + +test("login with Generic OIDC IDP, no user linked - auto link", async ({ page }) => { + test.skip(); + // Given idp Generic OIDC is configure on the organization as only authencation method + // Given idp Generic OIDC is configure with account linking allowed, and linking set to existing email + // Given user with email address user@zitadel.com exists + // User is automatically redirected to Generic OIDC + // User authenticates in Generic OIDC with user@zitadel.com + // User is redirect to ZITADEL login + // User is linked with existing user in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with Generic OIDC IDP, no user linked, linking not possible", async ({ page }) => { + test.skip(); + // Given idp Generic OIDC is configure on the organization as only authencation method + // Given idp Generic OIDC is configure with manually account linking not allowed, and linking set to existing email + // Given user with email address user@zitadel.com doesn't exists + // User is automatically redirected to Generic OIDC + // User authenticates in Generic OIDC with user@zitadel.com + // User is redirect to ZITADEL login + // User with email address user@zitadel.com can not be found + // User will get an error message that account linking wasn't possible +}); + +test("login with Generic OIDC IDP, no user linked, linking successful", async ({ page }) => { + test.skip(); + // Given idp Generic OIDC is configure on the organization as only authencation method + // Given idp Generic OIDC is configure with manually account linking allowed, and linking set to existing email + // Given user with email address user@zitadel.com doesn't exists + // User is automatically redirected to Generic OIDC + // User authenticates in Generic OIDC with user@zitadel.com + // User is redirect to ZITADEL login + // User with email address user@zitadel.com can not be found + // User is prompted to link the account manually + // User is redirected to the app (default redirect url) +}); diff --git a/apps/login/acceptance/tests/idp-github-enterprise.spec.ts b/apps/login/acceptance/tests/idp-github-enterprise.spec.ts new file mode 100644 index 0000000000..2c39092851 --- /dev/null +++ b/apps/login/acceptance/tests/idp-github-enterprise.spec.ts @@ -0,0 +1,103 @@ +import test from "@playwright/test"; + +test("login with GitHub Enterprise IDP", async ({ page }) => { + test.skip(); + // Given a GitHub Enterprise IDP is configured on the organization + // Given the user has GitHub Enterprise IDP added as auth method + // User authenticates with the GitHub Enterprise IDP + // User is redirected back to login + // User is redirected to the app +}); + +test("login with GitHub Enterprise IDP - error", async ({ page }) => { + test.skip(); + // Given the GitHub Enterprise IDP is configured on the organization + // Given the user has GitHub Enterprise IDP added as auth method + // User is redirected to the GitHub Enterprise IDP + // User authenticates with the GitHub Enterprise IDP and gets an error + // User is redirected back to login + // An error is shown to the user "Something went wrong" +}); + +test("login with GitHub Enterprise IDP, no user existing - auto register", async ({ page }) => { + test.skip(); + // Given idp GitHub Enterprise is configure on the organization as only authencation method + // Given idp GitHub Enterprise is configure with account creation alloweed, and automatic creation enabled + // Given ZITADEL Action is added to autofill missing user information + // Given no user exists yet + // User is automatically redirected to GitHub Enterprise + // User authenticates in GitHub Enterprise + // User is redirect to ZITADEL login + // User is created in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with GitHub Enterprise IDP, no user existing - auto register not possible", async ({ page }) => { + test.skip(); + // Given idp GitHub Enterprise is configure on the organization as only authencation method + // Given idp GitHub Enterprise is configure with account creation alloweed, and automatic creation enabled + // Given no user exists yet + // User is automatically redirected to GitHub Enterprise + // User authenticates in GitHub Enterprise + // User is redirect to ZITADEL login + // Because of missing informaiton on the user auto creation is not possible + // User will see the registration page with pre filled user information + // User fills missing information + // User clicks register button + // User is created in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with GitHub Enterprise IDP, no user existing - auto register enabled - manual creation disabled, creation not possible", async ({ + page, +}) => { + test.skip(); + // Given idp GitHub Enterprise is configure on the organization as only authencation method + // Given idp GitHub Enterprise is configure with account creation not allowed, and automatic creation enabled + // Given no user exists yet + // User is automatically redirected to GitHub Enterprise + // User authenticates in GitHub Enterprise + // User is redirect to ZITADEL login + // Because of missing informaiton on the user auto creation is not possible + // Error message is shown, that registration of the user was not possible due to missing information +}); + +test("login with GitHub Enterprise IDP, no user linked - auto link", async ({ page }) => { + test.skip(); + // Given idp GitHub Enterprise is configure on the organization as only authencation method + // Given idp GitHub Enterprise is configure with account linking allowed, and linking set to existing email + // Given ZITADEL Action is added to autofill missing user information + // Given user with email address user@zitadel.com exists + // User is automatically redirected to GitHub Enterprise + // User authenticates in GitHub Enterprise with user@zitadel.com + // User is redirect to ZITADEL login + // User is linked with existing user in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with GitHub Enterprise IDP, no user linked, linking not possible", async ({ page }) => { + test.skip(); + // Given idp GitHub Enterprise is configure on the organization as only authencation method + // Given idp GitHub Enterprise is configure with manually account linking not allowed, and linking set to existing email + // Given ZITADEL Action is added to autofill missing user information + // Given user with email address user@zitadel.com doesn't exists + // User is automatically redirected to GitHub Enterprise + // User authenticates in GitHub Enterprise with user@zitadel.com + // User is redirect to ZITADEL login + // User with email address user@zitadel.com can not be found + // User will get an error message that account linking wasn't possible +}); + +test("login with GitHub Enterprise IDP, no user linked, linking successful", async ({ page }) => { + test.skip(); + // Given idp GitHub Enterprise is configure on the organization as only authencation method + // Given idp GitHub Enterprise is configure with manually account linking allowed, and linking set to existing email + // Given ZITADEL Action is added to autofill missing user information + // Given user with email address user@zitadel.com doesn't exists + // User is automatically redirected to GitHub Enterprise + // User authenticates in GitHub Enterprise with user@zitadel.com + // User is redirect to ZITADEL login + // User with email address user@zitadel.com can not be found + // User is prompted to link the account manually + // User is redirected to the app (default redirect url) +}); diff --git a/apps/login/acceptance/tests/idp-github.spec.ts b/apps/login/acceptance/tests/idp-github.spec.ts new file mode 100644 index 0000000000..689e040537 --- /dev/null +++ b/apps/login/acceptance/tests/idp-github.spec.ts @@ -0,0 +1,103 @@ +import test from "@playwright/test"; + +test("login with GitHub IDP", async ({ page }) => { + test.skip(); + // Given a GitHub IDP is configured on the organization + // Given the user has GitHub IDP added as auth method + // User authenticates with the GitHub IDP + // User is redirected back to login + // User is redirected to the app +}); + +test("login with GitHub IDP - error", async ({ page }) => { + test.skip(); + // Given the GitHub IDP is configured on the organization + // Given the user has GitHub IDP added as auth method + // User is redirected to the GitHub IDP + // User authenticates with the GitHub IDP and gets an error + // User is redirected back to login + // An error is shown to the user "Something went wrong" +}); + +test("login with GitHub IDP, no user existing - auto register", async ({ page }) => { + test.skip(); + // Given idp GitHub is configure on the organization as only authencation method + // Given idp GitHub is configure with account creation alloweed, and automatic creation enabled + // Given ZITADEL Action is added to autofill missing user information + // Given no user exists yet + // User is automatically redirected to GitHub + // User authenticates in GitHub + // User is redirect to ZITADEL login + // User is created in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with GitHub IDP, no user existing - auto register not possible", async ({ page }) => { + test.skip(); + // Given idp GitHub is configure on the organization as only authencation method + // Given idp GitHub is configure with account creation alloweed, and automatic creation enabled + // Given no user exists yet + // User is automatically redirected to GitHub + // User authenticates in GitHub + // User is redirect to ZITADEL login + // Because of missing informaiton on the user auto creation is not possible + // User will see the registration page with pre filled user information + // User fills missing information + // User clicks register button + // User is created in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with GitHub IDP, no user existing - auto register enabled - manual creation disabled, creation not possible", async ({ + page, +}) => { + test.skip(); + // Given idp GitHub is configure on the organization as only authencation method + // Given idp GitHub is configure with account creation not allowed, and automatic creation enabled + // Given no user exists yet + // User is automatically redirected to GitHub + // User authenticates in GitHub + // User is redirect to ZITADEL login + // Because of missing informaiton on the user auto creation is not possible + // Error message is shown, that registration of the user was not possible due to missing information +}); + +test("login with GitHub IDP, no user linked - auto link", async ({ page }) => { + test.skip(); + // Given idp GitHub is configure on the organization as only authencation method + // Given idp GitHub is configure with account linking allowed, and linking set to existing email + // Given ZITADEL Action is added to autofill missing user information + // Given user with email address user@zitadel.com exists + // User is automatically redirected to GitHub + // User authenticates in GitHub with user@zitadel.com + // User is redirect to ZITADEL login + // User is linked with existing user in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with GitHub IDP, no user linked, linking not possible", async ({ page }) => { + test.skip(); + // Given idp GitHub is configure on the organization as only authencation method + // Given idp GitHub is configure with manually account linking not allowed, and linking set to existing email + // Given ZITADEL Action is added to autofill missing user information + // Given user with email address user@zitadel.com doesn't exists + // User is automatically redirected to GitHub + // User authenticates in GitHub with user@zitadel.com + // User is redirect to ZITADEL login + // User with email address user@zitadel.com can not be found + // User will get an error message that account linking wasn't possible +}); + +test("login with GitHub IDP, no user linked, linking successful", async ({ page }) => { + test.skip(); + // Given idp GitHub is configure on the organization as only authencation method + // Given idp GitHub is configure with manually account linking allowed, and linking set to existing email + // Given ZITADEL Action is added to autofill missing user information + // Given user with email address user@zitadel.com doesn't exists + // User is automatically redirected to GitHub + // User authenticates in GitHub with user@zitadel.com + // User is redirect to ZITADEL login + // User with email address user@zitadel.com can not be found + // User is prompted to link the account manually + // User is redirected to the app (default redirect url) +}); diff --git a/apps/login/acceptance/tests/idp-gitlab-self-hosted.spec.ts b/apps/login/acceptance/tests/idp-gitlab-self-hosted.spec.ts new file mode 100644 index 0000000000..1b05d5e19b --- /dev/null +++ b/apps/login/acceptance/tests/idp-gitlab-self-hosted.spec.ts @@ -0,0 +1,103 @@ +import test from "@playwright/test"; + +test("login with GitLab Self-Hosted IDP", async ({ page }) => { + test.skip(); + // Given a GitLab Self-Hosted IDP is configured on the organization + // Given the user has GitLab Self-Hosted IDP added as auth method + // User authenticates with the GitLab Self-Hosted IDP + // User is redirected back to login + // User is redirected to the app +}); + +test("login with GitLab Self-Hosted IDP - error", async ({ page }) => { + test.skip(); + // Given the GitLab Self-Hosted IDP is configured on the organization + // Given the user has GitLab Self-Hosted IDP added as auth method + // User is redirected to the GitLab Self-Hosted IDP + // User authenticates with the GitLab Self-Hosted IDP and gets an error + // User is redirected back to login + // An error is shown to the user "Something went wrong" +}); + +test("login with Gitlab Self-Hosted IDP, no user existing - auto register", async ({ page }) => { + test.skip(); + // Given idp Gitlab Self-Hosted is configure on the organization as only authencation method + // Given idp Gitlab Self-Hosted is configure with account creation alloweed, and automatic creation enabled + // Given ZITADEL Action is added to autofill missing user information + // Given no user exists yet + // User is automatically redirected to Gitlab Self-Hosted + // User authenticates in Gitlab Self-Hosted + // User is redirect to ZITADEL login + // User is created in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with Gitlab Self-Hosted IDP, no user existing - auto register not possible", async ({ page }) => { + test.skip(); + // Given idp Gitlab Self-Hosted is configure on the organization as only authencation method + // Given idp Gitlab Self-Hosted is configure with account creation alloweed, and automatic creation enabled + // Given no user exists yet + // User is automatically redirected to Gitlab Self-Hosted + // User authenticates in Gitlab Self-Hosted + // User is redirect to ZITADEL login + // Because of missing informaiton on the user auto creation is not possible + // User will see the registration page with pre filled user information + // User fills missing information + // User clicks register button + // User is created in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with Gitlab Self-Hosted IDP, no user existing - auto register enabled - manual creation disabled, creation not possible", async ({ + page, +}) => { + test.skip(); + // Given idp Gitlab Self-Hosted is configure on the organization as only authencation method + // Given idp Gitlab Self-Hosted is configure with account creation not allowed, and automatic creation enabled + // Given no user exists yet + // User is automatically redirected to Gitlab Self-Hosted + // User authenticates in Gitlab Self-Hosted + // User is redirect to ZITADEL login + // Because of missing informaiton on the user auto creation is not possible + // Error message is shown, that registration of the user was not possible due to missing information +}); + +test("login with Gitlab Self-Hosted IDP, no user linked - auto link", async ({ page }) => { + test.skip(); + // Given idp Gitlab Self-Hosted is configure on the organization as only authencation method + // Given idp Gitlab Self-Hosted is configure with account linking allowed, and linking set to existing email + // Given ZITADEL Action is added to autofill missing user information + // Given user with email address user@zitadel.com exists + // User is automatically redirected to Gitlab Self-Hosted + // User authenticates in Gitlab Self-Hosted with user@zitadel.com + // User is redirect to ZITADEL login + // User is linked with existing user in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with Gitlab Self-Hosted IDP, no user linked, linking not possible", async ({ page }) => { + test.skip(); + // Given idp Gitlab Self-Hosted is configure on the organization as only authencation method + // Given idp Gitlab Self-Hosted is configure with manually account linking not allowed, and linking set to existing email + // Given ZITADEL Action is added to autofill missing user information + // Given user with email address user@zitadel.com doesn't exists + // User is automatically redirected to Gitlab Self-Hosted + // User authenticates in Gitlab Self-Hosted with user@zitadel.com + // User is redirect to ZITADEL login + // User with email address user@zitadel.com can not be found + // User will get an error message that account linking wasn't possible +}); + +test("login with Gitlab Self-Hosted IDP, no user linked, linking successful", async ({ page }) => { + test.skip(); + // Given idp Gitlab Self-Hosted is configure on the organization as only authencation method + // Given idp Gitlab Self-Hosted is configure with manually account linking allowed, and linking set to existing email + // Given ZITADEL Action is added to autofill missing user information + // Given user with email address user@zitadel.com doesn't exists + // User is automatically redirected to Gitlab Self-Hosted + // User authenticates in Gitlab Self-Hosted with user@zitadel.com + // User is redirect to ZITADEL login + // User with email address user@zitadel.com can not be found + // User is prompted to link the account manually + // User is redirected to the app (default redirect url) +}); diff --git a/apps/login/acceptance/tests/idp-gitlab.spec.ts b/apps/login/acceptance/tests/idp-gitlab.spec.ts new file mode 100644 index 0000000000..fdb235843b --- /dev/null +++ b/apps/login/acceptance/tests/idp-gitlab.spec.ts @@ -0,0 +1,103 @@ +import test from "@playwright/test"; + +test("login with GitLab IDP", async ({ page }) => { + test.skip(); + // Given a GitLab IDP is configured on the organization + // Given the user has GitLab IDP added as auth method + // User authenticates with the GitLab IDP + // User is redirected back to login + // User is redirected to the app +}); + +test("login with GitLab IDP - error", async ({ page }) => { + test.skip(); + // Given the GitLab IDP is configured on the organization + // Given the user has GitLab IDP added as auth method + // User is redirected to the GitLab IDP + // User authenticates with the GitLab IDP and gets an error + // User is redirected back to login + // An error is shown to the user "Something went wrong" +}); + +test("login with Gitlab IDP, no user existing - auto register", async ({ page }) => { + test.skip(); + // Given idp Gitlab is configure on the organization as only authencation method + // Given idp Gitlab is configure with account creation alloweed, and automatic creation enabled + // Given ZITADEL Action is added to autofill missing user information + // Given no user exists yet + // User is automatically redirected to Gitlab + // User authenticates in Gitlab + // User is redirect to ZITADEL login + // User is created in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with Gitlab IDP, no user existing - auto register not possible", async ({ page }) => { + test.skip(); + // Given idp Gitlab is configure on the organization as only authencation method + // Given idp Gitlab is configure with account creation alloweed, and automatic creation enabled + // Given no user exists yet + // User is automatically redirected to Gitlab + // User authenticates in Gitlab + // User is redirect to ZITADEL login + // Because of missing informaiton on the user auto creation is not possible + // User will see the registration page with pre filled user information + // User fills missing information + // User clicks register button + // User is created in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with Gitlab IDP, no user existing - auto register enabled - manual creation disabled, creation not possible", async ({ + page, +}) => { + test.skip(); + // Given idp Gitlab is configure on the organization as only authencation method + // Given idp Gitlab is configure with account creation not allowed, and automatic creation enabled + // Given no user exists yet + // User is automatically redirected to Gitlab + // User authenticates in Gitlab + // User is redirect to ZITADEL login + // Because of missing informaiton on the user auto creation is not possible + // Error message is shown, that registration of the user was not possible due to missing information +}); + +test("login with Gitlab IDP, no user linked - auto link", async ({ page }) => { + test.skip(); + // Given idp Gitlab is configure on the organization as only authencation method + // Given idp Gitlab is configure with account linking allowed, and linking set to existing email + // Given ZITADEL Action is added to autofill missing user information + // Given user with email address user@zitadel.com exists + // User is automatically redirected to Gitlab + // User authenticates in Gitlab with user@zitadel.com + // User is redirect to ZITADEL login + // User is linked with existing user in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with Gitlab IDP, no user linked, linking not possible", async ({ page }) => { + test.skip(); + // Given idp Gitlab is configure on the organization as only authencation method + // Given idp Gitlab is configure with manually account linking not allowed, and linking set to existing email + // Given ZITADEL Action is added to autofill missing user information + // Given user with email address user@zitadel.com doesn't exists + // User is automatically redirected to Gitlab + // User authenticates in Gitlab with user@zitadel.com + // User is redirect to ZITADEL login + // User with email address user@zitadel.com can not be found + // User will get an error message that account linking wasn't possible +}); + +test("login with Gitlab IDP, no user linked, linking successful", async ({ page }) => { + test.skip(); + // Given idp Gitlab is configure on the organization as only authencation method + // Given idp Gitlab is configure with manually account linking allowed, and linking set to existing email + // Given ZITADEL Action is added to autofill missing user information + // Given user with email address user@zitadel.com doesn't exists + // User is automatically redirected to Gitlab + // User authenticates in Gitlab with user@zitadel.com + // User is redirect to ZITADEL login + // User with email address user@zitadel.com can not be found + // User is prompted to link the account manually + // User is redirected to the app (default redirect url) +}); diff --git a/apps/login/acceptance/tests/idp-google.spec.ts b/apps/login/acceptance/tests/idp-google.spec.ts new file mode 100644 index 0000000000..8eb4d54e34 --- /dev/null +++ b/apps/login/acceptance/tests/idp-google.spec.ts @@ -0,0 +1,99 @@ +import test from "@playwright/test"; + +test("login with Google IDP", async ({ page }) => { + test.skip(); + // Given a Google IDP is configured on the organization + // Given the user has Google IDP added as auth method + // User authenticates with the Google IDP + // User is redirected back to login + // User is redirected to the app +}); + +test("login with Google IDP - error", async ({ page }) => { + test.skip(); + // Given the Google IDP is configured on the organization + // Given the user has Google IDP added as auth method + // User is redirected to the Google IDP + // User authenticates with the Google IDP and gets an error + // User is redirected back to login + // An error is shown to the user "Something went wrong" +}); + +test("login with Google IDP, no user existing - auto register", async ({ page }) => { + test.skip(); + // Given idp Google is configure on the organization as only authencation method + // Given idp Google is configure with account creation alloweed, and automatic creation enabled + // Given no user exists yet + // User is automatically redirected to Google + // User authenticates in Google + // User is redirect to ZITADEL login + // User is created in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with Google IDP, no user existing - auto register not possible", async ({ page }) => { + test.skip(); + // Given idp Google is configure on the organization as only authencation method + // Given idp Google is configure with account creation alloweed, and automatic creation enabled + // Given no user exists yet + // User is automatically redirected to Google + // User authenticates in Google + // User is redirect to ZITADEL login + // Because of missing informaiton on the user auto creation is not possible + // User will see the registration page with pre filled user information + // User fills missing information + // User clicks register button + // User is created in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with Google IDP, no user existing - auto register enabled - manual creation disabled, creation not possible", async ({ + page, +}) => { + test.skip(); + // Given idp Google is configure on the organization as only authencation method + // Given idp Google is configure with account creation not allowed, and automatic creation enabled + // Given no user exists yet + // User is automatically redirected to Google + // User authenticates in Google + // User is redirect to ZITADEL login + // Because of missing informaiton on the user auto creation is not possible + // Error message is shown, that registration of the user was not possible due to missing information +}); + +test("login with Google IDP, no user linked - auto link", async ({ page }) => { + test.skip(); + // Given idp Google is configure on the organization as only authencation method + // Given idp Google is configure with account linking allowed, and linking set to existing email + // Given user with email address user@zitadel.com exists + // User is automatically redirected to Google + // User authenticates in Google with user@zitadel.com + // User is redirect to ZITADEL login + // User is linked with existing user in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with Google IDP, no user linked, linking not possible", async ({ page }) => { + test.skip(); + // Given idp Google is configure on the organization as only authencation method + // Given idp Google is configure with manually account linking not allowed, and linking set to existing email + // Given user with email address user@zitadel.com doesn't exists + // User is automatically redirected to Google + // User authenticates in Google with user@zitadel.com + // User is redirect to ZITADEL login + // User with email address user@zitadel.com can not be found + // User will get an error message that account linking wasn't possible +}); + +test("login with Google IDP, no user linked, linking successful", async ({ page }) => { + test.skip(); + // Given idp Google is configure on the organization as only authencation method + // Given idp Google is configure with manually account linking allowed, and linking set to existing email + // Given user with email address user@zitadel.com doesn't exists + // User is automatically redirected to Google + // User authenticates in Google with user@zitadel.com + // User is redirect to ZITADEL login + // User with email address user@zitadel.com can not be found + // User is prompted to link the account manually + // User is redirected to the app (default redirect url) +}); diff --git a/apps/login/acceptance/tests/idp-ldap.spec.ts b/apps/login/acceptance/tests/idp-ldap.spec.ts new file mode 100644 index 0000000000..0705ed45f8 --- /dev/null +++ b/apps/login/acceptance/tests/idp-ldap.spec.ts @@ -0,0 +1,99 @@ +import test from "@playwright/test"; + +test("login with LDAP IDP", async ({ page }) => { + test.skip(); + // Given a LDAP IDP is configured on the organization + // Given the user has LDAP IDP added as auth method + // User authenticates with the LDAP IDP + // User is redirected back to login + // User is redirected to the app +}); + +test("login with LDAP IDP - error", async ({ page }) => { + test.skip(); + // Given the LDAP IDP is configured on the organization + // Given the user has LDAP IDP added as auth method + // User is redirected to the LDAP IDP + // User authenticates with the LDAP IDP and gets an error + // User is redirected back to login + // An error is shown to the user "Something went wrong" +}); + +test("login with LDAP IDP, no user existing - auto register", async ({ page }) => { + test.skip(); + // Given idp LDAP is configure on the organization as only authencation method + // Given idp LDAP is configure with account creation alloweed, and automatic creation enabled + // Given no user exists yet + // User is automatically redirected to LDAP + // User authenticates in LDAP + // User is redirect to ZITADEL login + // User is created in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with LDAP IDP, no user existing - auto register not possible", async ({ page }) => { + test.skip(); + // Given idp LDAP is configure on the organization as only authencation method + // Given idp LDAP is configure with account creation alloweed, and automatic creation enabled + // Given no user exists yet + // User is automatically redirected to LDAP + // User authenticates in LDAP + // User is redirect to ZITADEL login + // Because of missing informaiton on the user auto creation is not possible + // User will see the registration page with pre filled user information + // User fills missing information + // User clicks register button + // User is created in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with LDAP IDP, no user existing - auto register enabled - manual creation disabled, creation not possible", async ({ + page, +}) => { + test.skip(); + // Given idp LDAP is configure on the organization as only authencation method + // Given idp LDAP is configure with account creation not allowed, and automatic creation enabled + // Given no user exists yet + // User is automatically redirected to LDAP + // User authenticates in LDAP + // User is redirect to ZITADEL login + // Because of missing informaiton on the user auto creation is not possible + // Error message is shown, that registration of the user was not possible due to missing information +}); + +test("login with LDAP IDP, no user linked - auto link", async ({ page }) => { + test.skip(); + // Given idp LDAP is configure on the organization as only authencation method + // Given idp LDAP is configure with account linking allowed, and linking set to existing email + // Given user with email address user@zitadel.com exists + // User is automatically redirected to LDAP + // User authenticates in LDAP with user@zitadel.com + // User is redirect to ZITADEL login + // User is linked with existing user in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with LDAP IDP, no user linked, linking not possible", async ({ page }) => { + test.skip(); + // Given idp LDAP is configure on the organization as only authencation method + // Given idp LDAP is configure with manually account linking not allowed, and linking set to existing email + // Given user with email address user@zitadel.com doesn't exists + // User is automatically redirected to LDAP + // User authenticates in LDAP with user@zitadel.com + // User is redirect to ZITADEL login + // User with email address user@zitadel.com can not be found + // User will get an error message that account linking wasn't possible +}); + +test("login with LDAP IDP, no user linked, linking successful", async ({ page }) => { + test.skip(); + // Given idp LDAP is configure on the organization as only authencation method + // Given idp LDAP is configure with manually account linking allowed, and linking set to existing email + // Given user with email address user@zitadel.com doesn't exists + // User is automatically redirected to LDAP + // User authenticates in LDAP with user@zitadel.com + // User is redirect to ZITADEL login + // User with email address user@zitadel.com can not be found + // User is prompted to link the account manually + // User is redirected to the app (default redirect url) +}); diff --git a/apps/login/acceptance/tests/idp-microsoft.spec.ts b/apps/login/acceptance/tests/idp-microsoft.spec.ts new file mode 100644 index 0000000000..15d67c28aa --- /dev/null +++ b/apps/login/acceptance/tests/idp-microsoft.spec.ts @@ -0,0 +1,102 @@ +// Note for all tests, in case Microsoft doesn't deliver all relevant information per default +// We should add an action in the needed cases + +import test from "@playwright/test"; + +test("login with Microsoft IDP", async ({ page }) => { + test.skip(); + // Given a Microsoft IDP is configured on the organization + // Given the user has Microsoft IDP added as auth method + // User authenticates with the Microsoft IDP + // User is redirected back to login + // User is redirected to the app +}); + +test("login with Microsoft IDP - error", async ({ page }) => { + test.skip(); + // Given the Microsoft IDP is configured on the organization + // Given the user has Microsoft IDP added as auth method + // User is redirected to the Microsoft IDP + // User authenticates with the Microsoft IDP and gets an error + // User is redirected back to login + // An error is shown to the user "Something went wrong" +}); + +test("login with Microsoft IDP, no user existing - auto register", async ({ page }) => { + test.skip(); + // Given idp Microsoft is configure on the organization as only authencation method + // Given idp Microsoft is configure with account creation alloweed, and automatic creation enabled + // Given no user exists yet + // User is automatically redirected to Microsoft + // User authenticates in Microsoft + // User is redirect to ZITADEL login + // User is created in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with Microsoft IDP, no user existing - auto register not possible", async ({ page }) => { + test.skip(); + // Given idp Microsoft is configure on the organization as only authencation method + // Given idp Microsoft is configure with account creation alloweed, and automatic creation enabled + // Given no user exists yet + // User is automatically redirected to Microsoft + // User authenticates in Microsoft + // User is redirect to ZITADEL login + // Because of missing informaiton on the user auto creation is not possible + // User will see the registration page with pre filled user information + // User fills missing information + // User clicks register button + // User is created in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with Microsoft IDP, no user existing - auto register enabled - manual creation disabled, creation not possible", async ({ + page, +}) => { + test.skip(); + // Given idp Microsoft is configure on the organization as only authencation method + // Given idp Microsoft is configure with account creation not allowed, and automatic creation enabled + // Given no user exists yet + // User is automatically redirected to Microsoft + // User authenticates in Microsoft + // User is redirect to ZITADEL login + // Because of missing informaiton on the user auto creation is not possible + // Error message is shown, that registration of the user was not possible due to missing information +}); + +test("login with Microsoft IDP, no user linked - auto link", async ({ page }) => { + test.skip(); + // Given idp Microsoft is configure on the organization as only authencation method + // Given idp Microsoft is configure with account linking allowed, and linking set to existing email + // Given user with email address user@zitadel.com exists + // User is automatically redirected to Microsoft + // User authenticates in Microsoft with user@zitadel.com + // User is redirect to ZITADEL login + // User is linked with existing user in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with Microsoft IDP, no user linked, linking not possible", async ({ page }) => { + test.skip(); + // Given idp Microsoft is configure on the organization as only authencation method + // Given idp Microsoft is configure with manually account linking not allowed, and linking set to existing email + // Given user with email address user@zitadel.com doesn't exists + // User is automatically redirected to Microsoft + // User authenticates in Microsoft with user@zitadel.com + // User is redirect to ZITADEL login + // User with email address user@zitadel.com can not be found + // User will get an error message that account linking wasn't possible +}); + +test("login with Microsoft IDP, no user linked, linking successful", async ({ page }) => { + test.skip(); + // Given idp Microsoft is configure on the organization as only authencation method + // Given idp Microsoft is configure with manually account linking allowed, and linking set to existing email + // Given user with email address user@zitadel.com doesn't exists + // User is automatically redirected to Microsoft + // User authenticates in Microsoft with user@zitadel.com + // User is redirect to ZITADEL login + // User with email address user@zitadel.com can not be found + // User is prompted to link the account manually + // User is redirected to the app (default redirect url) +}); diff --git a/apps/login/acceptance/tests/idp-saml.spec.ts b/apps/login/acceptance/tests/idp-saml.spec.ts new file mode 100644 index 0000000000..90d8d618b4 --- /dev/null +++ b/apps/login/acceptance/tests/idp-saml.spec.ts @@ -0,0 +1,103 @@ +import test from "@playwright/test"; + +test("login with SAML IDP", async ({ page }) => { + test.skip(); + // Given a SAML IDP is configured on the organization + // Given the user has SAML IDP added as auth method + // User authenticates with the SAML IDP + // User is redirected back to login + // User is redirected to the app +}); + +test("login with SAML IDP - error", async ({ page }) => { + test.skip(); + // Given the SAML IDP is configured on the organization + // Given the user has SAML IDP added as auth method + // User is redirected to the SAML IDP + // User authenticates with the SAML IDP and gets an error + // User is redirected back to login + // An error is shown to the user "Something went wrong" +}); + +test("login with SAML IDP, no user existing - auto register", async ({ page }) => { + test.skip(); + // Given idp SAML is configure on the organization as only authencation method + // Given idp SAML is configure with account creation alloweed, and automatic creation enabled + // Given ZITADEL Action is added to autofill missing user information + // Given no user exists yet + // User is automatically redirected to SAML + // User authenticates in SAML + // User is redirect to ZITADEL login + // User is created in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with SAML IDP, no user existing - auto register not possible", async ({ page }) => { + test.skip(); + // Given idp SAML is configure on the organization as only authencation method + // Given idp SAML is configure with account creation alloweed, and automatic creation enabled + // Given no user exists yet + // User is automatically redirected to SAML + // User authenticates in SAML + // User is redirect to ZITADEL login + // Because of missing informaiton on the user auto creation is not possible + // User will see the registration page with pre filled user information + // User fills missing information + // User clicks register button + // User is created in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with SAML IDP, no user existing - auto register enabled - manual creation disabled, creation not possible", async ({ + page, +}) => { + test.skip(); + // Given idp SAML is configure on the organization as only authencation method + // Given idp SAML is configure with account creation not allowed, and automatic creation enabled + // Given no user exists yet + // User is automatically redirected to SAML + // User authenticates in SAML + // User is redirect to ZITADEL login + // Because of missing informaiton on the user auto creation is not possible + // Error message is shown, that registration of the user was not possible due to missing information +}); + +test("login with SAML IDP, no user linked - auto link", async ({ page }) => { + test.skip(); + // Given idp SAML is configure on the organization as only authencation method + // Given idp SAML is configure with account linking allowed, and linking set to existing email + // Given ZITADEL Action is added to autofill missing user information + // Given user with email address user@zitadel.com exists + // User is automatically redirected to SAML + // User authenticates in SAML with user@zitadel.com + // User is redirect to ZITADEL login + // User is linked with existing user in ZITADEL + // User is redirected to the app (default redirect url) +}); + +test("login with SAML IDP, no user linked, linking not possible", async ({ page }) => { + test.skip(); + // Given idp SAML is configure on the organization as only authencation method + // Given idp SAML is configure with manually account linking not allowed, and linking set to existing email + // Given ZITADEL Action is added to autofill missing user information + // Given user with email address user@zitadel.com doesn't exists + // User is automatically redirected to SAML + // User authenticates in SAML with user@zitadel.com + // User is redirect to ZITADEL login + // User with email address user@zitadel.com can not be found + // User will get an error message that account linking wasn't possible +}); + +test("login with SAML IDP, no user linked, linking successful", async ({ page }) => { + test.skip(); + // Given idp SAML is configure on the organization as only authencation method + // Given idp SAML is configure with manually account linking allowed, and linking set to existing email + // Given ZITADEL Action is added to autofill missing user information + // Given user with email address user@zitadel.com doesn't exists + // User is automatically redirected to SAML + // User authenticates in SAML with user@zitadel.com + // User is redirect to ZITADEL login + // User with email address user@zitadel.com can not be found + // User is prompted to link the account manually + // User is redirected to the app (default redirect url) +}); diff --git a/apps/login/acceptance/tests/login-configuration-possiblities.spec.ts b/apps/login/acceptance/tests/login-configuration-possiblities.spec.ts new file mode 100644 index 0000000000..cc58dbcc71 --- /dev/null +++ b/apps/login/acceptance/tests/login-configuration-possiblities.spec.ts @@ -0,0 +1,57 @@ +import test from "@playwright/test"; + +test("login with mfa setup, mfa setup prompt", async ({ page }) => { + test.skip(); + // Given the organization has enabled at least one mfa types + // Given the user has a password but no mfa registered + // User authenticates with login name and password + // User is prompted to setup a mfa, mfa providers are listed, the user can choose the provider +}); + +test("login with mfa setup, no mfa setup prompt", async ({ page }) => { + test.skip(); + // Given the organization has set "multifactor init check time" to 0 + // Given the organization has enabled mfa types + // Given the user has a password but no mfa registered + // User authenticates with loginname and password + // user is directly loged in and not prompted to setup mfa +}); + +test("login with mfa setup, force mfa for local authenticated users", async ({ page }) => { + test.skip(); + // Given the organization has enabled force mfa for local authentiacted users + // Given the organization has enabled all possible mfa types + // Given the user has a password but no mfa registered + // User authenticates with loginname and password + // User is prompted to setup a mfa, all possible mfa providers are listed, the user can choose the provider +}); + +test("login with mfa setup, force mfa - local user", async ({ page }) => { + test.skip(); + // Given the organization has enabled force mfa for local authentiacted users + // Given the organization has enabled all possible mfa types + // Given the user has a password but no mfa registered + // User authenticates with loginname and password + // User is prompted to setup a mfa, all possible mfa providers are listed, the user can choose the provider +}); + +test("login with mfa setup, force mfa - external user", async ({ page }) => { + test.skip(); + // Given the organization has enabled force mfa + // Given the organization has enabled all possible mfa types + // Given the user has an idp but no mfa registered + // enter login name + // redirect to configured external idp + // User is prompted to setup a mfa, all possible mfa providers are listed, the user can choose the provider +}); + +test("login with mfa setup, force mfa - local user, wrong password", async ({ page }) => { + test.skip(); + // Given the organization has a password lockout policy set to 1 on the max password attempts + // Given the user has only a password as auth methos + // enter login name + // enter wrong password + // User will get an error "Wrong password" + // enter password + // User will get an error "Max password attempts reached - user is locked. Please reach out to your administrator" +}); diff --git a/apps/login/acceptance/tests/login.ts b/apps/login/acceptance/tests/login.ts new file mode 100644 index 0000000000..2076412456 --- /dev/null +++ b/apps/login/acceptance/tests/login.ts @@ -0,0 +1,41 @@ +import { expect, Page } from "@playwright/test"; +import { code, otpFromSink } from "./code"; +import { loginname } from "./loginname"; +import { password } from "./password"; +import { totp } from "./zitadel"; + +export async function startLogin(page: Page) { + await page.goto(`./loginname`); +} + +export async function loginWithPassword(page: Page, username: string, pw: string) { + await startLogin(page); + await loginname(page, username); + await password(page, pw); +} + +export async function loginWithPasskey(page: Page, authenticatorId: string, username: string) { + await startLogin(page); + await loginname(page, username); + // await passkey(page, authenticatorId); +} + +export async function loginScreenExpect(page: Page, fullName: string) { + await expect(page).toHaveURL(/.*signedin.*/); + await expect(page.getByRole("heading")).toContainText(fullName); +} + +export async function loginWithPasswordAndEmailOTP(page: Page, username: string, password: string, email: string) { + await loginWithPassword(page, username, password); + await otpFromSink(page, email); +} + +export async function loginWithPasswordAndPhoneOTP(page: Page, username: string, password: string, phone: string) { + await loginWithPassword(page, username, password); + await otpFromSink(page, phone); +} + +export async function loginWithPasswordAndTOTP(page: Page, username: string, password: string, secret: string) { + await loginWithPassword(page, username, password); + await code(page, totp(secret)); +} diff --git a/apps/login/acceptance/tests/loginname-screen.ts b/apps/login/acceptance/tests/loginname-screen.ts new file mode 100644 index 0000000000..be41a28eda --- /dev/null +++ b/apps/login/acceptance/tests/loginname-screen.ts @@ -0,0 +1,12 @@ +import { expect, Page } from "@playwright/test"; + +const usernameTextInput = "username-text-input"; + +export async function loginnameScreen(page: Page, username: string) { + await page.getByTestId(usernameTextInput).pressSequentially(username); +} + +export async function loginnameScreenExpect(page: Page, username: string) { + await expect(page.getByTestId(usernameTextInput)).toHaveValue(username); + await expect(page.getByTestId("error").locator("div")).toContainText("User not found in the system"); +} diff --git a/apps/login/acceptance/tests/loginname.ts b/apps/login/acceptance/tests/loginname.ts new file mode 100644 index 0000000000..2050ec1d3c --- /dev/null +++ b/apps/login/acceptance/tests/loginname.ts @@ -0,0 +1,7 @@ +import { Page } from "@playwright/test"; +import { loginnameScreen } from "./loginname-screen"; + +export async function loginname(page: Page, username: string) { + await loginnameScreen(page, username); + await page.getByTestId("submit-button").click(); +} diff --git a/apps/login/acceptance/tests/passkey.ts b/apps/login/acceptance/tests/passkey.ts new file mode 100644 index 0000000000..d8cda10ddb --- /dev/null +++ b/apps/login/acceptance/tests/passkey.ts @@ -0,0 +1,109 @@ +import { expect, Page } from "@playwright/test"; +import { CDPSession } from "playwright-core"; + +interface session { + client: CDPSession; + authenticatorId: string; +} + +async function client(page: Page): Promise { + const cdpSession = await page.context().newCDPSession(page); + await cdpSession.send("WebAuthn.enable", { enableUI: false }); + const result = await cdpSession.send("WebAuthn.addVirtualAuthenticator", { + options: { + protocol: "ctap2", + transport: "internal", + hasResidentKey: true, + hasUserVerification: true, + isUserVerified: true, + automaticPresenceSimulation: true, + }, + }); + return { client: cdpSession, authenticatorId: result.authenticatorId }; +} + +export async function passkeyRegister(page: Page): Promise { + const session = await client(page); + + await passkeyNotExisting(session.client, session.authenticatorId); + await simulateSuccessfulPasskeyRegister(session.client, session.authenticatorId, () => + page.getByTestId("submit-button").click(), + ); + await passkeyRegistered(session.client, session.authenticatorId); + + return session.authenticatorId; +} + +export async function passkey(page: Page, authenticatorId: string) { + const cdpSession = await page.context().newCDPSession(page); + await cdpSession.send("WebAuthn.enable", { enableUI: false }); + + const signCount = await passkeyExisting(cdpSession, authenticatorId); + + await simulateSuccessfulPasskeyInput(cdpSession, authenticatorId, () => page.getByTestId("submit-button").click()); + + await passkeyUsed(cdpSession, authenticatorId, signCount); +} + +async function passkeyNotExisting(client: CDPSession, authenticatorId: string) { + const result = await client.send("WebAuthn.getCredentials", { authenticatorId }); + expect(result.credentials).toHaveLength(0); +} + +async function passkeyRegistered(client: CDPSession, authenticatorId: string) { + const result = await client.send("WebAuthn.getCredentials", { authenticatorId }); + expect(result.credentials).toHaveLength(1); + await passkeyUsed(client, authenticatorId, 0); +} + +async function passkeyExisting(client: CDPSession, authenticatorId: string): Promise { + const result = await client.send("WebAuthn.getCredentials", { authenticatorId }); + expect(result.credentials).toHaveLength(1); + return result.credentials[0].signCount; +} + +async function passkeyUsed(client: CDPSession, authenticatorId: string, signCount: number) { + const result = await client.send("WebAuthn.getCredentials", { authenticatorId }); + expect(result.credentials).toHaveLength(1); + expect(result.credentials[0].signCount).toBeGreaterThan(signCount); +} + +async function simulateSuccessfulPasskeyRegister( + client: CDPSession, + authenticatorId: string, + operationTrigger: () => Promise, +) { + // initialize event listeners to wait for a successful passkey input event + const operationCompleted = new Promise((resolve) => { + client.on("WebAuthn.credentialAdded", () => { + console.log("Credential Added!"); + resolve(); + }); + }); + + // perform a user action that triggers passkey prompt + await operationTrigger(); + + // wait to receive the event that the passkey was successfully registered or verified + await operationCompleted; +} + +async function simulateSuccessfulPasskeyInput( + client: CDPSession, + authenticatorId: string, + operationTrigger: () => Promise, +) { + // initialize event listeners to wait for a successful passkey input event + const operationCompleted = new Promise((resolve) => { + client.on("WebAuthn.credentialAsserted", () => { + console.log("Credential Asserted!"); + resolve(); + }); + }); + + // perform a user action that triggers passkey prompt + await operationTrigger(); + + // wait to receive the event that the passkey was successfully registered or verified + await operationCompleted; +} diff --git a/apps/login/acceptance/tests/password-screen.ts b/apps/login/acceptance/tests/password-screen.ts new file mode 100644 index 0000000000..fda6f6d39f --- /dev/null +++ b/apps/login/acceptance/tests/password-screen.ts @@ -0,0 +1,98 @@ +import { expect, Page } from "@playwright/test"; +import { getCodeFromSink } from "./sink"; + +const codeField = "code-text-input"; +const passwordField = "password-text-input"; +const passwordChangeField = "password-change-text-input"; +const passwordChangeConfirmField = "password-change-confirm-text-input"; +const passwordSetField = "password-set-text-input"; +const passwordSetConfirmField = "password-set-confirm-text-input"; +const lengthCheck = "length-check"; +const symbolCheck = "symbol-check"; +const numberCheck = "number-check"; +const uppercaseCheck = "uppercase-check"; +const lowercaseCheck = "lowercase-check"; +const equalCheck = "equal-check"; + +const matchText = "Matches"; +const noMatchText = "Doesn't match"; + +export async function changePasswordScreen(page: Page, password1: string, password2: string) { + await page.getByTestId(passwordChangeField).pressSequentially(password1); + await page.getByTestId(passwordChangeConfirmField).pressSequentially(password2); +} + +export async function passwordScreen(page: Page, password: string) { + await page.getByTestId(passwordField).pressSequentially(password); +} + +export async function passwordScreenExpect(page: Page, password: string) { + await expect(page.getByTestId(passwordField)).toHaveValue(password); + await expect(page.getByTestId("error").locator("div")).toContainText("Failed to authenticate."); +} + +export async function changePasswordScreenExpect( + page: Page, + password1: string, + password2: string, + length: boolean, + symbol: boolean, + number: boolean, + uppercase: boolean, + lowercase: boolean, + equals: boolean, +) { + await expect(page.getByTestId(passwordChangeField)).toHaveValue(password1); + await expect(page.getByTestId(passwordChangeConfirmField)).toHaveValue(password2); + + await checkComplexity(page, length, symbol, number, uppercase, lowercase, equals); +} + +async function checkComplexity( + page: Page, + length: boolean, + symbol: boolean, + number: boolean, + uppercase: boolean, + lowercase: boolean, + equals: boolean, +) { + await checkContent(page, lengthCheck, length); + await checkContent(page, symbolCheck, symbol); + await checkContent(page, numberCheck, number); + await checkContent(page, uppercaseCheck, uppercase); + await checkContent(page, lowercaseCheck, lowercase); + await checkContent(page, equalCheck, equals); +} + +async function checkContent(page: Page, testid: string, match: boolean) { + if (match) { + await expect(page.getByTestId(testid)).toContainText(matchText); + } else { + await expect(page.getByTestId(testid)).toContainText(noMatchText); + } +} + +export async function resetPasswordScreen(page: Page, username: string, password1: string, password2: string) { + const c = await getCodeFromSink(username); + await page.getByTestId(codeField).pressSequentially(c); + await page.getByTestId(passwordSetField).pressSequentially(password1); + await page.getByTestId(passwordSetConfirmField).pressSequentially(password2); +} + +export async function resetPasswordScreenExpect( + page: Page, + password1: string, + password2: string, + length: boolean, + symbol: boolean, + number: boolean, + uppercase: boolean, + lowercase: boolean, + equals: boolean, +) { + await expect(page.getByTestId(passwordSetField)).toHaveValue(password1); + await expect(page.getByTestId(passwordSetConfirmField)).toHaveValue(password2); + + await checkComplexity(page, length, symbol, number, uppercase, lowercase, equals); +} diff --git a/apps/login/acceptance/tests/password.ts b/apps/login/acceptance/tests/password.ts new file mode 100644 index 0000000000..ccf3e509d9 --- /dev/null +++ b/apps/login/acceptance/tests/password.ts @@ -0,0 +1,29 @@ +import { Page } from "@playwright/test"; +import { changePasswordScreen, passwordScreen, resetPasswordScreen } from "./password-screen"; + +const passwordSubmitButton = "submit-button"; +const passwordResetButton = "reset-button"; + +export async function startChangePassword(page: Page, loginname: string) { + await page.goto("./password/change?" + new URLSearchParams({ loginName: loginname })); +} + +export async function changePassword(page: Page, password: string) { + await changePasswordScreen(page, password, password); + await page.getByTestId(passwordSubmitButton).click(); +} + +export async function password(page: Page, password: string) { + await passwordScreen(page, password); + await page.getByTestId(passwordSubmitButton).click(); +} + +export async function startResetPassword(page: Page) { + await page.getByTestId(passwordResetButton).click(); +} + +export async function resetPassword(page: Page, username: string, password: string) { + await startResetPassword(page); + await resetPasswordScreen(page, username, password, password); + await page.getByTestId(passwordSubmitButton).click(); +} diff --git a/apps/login/acceptance/tests/register-screen.ts b/apps/login/acceptance/tests/register-screen.ts new file mode 100644 index 0000000000..d14f5dc970 --- /dev/null +++ b/apps/login/acceptance/tests/register-screen.ts @@ -0,0 +1,27 @@ +import { Page } from "@playwright/test"; + +const passwordField = "password-text-input"; +const passwordConfirmField = "password-confirm-text-input"; + +export async function registerUserScreenPassword(page: Page, firstname: string, lastname: string, email: string) { + await registerUserScreen(page, firstname, lastname, email); + await page.getByTestId("password-radio").click(); +} + +export async function registerUserScreenPasskey(page: Page, firstname: string, lastname: string, email: string) { + await registerUserScreen(page, firstname, lastname, email); + await page.getByTestId("passkey-radio").click(); +} + +export async function registerPasswordScreen(page: Page, password1: string, password2: string) { + await page.getByTestId(passwordField).pressSequentially(password1); + await page.getByTestId(passwordConfirmField).pressSequentially(password2); +} + +export async function registerUserScreen(page: Page, firstname: string, lastname: string, email: string) { + await page.getByTestId("firstname-text-input").pressSequentially(firstname); + await page.getByTestId("lastname-text-input").pressSequentially(lastname); + await page.getByTestId("email-text-input").pressSequentially(email); + await page.getByTestId("privacy-policy-checkbox").check(); + await page.getByTestId("tos-checkbox").check(); +} diff --git a/apps/login/acceptance/tests/register.spec.ts b/apps/login/acceptance/tests/register.spec.ts new file mode 100644 index 0000000000..4ad7e9e349 --- /dev/null +++ b/apps/login/acceptance/tests/register.spec.ts @@ -0,0 +1,183 @@ +import { faker } from "@faker-js/faker"; +import { test } from "@playwright/test"; +import dotenv from "dotenv"; +import path from "path"; +import { loginScreenExpect } from "./login"; +import { registerWithPasskey, registerWithPassword } from "./register"; +import { removeUserByUsername } from "./zitadel"; + +// Read from ".env" file. +dotenv.config({ path: path.resolve(__dirname, "../../login/.env.test.local") }); + +test("register with password", async ({ page }) => { + const username = faker.internet.email(); + const password = "Password1!"; + const firstname = faker.person.firstName(); + const lastname = faker.person.lastName(); + + await registerWithPassword(page, firstname, lastname, username, password, password); + await loginScreenExpect(page, firstname + " " + lastname); + + // wait for projection of user + await page.waitForTimeout(10000); + await removeUserByUsername(username); +}); + +test("register with passkey", async ({ page }) => { + const username = faker.internet.email(); + const firstname = faker.person.firstName(); + const lastname = faker.person.lastName(); + + await registerWithPasskey(page, firstname, lastname, username); + await loginScreenExpect(page, firstname + " " + lastname); + + // wait for projection of user + await page.waitForTimeout(10000); + await removeUserByUsername(username); +}); + +test("register with username and password - only password enabled", async ({ page }) => { + test.skip(); + // Given on the default organization "username and password is allowed" is enabled + // Given on the default organization "username registeration allowed" is enabled + // Given on the default organization no idp is configured and enabled + // Given on the default organization passkey is not enabled + // Given user doesn't exist + // Click on button "register new user" + // User is redirected to registration page + // Only password is shown as an option - no passkey + // User enters "firstname", "lastname", "username" and "password" + // User is redirected to app (default redirect url) +}); + +test("register with username and password - wrong password not enough characters", async ({ page }) => { + test.skip(); + // Given on the default organization "username and password is allowed" is enabled + // Given on the default organization "username registeration allowed" is enabled + // Given on the default organization no idp is configured and enabled + // Given on the default organization passkey is not enabled + // Given password policy is set to 8 characters and must include number, symbol, lower and upper letter + // Given user doesn't exist + // Click on button "register new user" + // User is redirected to registration page + // Only password is shown as an option - no passkey + // User enters "firstname", "lastname", "username" and a password thats to short + // Error is shown "Password doesn't match the policy - it must have at least 8 characters" +}); + +test("register with username and password - wrong password number missing", async ({ page }) => { + test.skip(); + // Given on the default organization "username and password is allowed" is enabled + // Given on the default organization "username registeration allowed" is enabled + // Given on the default organization no idp is configured and enabled + // Given on the default organization passkey is not enabled + // Given password policy is set to 8 characters and must include number, symbol, lower and upper letter + // Given user doesn't exist + // Click on button "register new user" + // User is redirected to registration page + // Only password is shown as an option - no passkey + // User enters "firstname", "lastname", "username" and a password without a number + // Error is shown "Password doesn't match the policy - number missing" +}); + +test("register with username and password - wrong password upper case missing", async ({ page }) => { + test.skip(); + // Given on the default organization "username and password is allowed" is enabled + // Given on the default organization "username registeration allowed" is enabled + // Given on the default organization no idp is configured and enabled + // Given on the default organization passkey is not enabled + // Given password policy is set to 8 characters and must include number, symbol, lower and upper letter + // Given user doesn't exist + // Click on button "register new user" + // User is redirected to registration page + // Only password is shown as an option - no passkey + // User enters "firstname", "lastname", "username" and a password without an upper case + // Error is shown "Password doesn't match the policy - uppercase letter missing" +}); + +test("register with username and password - wrong password lower case missing", async ({ page }) => { + test.skip(); + // Given on the default organization "username and password is allowed" is enabled + // Given on the default organization "username registeration allowed" is enabled + // Given on the default organization no idp is configured and enabled + // Given on the default organization passkey is not enabled + // Given password policy is set to 8 characters and must include number, symbol, lower and upper letter + // Given user doesn't exist + // Click on button "register new user" + // User is redirected to registration page + // Only password is shown as an option - no passkey + // User enters "firstname", "lastname", "username" and a password without an lower case + // Error is shown "Password doesn't match the policy - lowercase letter missing" +}); + +test("register with username and password - wrong password symboo missing", async ({ page }) => { + test.skip(); + // Given on the default organization "username and password is allowed" is enabled + // Given on the default organization "username registeration allowed" is enabled + // Given on the default organization no idp is configured and enabled + // Given on the default organization passkey is not enabled + // Given password policy is set to 8 characters and must include number, symbol, lower and upper letter + // Given user doesn't exist + // Click on button "register new user" + // User is redirected to registration page + // Only password is shown as an option - no passkey + // User enters "firstname", "lastname", "username" and a password without an symbol + // Error is shown "Password doesn't match the policy - symbol missing" +}); + +test("register with username and password - password and passkey enabled", async ({ page }) => { + test.skip(); + // Given on the default organization "username and password is allowed" is enabled + // Given on the default organization "username registeration allowed" is enabled + // Given on the default organization no idp is configured and enabled + // Given on the default organization passkey is enabled + // Given user doesn't exist + // Click on button "register new user" + // User is redirected to registration page + // User enters "firstname", "lastname", "username" + // Password and passkey are shown as authentication option + // User clicks password + // User enters password + // User is redirected to app (default redirect url) +}); + +test("register with username and passkey - password and passkey enabled", async ({ page }) => { + test.skip(); + // Given on the default organization "username and password is allowed" is enabled + // Given on the default organization "username registeration allowed" is enabled + // Given on the default organization no idp is configured and enabled + // Given on the default organization passkey is enabled + // Given user doesn't exist + // Click on button "register new user" + // User is redirected to registration page + // User enters "firstname", "lastname", "username" + // Password and passkey are shown as authentication option + // User clicks passkey + // Passkey is opened automatically + // User verifies passkey + // User is redirected to app (default redirect url) +}); + +test("register with username and password - registration disabled", async ({ page }) => { + test.skip(); + // Given on the default organization "username and password is allowed" is enabled + // Given on the default organization "username registeration allowed" is enabled + // Given on the default organization no idp is configured and enabled + // Given user doesn't exist + // Button "register new user" is not available +}); + +test("register with username and password - multiple registration options", async ({ page }) => { + test.skip(); + // Given on the default organization "username and password is allowed" is enabled + // Given on the default organization "username registeration allowed" is enabled + // Given on the default organization one idp is configured and enabled + // Given user doesn't exist + // Click on button "register new user" + // User is redirected to registration options + // Local User and idp button are shown + // User clicks idp button + // User enters "firstname", "lastname", "username" and "password" + // User clicks next + // User is redirected to app (default redirect url) +}); diff --git a/apps/login/acceptance/tests/register.ts b/apps/login/acceptance/tests/register.ts new file mode 100644 index 0000000000..164a72753b --- /dev/null +++ b/apps/login/acceptance/tests/register.ts @@ -0,0 +1,39 @@ +import { Page } from "@playwright/test"; +import { emailVerify } from "./email-verify"; +import { passkeyRegister } from "./passkey"; +import { registerPasswordScreen, registerUserScreenPasskey, registerUserScreenPassword } from "./register-screen"; +import { getCodeFromSink } from "./sink"; + +export async function registerWithPassword( + page: Page, + firstname: string, + lastname: string, + email: string, + password1: string, + password2: string, +) { + await page.goto("./register"); + await registerUserScreenPassword(page, firstname, lastname, email); + await page.getByTestId("submit-button").click(); + await registerPasswordScreen(page, password1, password2); + await page.getByTestId("submit-button").click(); + await verifyEmail(page, email); +} + +export async function registerWithPasskey(page: Page, firstname: string, lastname: string, email: string): Promise { + await page.goto("./register"); + await registerUserScreenPasskey(page, firstname, lastname, email); + await page.getByTestId("submit-button").click(); + + // wait for projection of user + await page.waitForTimeout(10000); + const authId = await passkeyRegister(page); + + await verifyEmail(page, email); + return authId; +} + +async function verifyEmail(page: Page, email: string) { + const c = await getCodeFromSink(email); + await emailVerify(page, c); +} diff --git a/apps/login/acceptance/tests/select-account.ts b/apps/login/acceptance/tests/select-account.ts new file mode 100644 index 0000000000..64bd7cd145 --- /dev/null +++ b/apps/login/acceptance/tests/select-account.ts @@ -0,0 +1,5 @@ +import { Page } from "@playwright/test"; + +export async function selectNewAccount(page: Page) { + await page.getByRole("link", { name: "Add another account" }).click(); +} diff --git a/apps/login/acceptance/tests/sink.ts b/apps/login/acceptance/tests/sink.ts new file mode 100644 index 0000000000..bc3336b358 --- /dev/null +++ b/apps/login/acceptance/tests/sink.ts @@ -0,0 +1,43 @@ +import { Gaxios, GaxiosResponse } from "gaxios"; + +const awaitNotification = new Gaxios({ + url: process.env.SINK_NOTIFICATION_URL, + method: "POST", + retryConfig: { + httpMethodsToRetry: ["POST"], + statusCodesToRetry: [[404, 404]], + retry: Number.MAX_SAFE_INTEGER, // totalTimeout limits the number of retries + totalTimeout: 10000, // 10 seconds + onRetryAttempt: (error) => { + console.warn(`Retrying request to sink notification service: ${error.message}`); + }, + }, +}); + +export async function getOtpFromSink(recipient: string): Promise { + return awaitNotification.request({ data: { recipient } }).then((response) => { + expectSuccess(response); + const otp = response?.data?.args?.otp; + if (!otp) { + throw new Error(`Response does not contain an otp property: ${JSON.stringify(response.data, null, 2)}`); + } + return otp; + }); +} + +export async function getCodeFromSink(recipient: string): Promise { + return awaitNotification.request({ data: { recipient } }).then((response) => { + expectSuccess(response); + const code = response?.data?.args?.code; + if (!code) { + throw new Error(`Response does not contain a code property: ${JSON.stringify(response.data, null, 2)}`); + } + return code; + }); +} + +function expectSuccess(response: GaxiosResponse): void { + if (response.status !== 200) { + throw new Error(`Expected HTTP status 200, but got: ${response.status} - ${response.statusText}`); + } +} diff --git a/apps/login/acceptance/tests/user.ts b/apps/login/acceptance/tests/user.ts new file mode 100644 index 0000000000..3b03291408 --- /dev/null +++ b/apps/login/acceptance/tests/user.ts @@ -0,0 +1,177 @@ +import { Page } from "@playwright/test"; +import { registerWithPasskey } from "./register"; +import { activateOTP, addTOTP, addUser, eventualNewUser, getUserByUsername, removeUser } from "./zitadel"; + +export interface userProps { + email: string; + isEmailVerified?: boolean; + firstName: string; + lastName: string; + organization: string; + password: string; + passwordChangeRequired?: boolean; + phone: string; + isPhoneVerified?: boolean; +} + +class User { + private readonly props: userProps; + private user: string; + + constructor(userProps: userProps) { + this.props = userProps; + } + + async ensure(page: Page) { + const response = await addUser(this.props); + + this.setUserId(response.userId); + } + + async cleanup() { + await removeUser(this.getUserId()); + } + + public setUserId(userId: string) { + this.user = userId; + } + + public getUserId() { + return this.user; + } + + public getUsername() { + return this.props.email; + } + + public getPassword() { + return this.props.password; + } + + public getFirstname() { + return this.props.firstName; + } + + public getLastname() { + return this.props.lastName; + } + + public getPhone() { + return this.props.phone; + } + + public getFullName() { + return `${this.props.firstName} ${this.props.lastName}`; + } +} + +export class PasswordUser extends User { + async ensure(page: Page) { + await super.ensure(page); + await eventualNewUser(this.getUserId()); + } +} + +export enum OtpType { + sms = "sms", + email = "email", +} + +export interface otpUserProps { + email: string; + isEmailVerified?: boolean; + firstName: string; + lastName: string; + organization: string; + password: string; + passwordChangeRequired?: boolean; + phone: string; + isPhoneVerified?: boolean; + type: OtpType; +} + +export class PasswordUserWithOTP extends User { + private type: OtpType; + + constructor(props: otpUserProps) { + super({ + email: props.email, + firstName: props.firstName, + lastName: props.lastName, + organization: props.organization, + password: props.password, + phone: props.phone, + isEmailVerified: props.isEmailVerified, + isPhoneVerified: props.isPhoneVerified, + passwordChangeRequired: props.passwordChangeRequired, + }); + this.type = props.type; + } + + async ensure(page: Page) { + await super.ensure(page); + await activateOTP(this.getUserId(), this.type); + await eventualNewUser(this.getUserId()); + } +} + +export class PasswordUserWithTOTP extends User { + private secret: string; + + async ensure(page: Page) { + await super.ensure(page); + this.secret = await addTOTP(this.getUserId()); + await eventualNewUser(this.getUserId()); + } + + public getSecret(): string { + return this.secret; + } +} + +export interface passkeyUserProps { + email: string; + firstName: string; + lastName: string; + organization: string; + phone: string; + isEmailVerified?: boolean; + isPhoneVerified?: boolean; +} + +export class PasskeyUser extends User { + private authenticatorId: string; + + constructor(props: passkeyUserProps) { + super({ + email: props.email, + firstName: props.firstName, + lastName: props.lastName, + organization: props.organization, + password: "", + phone: props.phone, + isEmailVerified: props.isEmailVerified, + isPhoneVerified: props.isPhoneVerified, + }); + } + + public async ensure(page: Page) { + const authId = await registerWithPasskey(page, this.getFirstname(), this.getLastname(), this.getUsername()); + this.authenticatorId = authId; + + // wait for projection of user + await page.waitForTimeout(10000); + } + + async cleanup() { + const resp: any = await getUserByUsername(this.getUsername()); + if (!resp || !resp.result || !resp.result[0]) { + return; + } + await removeUser(resp.result[0].userId); + } + + public getAuthenticatorId(): string { + return this.authenticatorId; + } +} diff --git a/apps/login/acceptance/tests/username-passkey.spec.ts b/apps/login/acceptance/tests/username-passkey.spec.ts new file mode 100644 index 0000000000..dff1c65f5a --- /dev/null +++ b/apps/login/acceptance/tests/username-passkey.spec.ts @@ -0,0 +1,43 @@ +import { faker } from "@faker-js/faker"; +import { test as base } from "@playwright/test"; +import dotenv from "dotenv"; +import path from "path"; +import { loginScreenExpect, loginWithPasskey } from "./login"; +import { PasskeyUser } from "./user"; + +// Read from ".env" file. +dotenv.config({ path: path.resolve(__dirname, "../../login/.env.test.local") }); + +const test = base.extend<{ user: PasskeyUser }>({ + user: async ({ page }, use) => { + const user = new PasskeyUser({ + email: faker.internet.email(), + isEmailVerified: true, + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + organization: "", + phone: faker.phone.number(), + isPhoneVerified: false, + }); + await user.ensure(page); + await use(user); + await user.cleanup(); + }, +}); + +test("username and passkey login", async ({ user, page }) => { + await loginWithPasskey(page, user.getAuthenticatorId(), user.getUsername()); + await loginScreenExpect(page, user.getFullName()); +}); + +test("username and passkey login, multiple auth methods", async ({ page }) => { + test.skip(); + // Given passkey and password is enabled on the organization of the user + // Given the user has password and passkey registered + // enter username + // passkey popup is directly shown + // user aborts passkey authentication + // user switches to password authentication + // user enters password + // user is redirected to app +}); diff --git a/apps/login/acceptance/tests/username-password-change-required.spec.ts b/apps/login/acceptance/tests/username-password-change-required.spec.ts new file mode 100644 index 0000000000..50605e5ff0 --- /dev/null +++ b/apps/login/acceptance/tests/username-password-change-required.spec.ts @@ -0,0 +1,41 @@ +import { faker } from "@faker-js/faker"; +import { test as base } from "@playwright/test"; +import dotenv from "dotenv"; +import path from "path"; +import { loginScreenExpect, loginWithPassword } from "./login"; +import { changePassword } from "./password"; +import { PasswordUser } from "./user"; + +// Read from ".env" file. +dotenv.config({ path: path.resolve(__dirname, "../../login/.env.test.local") }); + +const test = base.extend<{ user: PasswordUser }>({ + user: async ({ page }, use) => { + const user = new PasswordUser({ + email: faker.internet.email(), + isEmailVerified: true, + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + organization: "", + phone: faker.phone.number(), + isPhoneVerified: false, + password: "Password1!", + passwordChangeRequired: true, + }); + await user.ensure(page); + await use(user); + await user.cleanup(); + }, +}); + +test("username and password login, change required", async ({ user, page }) => { + const changedPw = "ChangedPw1!"; + + await loginWithPassword(page, user.getUsername(), user.getPassword()); + await page.waitForTimeout(10000); + await changePassword(page, changedPw); + await loginScreenExpect(page, user.getFullName()); + + await loginWithPassword(page, user.getUsername(), changedPw); + await loginScreenExpect(page, user.getFullName()); +}); diff --git a/apps/login/acceptance/tests/username-password-changed.spec.ts b/apps/login/acceptance/tests/username-password-changed.spec.ts new file mode 100644 index 0000000000..dc29dc2286 --- /dev/null +++ b/apps/login/acceptance/tests/username-password-changed.spec.ts @@ -0,0 +1,54 @@ +import { faker } from "@faker-js/faker"; +import { test as base } from "@playwright/test"; +import dotenv from "dotenv"; +import path from "path"; +import { loginScreenExpect, loginWithPassword } from "./login"; +import { changePassword, startChangePassword } from "./password"; +import { changePasswordScreen, changePasswordScreenExpect } from "./password-screen"; +import { PasswordUser } from "./user"; + +// Read from ".env" file. +dotenv.config({ path: path.resolve(__dirname, "../../login/.env.test.local") }); + +const test = base.extend<{ user: PasswordUser }>({ + user: async ({ page }, use) => { + const user = new PasswordUser({ + email: faker.internet.email(), + isEmailVerified: true, + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + organization: "", + phone: faker.phone.number(), + isPhoneVerified: false, + password: "Password1!", + passwordChangeRequired: false, + }); + await user.ensure(page); + await use(user); + await user.cleanup(); + }, +}); + +test("username and password changed login", async ({ user, page }) => { + const changedPw = "ChangedPw1!"; + await loginWithPassword(page, user.getUsername(), user.getPassword()); + + // wait for projection of token + await page.waitForTimeout(10000); + + await startChangePassword(page, user.getUsername()); + await changePassword(page, changedPw); + await loginScreenExpect(page, user.getFullName()); + + await loginWithPassword(page, user.getUsername(), changedPw); + await loginScreenExpect(page, user.getFullName()); +}); + +test("password change not with desired complexity", async ({ user, page }) => { + const changedPw1 = "change"; + const changedPw2 = "chang"; + await loginWithPassword(page, user.getUsername(), user.getPassword()); + await startChangePassword(page, user.getUsername()); + await changePasswordScreen(page, changedPw1, changedPw2); + await changePasswordScreenExpect(page, changedPw1, changedPw2, false, false, false, false, true, false); +}); diff --git a/apps/login/acceptance/tests/username-password-otp_email.spec.ts b/apps/login/acceptance/tests/username-password-otp_email.spec.ts new file mode 100644 index 0000000000..e4a77751c1 --- /dev/null +++ b/apps/login/acceptance/tests/username-password-otp_email.spec.ts @@ -0,0 +1,98 @@ +import { faker } from "@faker-js/faker"; +import { test as base } from "@playwright/test"; +import dotenv from "dotenv"; +import path from "path"; +import { code, codeResend, otpFromSink } from "./code"; +import { codeScreenExpect } from "./code-screen"; +import { loginScreenExpect, loginWithPassword, loginWithPasswordAndEmailOTP } from "./login"; +import { OtpType, PasswordUserWithOTP } from "./user"; + +// Read from ".env" file. +dotenv.config({ path: path.resolve(__dirname, "../../login/.env.test.local") }); + +const test = base.extend<{ user: PasswordUserWithOTP; sink: any }>({ + user: async ({ page }, use) => { + const user = new PasswordUserWithOTP({ + email: faker.internet.email(), + isEmailVerified: true, + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + organization: "", + phone: faker.phone.number(), + isPhoneVerified: false, + password: "Password1!", + passwordChangeRequired: false, + type: OtpType.email, + }); + + await user.ensure(page); + await use(user); + await user.cleanup(); + }, +}); + +test.skip("DOESN'T WORK: username, password and email otp login, enter code manually", async ({ user, page }) => { + // Given email otp is enabled on the organization of the user + // Given the user has only email otp configured as second factor + // User enters username + // User enters password + // User receives an email with a verification code + // User enters the code into the ui + // User is redirected to the app (default redirect url) + await loginWithPasswordAndEmailOTP(page, user.getUsername(), user.getPassword(), user.getUsername()); + await loginScreenExpect(page, user.getFullName()); +}); + +test("username, password and email otp login, click link in email", async ({ page }) => { + base.skip(); + // Given email otp is enabled on the organization of the user + // Given the user has only email otp configured as second factor + // User enters username + // User enters password + // User receives an email with a verification code + // User clicks link in the email + // User is redirected to the app (default redirect url) +}); + +test.skip("DOESN'T WORK: username, password and email otp login, resend code", async ({ user, page }) => { + // Given email otp is enabled on the organization of the user + // Given the user has only email otp configured as second factor + // User enters username + // User enters password + // User receives an email with a verification code + // User clicks resend code + // User receives a new email with a verification code + // User enters the new code in the ui + // User is redirected to the app (default redirect url) + await loginWithPassword(page, user.getUsername(), user.getPassword()); + await codeResend(page); + await otpFromSink(page, user.getUsername()); + await loginScreenExpect(page, user.getFullName()); +}); + +test("username, password and email otp login, wrong code", async ({ user, page }) => { + // Given email otp is enabled on the organization of the user + // Given the user has only email otp configured as second factor + // User enters username + // User enters password + // User receives an email with a verification code + // User enters a wrong code + // Error message - "Invalid code" is shown + const c = "wrongcode"; + await loginWithPassword(page, user.getUsername(), user.getPassword()); + await code(page, c); + await codeScreenExpect(page, c); +}); + +test("username, password and email otp login, multiple mfa options", async ({ page }) => { + base.skip(); + // Given email otp and sms otp is enabled on the organization of the user + // Given the user has email and sms otp configured as second factor + // User enters username + // User enters password + // User receives an email with a verification code + // User clicks button to use sms otp as second factor + // User receives a sms with a verification code + // User enters code in ui + // User is redirected to the app (default redirect url) +}); diff --git a/apps/login/acceptance/tests/username-password-otp_sms.spec.ts b/apps/login/acceptance/tests/username-password-otp_sms.spec.ts new file mode 100644 index 0000000000..10901cd243 --- /dev/null +++ b/apps/login/acceptance/tests/username-password-otp_sms.spec.ts @@ -0,0 +1,71 @@ +import { faker } from "@faker-js/faker"; +import { test as base } from "@playwright/test"; +import dotenv from "dotenv"; +import path from "path"; +import { code } from "./code"; +import { codeScreenExpect } from "./code-screen"; +import { loginScreenExpect, loginWithPassword, loginWithPasswordAndPhoneOTP } from "./login"; +import { OtpType, PasswordUserWithOTP } from "./user"; + +// Read from ".env" file. +dotenv.config({ path: path.resolve(__dirname, "../../login/.env.test.local") }); + +const test = base.extend<{ user: PasswordUserWithOTP; sink: any }>({ + user: async ({ page }, use) => { + const user = new PasswordUserWithOTP({ + email: faker.internet.email(), + isEmailVerified: true, + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + organization: "", + phone: faker.phone.number({ style: "international" }), + isPhoneVerified: true, + password: "Password1!", + passwordChangeRequired: false, + type: OtpType.sms, + }); + + await user.ensure(page); + await use(user); + await user.cleanup(); + }, +}); + +test.skip("DOESN'T WORK: username, password and sms otp login, enter code manually", async ({ user, page }) => { + // Given sms otp is enabled on the organization of the user + // Given the user has only sms otp configured as second factor + // User enters username + // User enters password + // User receives a sms with a verification code + // User enters the code into the ui + // User is redirected to the app (default redirect url) + await loginWithPasswordAndPhoneOTP(page, user.getUsername(), user.getPassword(), user.getPhone()); + await loginScreenExpect(page, user.getFullName()); +}); + +test.skip("DOESN'T WORK: username, password and sms otp login, resend code", async ({ user, page }) => { + // Given sms otp is enabled on the organization of the user + // Given the user has only sms otp configured as second factor + // User enters username + // User enters password + // User receives a sms with a verification code + // User clicks resend code + // User receives a new sms with a verification code + // User is redirected to the app (default redirect url) + await loginWithPasswordAndPhoneOTP(page, user.getUsername(), user.getPassword(), user.getPhone()); + await loginScreenExpect(page, user.getFullName()); +}); + +test("username, password and sms otp login, wrong code", async ({ user, page }) => { + // Given sms otp is enabled on the organization of the user + // Given the user has only sms otp configured as second factor + // User enters username + // User enters password + // User receives a sms with a verification code + // User enters a wrong code + // Error message - "Invalid code" is shown + const c = "wrongcode"; + await loginWithPassword(page, user.getUsername(), user.getPassword()); + await code(page, c); + await codeScreenExpect(page, c); +}); diff --git a/apps/login/acceptance/tests/username-password-set.spec.ts b/apps/login/acceptance/tests/username-password-set.spec.ts new file mode 100644 index 0000000000..06ce42f1a7 --- /dev/null +++ b/apps/login/acceptance/tests/username-password-set.spec.ts @@ -0,0 +1,52 @@ +import { faker } from "@faker-js/faker"; +import { test as base } from "@playwright/test"; +import dotenv from "dotenv"; +import path from "path"; +import { loginScreenExpect, loginWithPassword, startLogin } from "./login"; +import { loginname } from "./loginname"; +import { resetPassword, startResetPassword } from "./password"; +import { resetPasswordScreen, resetPasswordScreenExpect } from "./password-screen"; +import { PasswordUser } from "./user"; + +// Read from ".env" file. +dotenv.config({ path: path.resolve(__dirname, "../../login/.env.test.local") }); + +const test = base.extend<{ user: PasswordUser }>({ + user: async ({ page }, use) => { + const user = new PasswordUser({ + email: faker.internet.email(), + isEmailVerified: true, + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + organization: "", + phone: faker.phone.number(), + isPhoneVerified: false, + password: "Password1!", + passwordChangeRequired: false, + }); + await user.ensure(page); + await use(user); + await user.cleanup(); + }, +}); + +test("username and password set login", async ({ user, page }) => { + const changedPw = "ChangedPw1!"; + await startLogin(page); + await loginname(page, user.getUsername()); + await resetPassword(page, user.getUsername(), changedPw); + await loginScreenExpect(page, user.getFullName()); + + await loginWithPassword(page, user.getUsername(), changedPw); + await loginScreenExpect(page, user.getFullName()); +}); + +test("password set not with desired complexity", async ({ user, page }) => { + const changedPw1 = "change"; + const changedPw2 = "chang"; + await startLogin(page); + await loginname(page, user.getUsername()); + await startResetPassword(page); + await resetPasswordScreen(page, user.getUsername(), changedPw1, changedPw2); + await resetPasswordScreenExpect(page, changedPw1, changedPw2, false, false, false, false, true, false); +}); diff --git a/apps/login/acceptance/tests/username-password-totp.spec.ts b/apps/login/acceptance/tests/username-password-totp.spec.ts new file mode 100644 index 0000000000..e495b16681 --- /dev/null +++ b/apps/login/acceptance/tests/username-password-totp.spec.ts @@ -0,0 +1,71 @@ +import { faker } from "@faker-js/faker"; +import { test as base } from "@playwright/test"; +import dotenv from "dotenv"; +import path from "path"; +import { code } from "./code"; +import { codeScreenExpect } from "./code-screen"; +import { loginScreenExpect, loginWithPassword, loginWithPasswordAndTOTP } from "./login"; +import { PasswordUserWithTOTP } from "./user"; + +// Read from ".env" file. +dotenv.config({ path: path.resolve(__dirname, "../../login/.env.test.local") }); + +const test = base.extend<{ user: PasswordUserWithTOTP; sink: any }>({ + user: async ({ page }, use) => { + const user = new PasswordUserWithTOTP({ + email: faker.internet.email(), + isEmailVerified: true, + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + organization: "", + phone: faker.phone.number({ style: "international" }), + isPhoneVerified: true, + password: "Password1!", + passwordChangeRequired: false, + }); + + await user.ensure(page); + await use(user); + await user.cleanup(); + }, +}); + +test("username, password and totp login", async ({ user, page }) => { + // Given totp is enabled on the organization of the user + // Given the user has only totp configured as second factor + // User enters username + // User enters password + // Screen for entering the code is shown directly + // User enters the code into the ui + // User is redirected to the app (default redirect url) + await loginWithPasswordAndTOTP(page, user.getUsername(), user.getPassword(), user.getSecret()); + await loginScreenExpect(page, user.getFullName()); +}); + +test("username, password and totp otp login, wrong code", async ({ user, page }) => { + // Given totp is enabled on the organization of the user + // Given the user has only totp configured as second factor + // User enters username + // User enters password + // Screen for entering the code is shown directly + // User enters a wrond code + // Error message - "Invalid code" is shown + const c = "wrongcode"; + await loginWithPassword(page, user.getUsername(), user.getPassword()); + await code(page, c); + await codeScreenExpect(page, c); +}); + +test("username, password and totp login, multiple mfa options", async ({ page }) => { + test.skip(); + // Given totp and email otp is enabled on the organization of the user + // Given the user has totp and email otp configured as second factor + // User enters username + // User enters password + // Screen for entering the code is shown directly + // Button to switch to email otp is shown + // User clicks button to use email otp instead + // User receives an email with a verification code + // User enters code in ui + // User is redirected to the app (default redirect url) +}); diff --git a/apps/login/acceptance/tests/username-password-u2f.spec.ts b/apps/login/acceptance/tests/username-password-u2f.spec.ts new file mode 100644 index 0000000000..dc23064fd6 --- /dev/null +++ b/apps/login/acceptance/tests/username-password-u2f.spec.ts @@ -0,0 +1,26 @@ +import { test } from "@playwright/test"; + +test("username, password and u2f login", async ({ page }) => { + test.skip(); + // Given u2f is enabled on the organization of the user + // Given the user has only u2f configured as second factor + // User enters username + // User enters password + // Popup for u2f is directly opened + // User verifies u2f + // User is redirected to the app (default redirect url) +}); + +test("username, password and u2f login, multiple mfa options", async ({ page }) => { + test.skip(); + // Given u2f and semailms otp is enabled on the organization of the user + // Given the user has u2f and email otp configured as second factor + // User enters username + // User enters password + // Popup for u2f is directly opened + // User aborts u2f verification + // User clicks button to use email otp as second factor + // User receives an email with a verification code + // User enters code in ui + // User is redirected to the app (default redirect url) +}); diff --git a/apps/login/acceptance/tests/username-password.spec.ts b/apps/login/acceptance/tests/username-password.spec.ts new file mode 100644 index 0000000000..ceb340f8da --- /dev/null +++ b/apps/login/acceptance/tests/username-password.spec.ts @@ -0,0 +1,157 @@ +import { faker } from "@faker-js/faker"; +import { test as base } from "@playwright/test"; +import dotenv from "dotenv"; +import path from "path"; +import { loginScreenExpect, loginWithPassword, startLogin } from "./login"; +import { loginname } from "./loginname"; +import { loginnameScreenExpect } from "./loginname-screen"; +import { password } from "./password"; +import { passwordScreenExpect } from "./password-screen"; +import { PasswordUser } from "./user"; + +// Read from ".env" file. +dotenv.config({ path: path.resolve(__dirname, "../../login/.env.test.local") }); + +const test = base.extend<{ user: PasswordUser }>({ + user: async ({ page }, use) => { + const user = new PasswordUser({ + email: faker.internet.email(), + isEmailVerified: true, + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + organization: "", + phone: faker.phone.number(), + isPhoneVerified: false, + password: "Password1!", + passwordChangeRequired: false, + }); + await user.ensure(page); + await use(user); + await user.cleanup(); + }, +}); + +test("username and password login", async ({ user, page }) => { + await loginWithPassword(page, user.getUsername(), user.getPassword()); + await loginScreenExpect(page, user.getFullName()); +}); + +test("username and password login, unknown username", async ({ page }) => { + const username = "unknown"; + await startLogin(page); + await loginname(page, username); + await loginnameScreenExpect(page, username); +}); + +test("username and password login, wrong password", async ({ user, page }) => { + await startLogin(page); + await loginname(page, user.getUsername()); + await password(page, "wrong"); + await passwordScreenExpect(page, "wrong"); +}); + +test("username and password login, wrong username, ignore unknown usernames", async ({ user, page }) => { + test.skip(); + // Given user doesn't exist but ignore unknown usernames setting is set to true + // Given username password login is enabled on the users organization + // enter login name + // enter password + // redirect to loginname page --> error message username or password wrong +}); + +test("username and password login, initial password change", async ({ user, page }) => { + test.skip(); + // Given user is created and has changePassword set to true + // Given username password login is enabled on the users organization + // enter login name + // enter password + // create new password +}); + +test("username and password login, reset password hidden", async ({ user, page }) => { + test.skip(); + // Given the organization has enabled "Password reset hidden" in the login policy + // Given username password login is enabled on the users organization + // enter login name + // password reset link should not be shown on password screen +}); + +test("username and password login, reset password - enter code manually", async ({ user, page }) => { + test.skip(); + // Given user has forgotten password and clicks the forgot password button + // Given username password login is enabled on the users organization + // enter login name + // click password forgotten + // enter code from email + // user is redirected to app (default redirect url) +}); + +test("username and password login, reset password - click link", async ({ user, page }) => { + test.skip(); + // Given user has forgotten password and clicks the forgot password button, and then the link in the email + // Given username password login is enabled on the users organization + // enter login name + // click password forgotten + // click link in email + // set new password + // redirect to app (default redirect url) +}); + +test("username and password login, reset password, resend code", async ({ user, page }) => { + test.skip(); + // Given user has forgotten password and clicks the forgot password button and then resend code + // Given username password login is enabled on the users organization + // enter login name + // click password forgotten + // click resend code + // enter code from second email + // user is redirected to app (default redirect url) +}); + +test("email login enabled", async ({ user, page }) => { + test.skip(); + // Given user with the username "testuser", email test@zitadel.com and phone number 0711111111 exists + // Given no other user with the same email address exists + // enter email address "test@zitadel.com " in login screen + // user will get to password screen +}); + +test("email login disabled", async ({ user, page }) => { + test.skip(); + // Given user with the username "testuser", email test@zitadel.com and phone number 0711111111 exists + // Given no other user with the same email address exists + // enter email address "test@zitadel.com" in login screen + // user will see error message "user not found" +}); + +test("email login enabled - multiple users", async ({ user, page }) => { + test.skip(); + // Given user with the username "testuser", email test@zitadel.com and phone number 0711111111 exists + // Given a second user with the username "testuser2", email test@zitadel.com and phone number 0711111111 exists + // enter email address "test@zitadel.com" in login screen + // user will see error message "user not found" +}); + +test("phone login enabled", async ({ user, page }) => { + test.skip(); + // Given user with the username "testuser", email test@zitadel.com and phone number 0711111111 exists + // Given no other user with the same phon number exists + // enter phone number "0711111111" in login screen + // user will get to password screen +}); + +test("phone login disabled", async ({ user, page }) => { + test.skip(); + // Given user with the username "testuser", email test@zitadel.com and phone number 0711111111 exists + // Given no other user with the same phone number exists + // enter phone number "0711111111" in login screen + // user will see error message "user not found" +}); + +test("phone login enabled - multiple users", async ({ user, page }) => { + test.skip(); + // Given user with the username "testuser", email test@zitadel.com and phone number 0711111111 exists + // Given a second user with the username "testuser2", email test@zitadel.com and phone number 0711111111 exists + // enter phone number "0711111111" in login screen + // user will see error message "user not found" +}); diff --git a/apps/login/acceptance/tests/welcome.ts b/apps/login/acceptance/tests/welcome.ts new file mode 100644 index 0000000000..34267c2bd0 --- /dev/null +++ b/apps/login/acceptance/tests/welcome.ts @@ -0,0 +1,6 @@ +import { test } from "@playwright/test"; + +test("login is accessible", async ({ page }) => { + await page.goto("./"); + await page.getByRole("heading", { name: "Welcome back!" }).isVisible(); +}); diff --git a/apps/login/acceptance/tests/zitadel.ts b/apps/login/acceptance/tests/zitadel.ts new file mode 100644 index 0000000000..b252654f86 --- /dev/null +++ b/apps/login/acceptance/tests/zitadel.ts @@ -0,0 +1,190 @@ +import { Authenticator } from "@otplib/core"; +import { createDigest, createRandomBytes } from "@otplib/plugin-crypto"; +import { keyDecoder, keyEncoder } from "@otplib/plugin-thirty-two"; // use your chosen base32 plugin +import axios from "axios"; +import dotenv from "dotenv"; +import { request } from "gaxios"; +import path from "path"; +import { OtpType, userProps } from "./user"; + +dotenv.config({ path: path.resolve(__dirname, "../../login/.env.test.local") }); + +export async function addUser(props: userProps) { + const body = { + username: props.email, + organization: { + orgId: props.organization, + }, + profile: { + givenName: props.firstName, + familyName: props.lastName, + }, + email: { + email: props.email, + isVerified: true, + }, + phone: { + phone: props.phone, + isVerified: true, + }, + password: { + password: props.password, + changeRequired: props.passwordChangeRequired ?? false, + }, + }; + if (!props.isEmailVerified) { + delete body.email.isVerified; + } + if (!props.isPhoneVerified) { + delete body.phone.isVerified; + } + + return await listCall(`${process.env.ZITADEL_API_URL}/v2/users/human`, body); +} + +export async function removeUserByUsername(username: string) { + const resp = await getUserByUsername(username); + if (!resp || !resp.result || !resp.result[0]) { + return; + } + await removeUser(resp.result[0].userId); +} + +export async function removeUser(id: string) { + await deleteCall(`${process.env.ZITADEL_API_URL}/v2/users/${id}`); +} + +async function deleteCall(url: string) { + try { + const response = await axios.delete(url, { + headers: { + Authorization: `Bearer ${process.env.ZITADEL_ADMIN_TOKEN}`, + }, + }); + + if (response.status >= 400 && response.status !== 404) { + const error = `HTTP Error: ${response.status} - ${response.statusText}`; + console.error(error); + throw new Error(error); + } + } catch (error) { + console.error("Error making request:", error); + throw error; + } +} + +export async function getUserByUsername(username: string): Promise { + const listUsersBody = { + queries: [ + { + userNameQuery: { + userName: username, + }, + }, + ], + }; + + return await listCall(`${process.env.ZITADEL_API_URL}/v2/users`, listUsersBody); +} + +async function listCall(url: string, data: any): Promise { + try { + const response = await axios.post(url, data, { + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${process.env.ZITADEL_ADMIN_TOKEN}`, + }, + }); + + if (response.status >= 400) { + const error = `HTTP Error: ${response.status} - ${response.statusText}`; + console.error(error); + throw new Error(error); + } + + return response.data; + } catch (error) { + console.error("Error making request:", error); + throw error; + } +} + +export async function activateOTP(userId: string, type: OtpType) { + let url = "otp_"; + switch (type) { + case OtpType.sms: + url = url + "sms"; + break; + case OtpType.email: + url = url + "email"; + break; + } + + await pushCall(`${process.env.ZITADEL_API_URL}/v2/users/${userId}/${url}`, {}); +} + +async function pushCall(url: string, data: any) { + try { + const response = await axios.post(url, data, { + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${process.env.ZITADEL_ADMIN_TOKEN}`, + }, + }); + + if (response.status >= 400) { + const error = `HTTP Error: ${response.status} - ${response.statusText}`; + console.error(error); + throw new Error(error); + } + } catch (error) { + console.error("Error making request:", error); + throw error; + } +} + +export async function addTOTP(userId: string): Promise { + const response = await listCall(`${process.env.ZITADEL_API_URL}/v2/users/${userId}/totp`, {}); + const code = totp(response.secret); + await pushCall(`${process.env.ZITADEL_API_URL}/v2/users/${userId}/totp/verify`, { code: code }); + return response.secret; +} + +export function totp(secret: string) { + const authenticator = new Authenticator({ + createDigest, + createRandomBytes, + keyDecoder, + keyEncoder, + }); + // google authenticator usage + const token = authenticator.generate(secret); + + // check if token can be used + if (!authenticator.verify({ token: token, secret: secret })) { + const error = `Generated token could not be verified`; + console.error(error); + throw new Error(error); + } + + return token; +} + +export async function eventualNewUser(id: string) { + return request({ + url: `${process.env.ZITADEL_API_URL}/v2/users/${id}`, + method: "GET", + headers: { + Authorization: `Bearer ${process.env.ZITADEL_ADMIN_TOKEN}`, + "Content-Type": "application/json", + }, + retryConfig: { + statusCodesToRetry: [[404, 404]], + retry: Number.MAX_SAFE_INTEGER, // totalTimeout limits the number of retries + totalTimeout: 10000, // 10 seconds + onRetryAttempt: (error) => { + console.warn(`Retrying to query new user ${id}: ${error.message}`); + }, + }, + }); +} diff --git a/apps/login/acceptance/zitadel.yaml b/apps/login/acceptance/zitadel.yaml new file mode 100644 index 0000000000..986574fed2 --- /dev/null +++ b/apps/login/acceptance/zitadel.yaml @@ -0,0 +1,65 @@ +ExternalSecure: false +TLS.Enabled: false + +FirstInstance: + PatPath: /pat/zitadel-admin-sa.pat + Org: + Human: + UserName: zitadel-admin + FirstName: ZITADEL + LastName: Admin + Password: Password1! + PasswordChangeRequired: false + PreferredLanguage: en + Machine: + Machine: + Username: zitadel-admin-sa + Name: Admin + Pat.ExpirationDate: 2099-01-01T00:00:00Z + LoginClient: + Machine: + Username: login-client-sa + Name: Login Client + Pat.ExpirationDate: 2099-01-01T00:00:00Z + +DefaultInstance: + LoginPolicy: + AllowUsernamePassword: true # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_ALLOWUSERNAMEPASSWORD + AllowRegister: true # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_ALLOWREGISTER + AllowExternalIDP: true # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_ALLOWEXTERNALIDP + ForceMFA: false # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_FORCEMFA + HidePasswordReset: false # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_HIDEPASSWORDRESET + IgnoreUnknownUsernames: false # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_IGNOREUNKNOWNUSERNAMES + AllowDomainDiscovery: true # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_ALLOWDOMAINDISCOVERY + # 1 is allowed, 0 is not allowed + PasswordlessType: 1 # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_PASSWORDLESSTYPE + # DefaultRedirectURL is empty by default because we use the Console UI + DefaultRedirectURI: # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_DEFAULTREDIRECTURI + # 240h = 10d + PasswordCheckLifetime: 240h # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_PASSWORDCHECKLIFETIME + # 240h = 10d + ExternalLoginCheckLifetime: 240h # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_EXTERNALLOGINCHECKLIFETIME + # 720h = 30d + MfaInitSkipLifetime: 0h # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_MFAINITSKIPLIFETIME + SecondFactorCheckLifetime: 18h # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_SECONDFACTORCHECKLIFETIME + MultiFactorCheckLifetime: 12h # ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_MULTIFACTORCHECKLIFETIME + PrivacyPolicy: + TOSLink: "https://zitadel.com/docs/legal/terms-of-service" + PrivacyLink: "https://zitadel.com/docs/legal/policies/privacy-policy" + HelpLink: "https://zitadel.com/docs" + SupportEmail: "support@zitadel.com" + DocsLink: "https://zitadel.com/docs" + +Database: + EventPushConnRatio: 0.2 # 4 + ProjectionSpoolerConnRatio: 0.3 # 6 + postgres: + Host: db + MaxOpenConns: 20 + MaxIdleConns: 20 + MaxConnLifetime: 1h + MaxConnIdleTime: 5m + User.Password: zitadel + +Logstore.Access.Stdout.Enabled: true +Log.Formatter.Format: json \ No newline at end of file diff --git a/apps/login/constants/csp.js b/apps/login/constants/csp.js new file mode 100644 index 0000000000..5cc1e254f3 --- /dev/null +++ b/apps/login/constants/csp.js @@ -0,0 +1,2 @@ +export const DEFAULT_CSP = + "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://va.vercel-scripts.com; connect-src 'self'; child-src; style-src 'self' 'unsafe-inline'; font-src 'self'; object-src 'none'; img-src 'self' https://vercel.com;"; diff --git a/apps/login/docker-bake-release.hcl b/apps/login/docker-bake-release.hcl new file mode 100644 index 0000000000..51e1c194f6 --- /dev/null +++ b/apps/login/docker-bake-release.hcl @@ -0,0 +1,3 @@ +target "release" { + platforms = ["linux/amd64", "linux/arm64"] +} diff --git a/apps/login/docker-bake.hcl b/apps/login/docker-bake.hcl new file mode 100644 index 0000000000..e09d1176e0 --- /dev/null +++ b/apps/login/docker-bake.hcl @@ -0,0 +1,25 @@ +variable "LOGIN_TAG" { + default = "zitadel-login:local" +} + +group "default" { + targets = ["login-standalone"] +} + +# The release target is overwritten in docker-bake-release.hcl +# It makes sure the image is built for multiple platforms. +# By default the platforms property is empty, so images are only built for the current bake runtime platform. +target "release" {} + +target "docker-metadata-action" { + # In the pipeline, this target is overwritten by the docker metadata action. + tags = ["${LOGIN_TAG}"] +} + +# We run integration and acceptance tests against the next standalone server for docker. +target "login-standalone" { + inherits = [ + "docker-metadata-action", + "release", + ] +} diff --git a/apps/login/integration/.eslintrc.cjs b/apps/login/integration/.eslintrc.cjs new file mode 100644 index 0000000000..84711c5881 --- /dev/null +++ b/apps/login/integration/.eslintrc.cjs @@ -0,0 +1,10 @@ +module.exports = { + root: true, + // Use basic ESLint config since the login app has its own detailed config + extends: ["eslint:recommended"], + settings: { + next: { + rootDir: ["apps/*/"], + }, + }, +}; diff --git a/apps/login/integration/.gitignore b/apps/login/integration/.gitignore new file mode 100644 index 0000000000..2ca81ab137 --- /dev/null +++ b/apps/login/integration/.gitignore @@ -0,0 +1,2 @@ +screenshots +videos \ No newline at end of file diff --git a/apps/login/integration/.npmrc b/apps/login/integration/.npmrc new file mode 100644 index 0000000000..bc63bba6e3 --- /dev/null +++ b/apps/login/integration/.npmrc @@ -0,0 +1 @@ +side-effects-cache=false diff --git a/apps/login/integration/core-mock/Dockerfile b/apps/login/integration/core-mock/Dockerfile new file mode 100644 index 0000000000..447c73b534 --- /dev/null +++ b/apps/login/integration/core-mock/Dockerfile @@ -0,0 +1,15 @@ +FROM bufbuild/buf:1.54.0 AS proto-files +RUN buf export https://github.com/envoyproxy/protoc-gen-validate.git --path validate --output /proto-files && \ + buf export https://github.com/grpc-ecosystem/grpc-gateway.git --path protoc-gen-openapiv2 --output /proto-files && \ + buf export https://github.com/googleapis/googleapis.git --path protos/zitadelgoogle/api/annotations.proto --path google/api/http.proto --path google/api/field_behavior.proto --output /proto-files && \ + buf export https://github.com/zitadel/zitadel.git --path ./proto/zitadel --output /proto-files + +FROM golang:1.20.5-alpine3.18 AS mock-zitadel + +RUN go install github.com/eliobischof/grpc-mock/cmd/grpc-mock@01b09f60db1b501178af59bed03b2c22661df48c + +COPY mocked-services.cfg . +COPY initial-stubs initial-stubs +COPY --from=proto-files /proto-files/ ./ + +ENTRYPOINT [ "sh", "-c", "grpc-mock -v 1 -proto $(tr '\n' ',' < ./mocked-services.cfg) -stub-dir ./initial-stubs -mock-addr :22222" ] diff --git a/apps/login/integration/core-mock/initial-stubs/zitadel.settings.v2.SettingsService.json b/apps/login/integration/core-mock/initial-stubs/zitadel.settings.v2.SettingsService.json new file mode 100644 index 0000000000..ebfaaadb85 --- /dev/null +++ b/apps/login/integration/core-mock/initial-stubs/zitadel.settings.v2.SettingsService.json @@ -0,0 +1,66 @@ +[ + { + "service": "zitadel.settings.v2.SettingsService", + "method": "GetBrandingSettings", + "out": { + "data": {} + } + }, + { + "service": "zitadel.settings.v2.SettingsService", + "method": "GetSecuritySettings", + "out": { + "data": {} + } + }, + { + "service": "zitadel.settings.v2.SettingsService", + "method": "GetLegalAndSupportSettings", + "out": { + "data": { + "settings": { + "tosLink": "http://whatever.com/help", + "privacyPolicyLink": "http://whatever.com/help", + "helpLink": "http://whatever.com/help" + } + } + } + }, + { + "service": "zitadel.settings.v2.SettingsService", + "method": "GetActiveIdentityProviders", + "out": { + "data": { + "identityProviders": [ + { + "id": "123", + "name": "Hubba bubba", + "type": 10 + } + ] + } + } + }, + { + "service": "zitadel.settings.v2.SettingsService", + "method": "GetPasswordComplexitySettings", + "out": { + "data": { + "settings": { + "minLength": 8, + "requiresUppercase": true, + "requiresLowercase": true, + "requiresNumber": true, + "requiresSymbol": true + } + } + } + }, + { + "service": "zitadel.settings.v2.SettingsService", + "method": "GetHostedLoginTranslation", + "out": { + "data": {} + } + } +] diff --git a/apps/login/integration/core-mock/mocked-services.cfg b/apps/login/integration/core-mock/mocked-services.cfg new file mode 100644 index 0000000000..6a758ab8c1 --- /dev/null +++ b/apps/login/integration/core-mock/mocked-services.cfg @@ -0,0 +1,7 @@ +zitadel/user/v2/user_service.proto +zitadel/org/v2/org_service.proto +zitadel/session/v2/session_service.proto +zitadel/settings/v2/settings_service.proto +zitadel/management.proto +zitadel/auth.proto +zitadel/admin.proto \ No newline at end of file diff --git a/apps/login/integration/cypress.config.ts b/apps/login/integration/cypress.config.ts new file mode 100644 index 0000000000..a115cd9d1a --- /dev/null +++ b/apps/login/integration/cypress.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from "cypress"; + +export default defineConfig({ + reporter: "list", + + e2e: { + baseUrl: process.env.LOGIN_BASE_URL || "http://localhost:3001/ui/v2/login", + specPattern: "integration/**/*.cy.{js,jsx,ts,tsx}", + supportFile: "support/e2e.{js,jsx,ts,tsx}", + setupNodeEvents(on, config) { + // implement node event listeners here + }, + }, +}); diff --git a/apps/login/integration/fixtures/example.json b/apps/login/integration/fixtures/example.json new file mode 100644 index 0000000000..02e4254378 --- /dev/null +++ b/apps/login/integration/fixtures/example.json @@ -0,0 +1,5 @@ +{ + "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/apps/login/integration/integration/invite.cy.ts b/apps/login/integration/integration/invite.cy.ts new file mode 100644 index 0000000000..a68ff96c36 --- /dev/null +++ b/apps/login/integration/integration/invite.cy.ts @@ -0,0 +1,110 @@ +import { stub } from "../support/e2e"; + +describe("verify invite", () => { + beforeEach(() => { + stub("zitadel.org.v2.OrganizationService", "ListOrganizations", { + data: { + details: { + totalResult: 1, + }, + result: [{ id: "256088834543534543" }], + }, + }); + + stub("zitadel.user.v2.UserService", "ListAuthenticationMethodTypes", { + data: { + authMethodTypes: [], // user with no auth methods was invited + }, + }); + + stub("zitadel.user.v2.UserService", "GetUserByID", { + data: { + user: { + userId: "221394658884845598", + state: 1, + username: "john@zitadel.com", + loginNames: ["john@zitadel.com"], + preferredLoginName: "john@zitadel.com", + human: { + userId: "221394658884845598", + state: 1, + username: "john@zitadel.com", + loginNames: ["john@zitadel.com"], + preferredLoginName: "john@zitadel.com", + profile: { + givenName: "John", + familyName: "Doe", + avatarUrl: "https://zitadel.com/avatar.jpg", + }, + email: { + email: "john@zitadel.com", + isVerified: false, + }, + }, + }, + }, + }); + + stub("zitadel.session.v2.SessionService", "CreateSession", { + data: { + details: { + sequence: 859, + changeDate: new Date("2024-04-04T09:40:55.577Z"), + resourceOwner: "220516472055706145", + }, + sessionId: "221394658884845598", + sessionToken: "SDMc7DlYXPgwRJ-Tb5NlLqynysHjEae3csWsKzoZWLplRji0AYY3HgAkrUEBqtLCvOayLJPMd0ax4Q", + challenges: undefined, + }, + }); + + stub("zitadel.session.v2.SessionService", "GetSession", { + data: { + session: { + id: "221394658884845598", + creationDate: new Date("2024-04-04T09:40:55.577Z"), + changeDate: new Date("2024-04-04T09:40:55.577Z"), + sequence: 859, + factors: { + user: { + id: "221394658884845598", + loginName: "john@zitadel.com", + }, + password: undefined, + webAuthN: undefined, + intent: undefined, + }, + metadata: {}, + }, + }, + }); + + stub("zitadel.settings.v2.SettingsService", "GetLoginSettings", { + data: { + settings: { + passkeysType: 1, + allowUsernamePassword: true, + }, + }, + }); + }); + + it.only("shows authenticators after successful invite verification", () => { + stub("zitadel.user.v2.UserService", "VerifyInviteCode"); + + cy.visit("/verify?userId=221394658884845598&code=abc&invite=true"); + cy.url({ timeout: 10_000 }).should("include", Cypress.config().baseUrl + "/authenticator/set"); + }); + + it("shows an error if invite code validation failed", () => { + stub("zitadel.user.v2.UserService", "VerifyInviteCode", { + code: 3, + error: "error validating code", + }); + + // TODO: Avoid uncaught exception in application + cy.once("uncaught:exception", () => false); + cy.visit("/verify?userId=221394658884845598&code=abc&invite=true"); + cy.contains("Could not verify invite", { timeout: 10_000 }); + }); +}); diff --git a/apps/login/integration/integration/login.cy.ts b/apps/login/integration/integration/login.cy.ts new file mode 100644 index 0000000000..917d719cb1 --- /dev/null +++ b/apps/login/integration/integration/login.cy.ts @@ -0,0 +1,172 @@ +import { stub } from "../support/e2e"; + +describe("login", () => { + beforeEach(() => { + stub("zitadel.org.v2.OrganizationService", "ListOrganizations", { + data: { + details: { + totalResult: 1, + }, + result: [{ id: "256088834543534543" }], + }, + }); + stub("zitadel.session.v2.SessionService", "CreateSession", { + data: { + details: { + sequence: 859, + changeDate: new Date("2024-04-04T09:40:55.577Z"), + resourceOwner: "220516472055706145", + }, + sessionId: "221394658884845598", + sessionToken: "SDMc7DlYXPgwRJ-Tb5NlLqynysHjEae3csWsKzoZWLplRji0AYY3HgAkrUEBqtLCvOayLJPMd0ax4Q", + challenges: undefined, + }, + }); + + stub("zitadel.session.v2.SessionService", "GetSession", { + data: { + session: { + id: "221394658884845598", + creationDate: new Date("2024-04-04T09:40:55.577Z"), + changeDate: new Date("2024-04-04T09:40:55.577Z"), + sequence: 859, + factors: { + user: { + id: "221394658884845598", + loginName: "john@zitadel.com", + }, + password: undefined, + webAuthN: undefined, + intent: undefined, + }, + metadata: {}, + }, + }, + }); + + stub("zitadel.settings.v2.SettingsService", "GetLoginSettings", { + data: { + settings: { + passkeysType: 1, + allowUsernamePassword: true, + }, + }, + }); + }); + describe("password login", () => { + beforeEach(() => { + stub("zitadel.user.v2.UserService", "ListUsers", { + data: { + details: { + totalResult: 1, + }, + result: [ + { + userId: "221394658884845598", + state: 1, + username: "john@zitadel.com", + loginNames: ["john@zitadel.com"], + preferredLoginName: "john@zitadel.com", + human: { + userId: "221394658884845598", + state: 1, + username: "john@zitadel.com", + loginNames: ["john@zitadel.com"], + preferredLoginName: "john@zitadel.com", + profile: { + givenName: "John", + familyName: "Doe", + avatarUrl: "https://zitadel.com/avatar.jpg", + }, + email: { + email: "john@zitadel.com", + isVerified: true, + }, + }, + }, + ], + }, + }); + stub("zitadel.user.v2.UserService", "ListAuthenticationMethodTypes", { + data: { + authMethodTypes: [1], // 1 for password authentication + }, + }); + }); + it("should redirect a user with password authentication to /password", () => { + cy.visit("/loginname?loginName=john%40zitadel.com&submit=true"); + cy.url({ timeout: 10_000 }).should("include", Cypress.config().baseUrl + "/password"); + }); + describe("with passkey prompt", () => { + beforeEach(() => { + stub("zitadel.session.v2.SessionService", "SetSession", { + data: { + details: { + sequence: 859, + changeDate: "2023-07-04T07:58:20.126Z", + resourceOwner: "220516472055706145", + }, + sessionToken: "SDMc7DlYXPgwRJ-Tb5NlLqynysHjEae3csWsKzoZWLplRji0AYY3HgAkrUEBqtLCvOayLJPMd0ax4Q", + challenges: undefined, + }, + }); + }); + // it("should prompt a user to setup passwordless authentication if passkey is allowed in the login settings", () => { + // cy.visit("/loginname?loginName=john%40zitadel.com&submit=true"); + // cy.location("pathname", { timeout: 10_000 }).should("eq", "/password"); + // cy.get('input[type="password"]').focus().type("MyStrongPassword!1"); + // cy.get('button[type="submit"]').click(); + // cy.location("pathname", { timeout: 10_000 }).should( + // "eq", + // "/passkey/set", + // ); + // }); + }); + }); + describe("passkey login", () => { + beforeEach(() => { + stub("zitadel.user.v2.UserService", "ListUsers", { + data: { + details: { + totalResult: 1, + }, + result: [ + { + userId: "221394658884845598", + state: 1, + username: "john@zitadel.com", + loginNames: ["john@zitadel.com"], + preferredLoginName: "john@zitadel.com", + human: { + userId: "221394658884845598", + state: 1, + username: "john@zitadel.com", + loginNames: ["john@zitadel.com"], + preferredLoginName: "john@zitadel.com", + profile: { + givenName: "John", + familyName: "Doe", + avatarUrl: "https://zitadel.com/avatar.jpg", + }, + email: { + email: "john@zitadel.com", + isVerified: true, + }, + }, + }, + ], + }, + }); + stub("zitadel.user.v2.UserService", "ListAuthenticationMethodTypes", { + data: { + authMethodTypes: [2], // 2 for passwordless authentication + }, + }); + }); + + it("should redirect a user with passwordless authentication to /passkey", () => { + cy.visit("/loginname?loginName=john%40zitadel.com&submit=true"); + cy.url({ timeout: 10_000 }).should("include", Cypress.config().baseUrl + "/passkey"); + }); + }); +}); diff --git a/apps/login/integration/integration/register-idp.cy.ts b/apps/login/integration/integration/register-idp.cy.ts new file mode 100644 index 0000000000..73a0c32e00 --- /dev/null +++ b/apps/login/integration/integration/register-idp.cy.ts @@ -0,0 +1,21 @@ +import { stub } from "../support/e2e"; + +const IDP_URL = "https://example.com/idp/url"; + +describe("register idps", () => { + beforeEach(() => { + stub("zitadel.user.v2.UserService", "StartIdentityProviderIntent", { + data: { + authUrl: IDP_URL, + }, + }); + }); + + it("should redirect the user to the correct url", () => { + cy.visit("/idp"); + cy.get('button[e2e="google"]').click(); + cy.origin(IDP_URL, { args: IDP_URL }, (url) => { + cy.location("href", { timeout: 10_000 }).should("eq", url); + }); + }); +}); diff --git a/apps/login/integration/integration/register.cy.ts b/apps/login/integration/integration/register.cy.ts new file mode 100644 index 0000000000..44c53647c1 --- /dev/null +++ b/apps/login/integration/integration/register.cy.ts @@ -0,0 +1,73 @@ +import { stub } from "../support/e2e"; + +describe("register", () => { + beforeEach(() => { + stub("zitadel.org.v2.OrganizationService", "ListOrganizations", { + data: { + details: { + totalResult: 1, + }, + result: [{ id: "256088834543534543" }], + }, + }); + stub("zitadel.settings.v2.SettingsService", "GetLoginSettings", { + data: { + settings: { + passkeysType: 1, + allowRegister: true, + allowUsernamePassword: true, + defaultRedirectUri: "", + }, + }, + }); + stub("zitadel.user.v2.UserService", "AddHumanUser", { + data: { + userId: "221394658884845598", + }, + }); + stub("zitadel.session.v2.SessionService", "CreateSession", { + data: { + details: { + sequence: 859, + changeDate: new Date("2024-04-04T09:40:55.577Z"), + resourceOwner: "220516472055706145", + }, + sessionId: "221394658884845598", + sessionToken: "SDMc7DlYXPgwRJ-Tb5NlLqynysHjEae3csWsKzoZWLplRji0AYY3HgAkrUEBqtLCvOayLJPMd0ax4Q", + challenges: undefined, + }, + }); + + stub("zitadel.session.v2.SessionService", "GetSession", { + data: { + session: { + id: "221394658884845598", + creationDate: new Date("2024-04-04T09:40:55.577Z"), + changeDate: new Date("2024-04-04T09:40:55.577Z"), + sequence: 859, + factors: { + user: { + id: "221394658884845598", + loginName: "john@zitadel.com", + }, + password: undefined, + webAuthN: undefined, + intent: undefined, + }, + metadata: {}, + }, + }, + }); + }); + + it("should redirect a user who selects passwordless on register to /passkey/set", () => { + cy.visit("/register"); + cy.get('input[data-testid="firstname-text-input"]').focus().type("John"); + cy.get('input[data-testid="lastname-text-input"]').focus().type("Doe"); + cy.get('input[data-testid="email-text-input"]').focus().type("john@zitadel.com"); + cy.get('input[type="checkbox"][value="privacypolicy"]').check(); + cy.get('input[type="checkbox"][value="tos"]').check(); + cy.get('button[type="submit"]').click(); + cy.url({ timeout: 10_000 }).should("include", Cypress.config().baseUrl + "/passkey/set"); + }); +}); diff --git a/apps/login/integration/integration/verify.cy.ts b/apps/login/integration/integration/verify.cy.ts new file mode 100644 index 0000000000..db80cea720 --- /dev/null +++ b/apps/login/integration/integration/verify.cy.ts @@ -0,0 +1,95 @@ +import { stub } from "../support/e2e"; + +describe("verify email", () => { + beforeEach(() => { + stub("zitadel.org.v2.OrganizationService", "ListOrganizations", { + data: { + details: { + totalResult: 1, + }, + result: [{ id: "256088834543534543" }], + }, + }); + + stub("zitadel.user.v2.UserService", "ListAuthenticationMethodTypes", { + data: { + authMethodTypes: [1], // set one method such that we know that the user was not invited + }, + }); + + stub("zitadel.user.v2.UserService", "SendEmailCode"); + + stub("zitadel.user.v2.UserService", "GetUserByID", { + data: { + user: { + userId: "221394658884845598", + state: 1, + username: "john@zitadel.com", + loginNames: ["john@zitadel.com"], + preferredLoginName: "john@zitadel.com", + human: { + userId: "221394658884845598", + state: 1, + username: "john@zitadel.com", + loginNames: ["john@zitadel.com"], + preferredLoginName: "john@zitadel.com", + profile: { + givenName: "John", + familyName: "Doe", + avatarUrl: "https://zitadel.com/avatar.jpg", + }, + email: { + email: "john@zitadel.com", + isVerified: false, // email is not verified yet + }, + }, + }, + }, + }); + + stub("zitadel.session.v2.SessionService", "CreateSession", { + data: { + details: { + sequence: 859, + changeDate: new Date("2024-04-04T09:40:55.577Z"), + resourceOwner: "220516472055706145", + }, + sessionId: "221394658884845598", + sessionToken: "SDMc7DlYXPgwRJ-Tb5NlLqynysHjEae3csWsKzoZWLplRji0AYY3HgAkrUEBqtLCvOayLJPMd0ax4Q", + challenges: undefined, + }, + }); + + stub("zitadel.session.v2.SessionService", "GetSession", { + data: { + session: { + id: "221394658884845598", + creationDate: new Date("2024-04-04T09:40:55.577Z"), + changeDate: new Date("2024-04-04T09:40:55.577Z"), + sequence: 859, + factors: { + user: { + id: "221394658884845598", + loginName: "john@zitadel.com", + }, + password: undefined, + webAuthN: undefined, + intent: undefined, + }, + metadata: {}, + }, + }, + }); + }); + + it("shows an error if email code validation failed", () => { + stub("zitadel.user.v2.UserService", "VerifyEmail", { + code: 3, + error: "error validating code", + }); + // TODO: Avoid uncaught exception in application + cy.once("uncaught:exception", () => false); + cy.visit("/verify?userId=221394658884845598&code=abc"); + cy.contains("Could not verify email", { timeout: 10_000 }); + }); +}); diff --git a/apps/login/integration/support/e2e.ts b/apps/login/integration/support/e2e.ts new file mode 100644 index 0000000000..1ac0eb3948 --- /dev/null +++ b/apps/login/integration/support/e2e.ts @@ -0,0 +1,29 @@ +const url = Cypress.env("CORE_MOCK_STUBS_URL") || "http://mock-zitadel:22220/v1/stubs"; + +function removeStub(service: string, method: string) { + return cy.request({ + url, + method: "DELETE", + qs: { + service, + method, + }, + }); +} + +export function stub(service: string, method: string, out?: any) { + removeStub(service, method); + return cy.request({ + url, + method: "POST", + body: { + stubs: [ + { + service, + method, + out, + }, + ], + }, + }); +} diff --git a/apps/login/integration/tsconfig.json b/apps/login/integration/tsconfig.json new file mode 100644 index 0000000000..18edb199ac --- /dev/null +++ b/apps/login/integration/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["es5", "dom"], + "types": ["cypress", "node"] + }, + "include": ["**/*.ts"] +} diff --git a/apps/login/locales/de.json b/apps/login/locales/de.json new file mode 100644 index 0000000000..7b2a507fe4 --- /dev/null +++ b/apps/login/locales/de.json @@ -0,0 +1,292 @@ +{ + "common": { + "back": "Zurück" + }, + "accounts": { + "title": "Konten", + "description": "Wählen Sie das Konto aus, das Sie verwenden möchten.", + "addAnother": "Ein weiteres Konto hinzufügen", + "noResults": "Keine Konten gefunden", + "verified": "verifiziert", + "expired": "abgelaufen" + }, + "logout": { + "title": "Logout", + "description": "Wählen Sie den Account aus, das Sie entfernen möchten", + "noResults": "Keine Konten gefunden", + "clear": "Session beenden", + "verifiedAt": "Zuletzt aktiv: {time}", + "success": { + "title": "Logout erfolgreich", + "description": "Sie haben sich erfolgreich abgemeldet." + } + }, + "loginname": { + "title": "Willkommen zurück!", + "description": "Geben Sie Ihre Anmeldedaten ein.", + "register": "Neuen Benutzer registrieren", + "submit": "Weiter", + "required": { + "loginName": "Dieses Feld ist erforderlich" + } + }, + "password": { + "verify": { + "title": "Passwort", + "description": "Geben Sie Ihr Passwort ein.", + "resetPassword": "Passwort zurücksetzen", + "submit": "Weiter", + "required": { + "password": "Dieses Feld ist erforderlich" + } + }, + "set": { + "title": "Passwort festlegen", + "description": "Legen Sie das Passwort für Ihr Konto fest", + "codeSent": "Ein Code wurde an Ihre E-Mail-Adresse gesendet.", + "noCodeReceived": "Keinen Code erhalten?", + "resend": "Erneut senden", + "submit": "Weiter", + "required": { + "code": "Dieses Feld ist erforderlich", + "newPassword": "Bitte geben Sie ein Passwort ein!", + "confirmPassword": "Dieses Feld ist erforderlich" + } + }, + "change": { + "title": "Passwort ändern", + "description": "Legen Sie das Passwort für Ihr Konto fest", + "submit": "Weiter", + "required": { + "newPassword": "Bitte geben Sie ein neues Passwort ein!", + "confirmPassword": "Dieses Feld ist erforderlich" + } + } + }, + "idp": { + "title": "Mit SSO anmelden", + "description": "Wählen Sie einen der folgenden Anbieter, um sich anzumelden", + "orSignInWith": "oder melden Sie sich an mit", + "signInWithApple": "Mit Apple anmelden", + "signInWithGoogle": "Mit Google anmelden", + "signInWithAzureAD": "Mit AzureAD anmelden", + "signInWithGithub": "Mit GitHub anmelden", + "signInWithGitlab": "Mit GitLab anmelden", + "loginSuccess": { + "title": "Anmeldung erfolgreich", + "description": "Sie haben sich erfolgreich angemeldet!" + }, + "linkingSuccess": { + "title": "Konto verknüpft", + "description": "Sie haben Ihr Konto erfolgreich verknüpft!" + }, + "registerSuccess": { + "title": "Registrierung erfolgreich", + "description": "Sie haben sich erfolgreich registriert!" + }, + "loginError": { + "title": "Anmeldung fehlgeschlagen", + "description": "Beim Anmelden ist ein Fehler aufgetreten." + }, + "linkingError": { + "title": "Konto-Verknüpfung fehlgeschlagen", + "description": "Beim Verknüpfen Ihres Kontos ist ein Fehler aufgetreten." + }, + "completeRegister": { + "title": "Registrierung abschließen", + "description": "Bitte vervollständige die Registrierung, um dein Konto zu erstellen." + } + }, + "ldap": { + "title": "LDAP Login", + "description": "Geben Sie Ihre LDAP-Anmeldedaten ein.", + "username": "Benutzername", + "password": "Passwort", + "submit": "Weiter", + "required": { + "username": "Dieses Feld ist erforderlich", + "password": "Dieses Feld ist erforderlich" + } + }, + "mfa": { + "verify": { + "title": "Bestätigen Sie Ihre Identität", + "description": "Wählen Sie einen der folgenden Faktoren.", + "noResults": "Keine zweiten Faktoren verfügbar, um sie einzurichten." + }, + "set": { + "title": "2-Faktor einrichten", + "description": "Wählen Sie einen der folgenden zweiten Faktoren.", + "skip": "Überspringen" + } + }, + "otp": { + "verify": { + "title": "2-Faktor bestätigen", + "totpDescription": "Geben Sie den Code aus Ihrer Authentifizierungs-App ein.", + "smsDescription": "Geben Sie den Code ein, den Sie per SMS erhalten haben.", + "emailDescription": "Geben Sie den Code ein, den Sie per E-Mail erhalten haben.", + "noCodeReceived": "Keinen Code erhalten?", + "resendCode": "Code erneut senden", + "submit": "Weiter", + "required": { + "code": "Dieses Feld ist erforderlich" + } + }, + "set": { + "title": "2-Faktor einrichten", + "totpDescription": "Scannen Sie den QR-Code mit Ihrer Authentifizierungs-App.", + "smsDescription": "Geben Sie Ihre Telefonnummer ein, um einen Code per SMS zu erhalten.", + "emailDescription": "Geben Sie Ihre E-Mail-Adresse ein, um einen Code per E-Mail zu erhalten.", + "totpRegisterDescription": "Scannen Sie den QR-Code oder navigieren Sie manuell zur URL.", + "submit": "Weiter", + "required": { + "code": "Dieses Feld ist erforderlich" + } + } + }, + "passkey": { + "verify": { + "title": "Mit einem Passkey authentifizieren", + "description": "Ihr Gerät wird nach Ihrem Fingerabdruck, Gesicht oder Bildschirmsperre fragen", + "usePassword": "Passwort verwenden", + "submit": "Weiter" + }, + "set": { + "title": "Passkey einrichten", + "description": "Ihr Gerät wird nach Ihrem Fingerabdruck, Gesicht oder Bildschirmsperre fragen", + "info": { + "description": "Ein Passkey ist eine Authentifizierungsmethode auf einem Gerät wie Ihr Fingerabdruck, Apple FaceID oder ähnliches.", + "link": "Passwortlose Authentifizierung" + }, + "skip": "Überspringen", + "submit": "Weiter" + } + }, + "u2f": { + "verify": { + "title": "2-Faktor bestätigen", + "description": "Bestätigen Sie Ihr Konto mit Ihrem Gerät." + }, + "set": { + "title": "2-Faktor einrichten", + "description": "Richten Sie ein Gerät als zweiten Faktor ein.", + "submit": "Weiter" + } + }, + "register": { + "methods": { + "passkey": "Passkey", + "password": "Password" + }, + "disabled": { + "title": "Registrierung deaktiviert", + "description": "Die Registrierung ist deaktiviert. Bitte wenden Sie sich an den Administrator." + }, + "missingdata": { + "title": "Registrierung fehlgeschlagen", + "description": "Einige Daten fehlen. Bitte überprüfen Sie Ihre Eingaben." + }, + "title": "Registrieren", + "description": "Erstellen Sie Ihr ZITADEL-Konto.", + "noMethodAvailableWarning": "Keine Authentifizierungsmethode verfügbar. Bitte wenden Sie sich an den Administrator.", + "selectMethod": "Wählen Sie die Methode, mit der Sie sich authentifizieren möchten", + "agreeTo": "Um sich zu registrieren, müssen Sie den Nutzungsbedingungen zustimmen", + "termsOfService": "Nutzungsbedingungen", + "privacyPolicy": "Datenschutzrichtlinie", + "submit": "Weiter", + "orUseIDP": "oder verwenden Sie einen Identitätsanbieter", + "password": { + "title": "Passwort festlegen", + "description": "Legen Sie das Passwort für Ihr Konto fest", + "submit": "Weiter", + "required": { + "password": "Bitte geben Sie ein Passwort ein!", + "confirmPassword": "Dieses Feld ist erforderlich" + } + }, + "required": { + "firstname": "Dieses Feld ist erforderlich", + "lastname": "Dieses Feld ist erforderlich", + "email": "Dieses Feld ist erforderlich" + } + }, + "invite": { + "title": "Benutzer einladen", + "description": "Geben Sie die E-Mail-Adresse des Benutzers ein, den Sie einladen möchten.", + "info": "Der Benutzer erhält eine E-Mail mit einem Link, um sich zu registrieren.", + "notAllowed": "Sie haben keine Berechtigung, Benutzer einzuladen.", + "submit": "Einladen", + "success": { + "title": "Einladung erfolgreich", + "description": "Der Benutzer wurde erfolgreich eingeladen.", + "verified": "Der Benutzer wurde eingeladen und hat seine E-Mail bereits verifiziert.", + "notVerifiedYet": "Der Benutzer wurde eingeladen. Er erhält eine E-Mail mit weiteren Anweisungen.", + "submit": "Weiteren Benutzer einladen" + } + }, + "signedin": { + "title": "Willkommen {user}!", + "description": "Sie sind angemeldet.", + "continue": "Weiter", + "error": { + "title": "Fehler", + "description": "Ein Fehler ist aufgetreten." + } + }, + "verify": { + "userIdMissing": "Keine Benutzer-ID angegeben!", + "successTitle": "Benutzer verifiziert", + "successDescription": "Der Benutzer wurde erfolgreich verifiziert.", + "setupAuthenticator": "Authentifikator einrichten", + "verify": { + "title": "Benutzer verifizieren", + "description": "Geben Sie den Code ein, der in der Bestätigungs-E-Mail angegeben ist.", + "noCodeReceived": "Keinen Code erhalten?", + "resendCode": "Code erneut senden", + "codeSent": "Ein Code wurde gerade an Ihre E-Mail-Adresse gesendet.", + "submit": "Weiter", + "required": { + "code": "Dieses Feld ist erforderlich" + } + } + }, + "authenticator": { + "title": "Authentifizierungsmethode auswählen", + "description": "Wählen Sie die Methode, mit der Sie sich authentifizieren möchten.", + "noMethodsAvailable": "Keine Authentifizierungsmethoden verfügbar", + "allSetup": "Sie haben bereits einen Authentifikator eingerichtet!", + "linkWithIDP": "oder verknüpfe mit einem Identitätsanbieter" + }, + "device": { + "usercode": { + "title": "Gerätecode", + "description": "Geben Sie den Code ein.", + "submit": "Weiter", + "required": { + "code": "Dieses Feld ist erforderlich" + } + }, + "request": { + "title": "{appName} möchte eine Verbindung herstellen:", + "disclaimer": "{appName} hat Zugriff auf:", + "description": "Durch Klicken auf Zulassen erlauben Sie {appName} und Zitadel, Ihre Informationen gemäß ihren jeweiligen Nutzungsbedingungen und Datenschutzrichtlinien zu verwenden. Sie können diesen Zugriff jederzeit widerrufen.", + "submit": "Zulassen", + "deny": "Ablehnen" + }, + "scope": { + "openid": "Überprüfen Ihrer Identität.", + "email": "Zugriff auf Ihre E-Mail-Adresse.", + "profile": "Zugriff auf Ihre vollständigen Profilinformationen.", + "offline_access": "Erlauben Sie den Offline-Zugriff auf Ihr Konto." + } + }, + "error": { + "noUserCode": "Kein Benutzercode angegeben!", + "noDeviceRequest": " Es wurde keine Geräteanforderung gefunden. Bitte überprüfen Sie die URL.", + "unknownContext": "Der Kontext des Benutzers konnte nicht ermittelt werden. Stellen Sie sicher, dass Sie zuerst den Benutzernamen eingeben oder einen loginName als Suchparameter angeben.", + "sessionExpired": "Ihre aktuelle Sitzung ist abgelaufen. Bitte melden Sie sich erneut an.", + "failedLoading": "Daten konnten nicht geladen werden. Bitte versuchen Sie es erneut.", + "tryagain": "Erneut versuchen" + } +} diff --git a/apps/login/locales/en.json b/apps/login/locales/en.json new file mode 100644 index 0000000000..e1b7e4e82c --- /dev/null +++ b/apps/login/locales/en.json @@ -0,0 +1,292 @@ +{ + "common": { + "back": "Back" + }, + "accounts": { + "title": "Accounts", + "description": "Select the account you want to use.", + "addAnother": "Add another account", + "noResults": "No accounts found", + "verified": "verified", + "expired": "expired" + }, + "logout": { + "title": "Logout", + "description": "Click an account to end the session", + "noResults": "No accounts found", + "clear": "End Session", + "verifiedAt": "Last active: {time}", + "success": { + "title": "Logout successful", + "description": "You have successfully logged out." + } + }, + "loginname": { + "title": "Welcome back!", + "description": "Enter your login data.", + "register": "Register new user", + "submit": "Continue", + "required": { + "loginName": "This field is required" + } + }, + "password": { + "verify": { + "title": "Password", + "description": "Enter your password.", + "resetPassword": "Reset Password", + "submit": "Continue", + "required": { + "password": "This field is required" + } + }, + "set": { + "title": "Set Password", + "description": "Set the password for your account", + "codeSent": "A code has been sent to your email address.", + "noCodeReceived": "Didn't receive a code?", + "resend": "Resend code", + "submit": "Continue", + "required": { + "code": "This field is required", + "newPassword": "You have to provide a password!", + "confirmPassword": "This field is required" + } + }, + "change": { + "title": "Change Password", + "description": "Set the password for your account", + "submit": "Continue", + "required": { + "newPassword": "You have to provide a new password!", + "confirmPassword": "This field is required" + } + } + }, + "idp": { + "title": "Sign in with SSO", + "description": "Select one of the following providers to sign in", + "orSignInWith": "or sign in with", + "signInWithApple": "Sign in with Apple", + "signInWithGoogle": "Sign in with Google", + "signInWithAzureAD": "Sign in with AzureAD", + "signInWithGithub": "Sign in with GitHub", + "signInWithGitlab": "Sign in with GitLab", + "loginSuccess": { + "title": "Login successful", + "description": "You have successfully been loggedIn!" + }, + "linkingSuccess": { + "title": "Account linked", + "description": "You have successfully linked your account!" + }, + "registerSuccess": { + "title": "Registration successful", + "description": "You have successfully registered!" + }, + "loginError": { + "title": "Login failed", + "description": "An error occurred while trying to login." + }, + "linkingError": { + "title": "Account linking failed", + "description": "An error occurred while trying to link your account." + }, + "completeRegister": { + "title": "Complete your data", + "description": "You need to complete your registration by providing your email address and name." + } + }, + "ldap": { + "title": "LDAP Login", + "description": "Enter your LDAP credentials.", + "username": "Username", + "password": "Password", + "submit": "Continue", + "required": { + "username": "This field is required", + "password": "This field is required" + } + }, + "mfa": { + "verify": { + "title": "Verify your identity", + "description": "Choose one of the following factors.", + "noResults": "No second factors available to setup." + }, + "set": { + "title": "Set up 2-Factor", + "description": "Choose one of the following second factors.", + "skip": "Skip" + } + }, + "otp": { + "verify": { + "title": "Verify 2-Factor", + "totpDescription": "Enter the code from your authenticator app.", + "smsDescription": "Enter the code you received via SMS.", + "emailDescription": "Enter the code you received via email.", + "noCodeReceived": "Didn't receive a code?", + "resendCode": "Resend code", + "submit": "Continue", + "required": { + "code": "This field is required" + } + }, + "set": { + "title": "Set up 2-Factor", + "totpDescription": "Scan the QR code with your authenticator app.", + "smsDescription": "Enter your phone number to receive a code via SMS.", + "emailDescription": "Enter your email address to receive a code via email.", + "totpRegisterDescription": "Scan the QR Code or navigate to the URL manually.", + "submit": "Continue", + "required": { + "code": "This field is required" + } + } + }, + "passkey": { + "verify": { + "title": "Authenticate with a passkey", + "description": "Your device will ask for your fingerprint, face, or screen lock", + "usePassword": "Use password", + "submit": "Continue" + }, + "set": { + "title": "Setup a passkey", + "description": "Your device will ask for your fingerprint, face, or screen lock", + "info": { + "description": "A passkey is an authentication method on a device like your fingerprint, Apple FaceID or similar. ", + "link": "Passwordless Authentication" + }, + "skip": "Skip", + "submit": "Continue" + } + }, + "u2f": { + "verify": { + "title": "Verify 2-Factor", + "description": "Verify your account with your device." + }, + "set": { + "title": "Set up 2-Factor", + "description": "Set up a device as a second factor.", + "submit": "Continue" + } + }, + "register": { + "methods": { + "passkey": "Passkey", + "password": "Password" + }, + "disabled": { + "title": "Registration disabled", + "description": "The registration is disabled. Please contact your administrator." + }, + "missingdata": { + "title": "Missing data", + "description": "Provide email, first and last name to register." + }, + "title": "Register", + "description": "Create your ZITADEL account.", + "noMethodAvailableWarning": "No authentication method available. Please contact your administrator.", + "selectMethod": "Select the method you would like to authenticate", + "agreeTo": "To register you must agree to the terms and conditions", + "termsOfService": "Terms of Service", + "privacyPolicy": "Privacy Policy", + "submit": "Continue", + "orUseIDP": "or use an Identity Provider", + "password": { + "title": "Set Password", + "description": "Set the password for your account", + "submit": "Continue", + "required": { + "password": "You have to provide a password!", + "confirmPassword": "This field is required" + } + }, + "required": { + "firstname": "This field is required", + "lastname": "This field is required", + "email": "This field is required" + } + }, + "invite": { + "title": "Invite User", + "description": "Provide the email address and the name of the user you want to invite.", + "info": "The user will receive an email with further instructions.", + "notAllowed": "Your settings do not allow you to invite users.", + "submit": "Continue", + "success": { + "title": "User invited", + "description": "The email has successfully been sent.", + "verified": "The user has been invited and has already verified his email.", + "notVerifiedYet": "The user has been invited. They will receive an email with further instructions.", + "submit": "Invite another user" + } + }, + "signedin": { + "title": "Welcome {user}!", + "description": "You are signed in.", + "continue": "Continue", + "error": { + "title": "Error", + "description": "An error occurred while trying to sign in." + } + }, + "verify": { + "userIdMissing": "No userId provided!", + "successTitle": "User verified", + "successDescription": "The user has been verified successfully.", + "setupAuthenticator": "Setup authenticator", + "verify": { + "title": "Verify user", + "description": "Enter the Code provided in the verification email.", + "noCodeReceived": "Didn't receive a code?", + "resendCode": "Resend code", + "codeSent": "A code has just been sent to your email address.", + "submit": "Continue", + "required": { + "code": "This field is required" + } + } + }, + "authenticator": { + "title": "Choose authentication method", + "description": "Select the method you would like to authenticate", + "noMethodsAvailable": "No authentication methods available", + "allSetup": "You have already setup an authenticator!", + "linkWithIDP": "or link with an Identity Provider" + }, + "device": { + "usercode": { + "title": "Device code", + "description": "Enter the code displayed on your app or device.", + "submit": "Continue", + "required": { + "code": "This field is required" + } + }, + "request": { + "title": "{appName} would like to connect", + "description": "{appName} will have access to:", + "disclaimer": "By clicking Allow, you allow {appName} and Zitadel to use your information in accordance with their respective terms of service and privacy policies. You can revoke this access at any time.", + "submit": "Allow", + "deny": "Deny" + }, + "scope": { + "openid": "Verify your identity.", + "email": "View your email address.", + "profile": "View your full profile information.", + "offline_access": "Allow offline access to your account." + } + }, + "error": { + "noUserCode": "No user code provided!", + "noDeviceRequest": "No device request found.", + "unknownContext": "Could not get the context of the user. Make sure to enter the username first or provide a loginName as searchParam.", + "sessionExpired": "Your current session has expired. Please login again.", + "failedLoading": "Failed to load data. Please try again.", + "tryagain": "Try Again" + } +} diff --git a/apps/login/locales/es.json b/apps/login/locales/es.json new file mode 100644 index 0000000000..b1c63583a7 --- /dev/null +++ b/apps/login/locales/es.json @@ -0,0 +1,292 @@ +{ + "common": { + "back": "Atrás" + }, + "accounts": { + "title": "Cuentas", + "description": "Seleccione la cuenta que desea utilizar.", + "addAnother": "Agregar otra cuenta", + "noResults": "No se encontraron cuentas", + "verified": "verificado", + "expired": "expirado" + }, + "logout": { + "title": "Cerrar sesión", + "description": "Selecciona la cuenta que deseas eliminar", + "noResults": "No se encontraron cuentas", + "clear": "Eliminar sesión", + "verifiedAt": "Última actividad: {time}", + "success": { + "title": "Cierre de sesión exitoso", + "description": "Has cerrado sesión correctamente." + } + }, + "loginname": { + "title": "¡Bienvenido de nuevo!", + "description": "Introduce tus datos de acceso.", + "register": "Registrar nuevo usuario", + "submit": "Continuar", + "required": { + "loginName": "Este campo es obligatorio" + } + }, + "password": { + "verify": { + "title": "Contraseña", + "description": "Introduce tu contraseña.", + "resetPassword": "Restablecer contraseña", + "submit": "Continuar", + "required": { + "password": "Este campo es obligatorio" + } + }, + "set": { + "title": "Establecer Contraseña", + "description": "Establece la contraseña para tu cuenta", + "codeSent": "Se ha enviado un código a su correo electrónico.", + "noCodeReceived": "¿No recibiste un código?", + "resend": "Reenviar código", + "submit": "Continuar", + "required": { + "code": "Este campo es obligatorio", + "newPassword": "¡Debes proporcionar una contraseña!", + "confirmPassword": "Este campo es obligatorio" + } + }, + "change": { + "title": "Cambiar Contraseña", + "description": "Establece la contraseña para tu cuenta", + "submit": "Continuar", + "required": { + "newPassword": "¡Debes proporcionar una nueva contraseña!", + "confirmPassword": "Este campo es obligatorio" + } + } + }, + "idp": { + "title": "Iniciar sesión con SSO", + "description": "Selecciona uno de los siguientes proveedores para iniciar sesión", + "orSignInWith": "o iniciar sesión con", + "signInWithApple": "Iniciar sesión con Apple", + "signInWithGoogle": "Iniciar sesión con Google", + "signInWithAzureAD": "Iniciar sesión con AzureAD", + "signInWithGithub": "Iniciar sesión con GitHub", + "signInWithGitlab": "Iniciar sesión con GitLab", + "loginSuccess": { + "title": "Inicio de sesión exitoso", + "description": "¡Has iniciado sesión con éxito!" + }, + "linkingSuccess": { + "title": "Cuenta vinculada", + "description": "¡Has vinculado tu cuenta con éxito!" + }, + "registerSuccess": { + "title": "Registro exitoso", + "description": "¡Te has registrado con éxito!" + }, + "loginError": { + "title": "Error de inicio de sesión", + "description": "Ocurrió un error al intentar iniciar sesión." + }, + "linkingError": { + "title": "Error al vincular la cuenta", + "description": "Ocurrió un error al intentar vincular tu cuenta." + }, + "completeRegister": { + "title": "Completar registro", + "description": "Para completar el registro, debes establecer una contraseña." + } + }, + "ldap": { + "title": "Iniciar sesión con LDAP", + "description": "Introduce tus credenciales LDAP.", + "username": "Nombre de usuario", + "password": "Contraseña", + "submit": "Continuar", + "required": { + "username": "Este campo es obligatorio", + "password": "Este campo es obligatorio" + } + }, + "mfa": { + "verify": { + "title": "Verifica tu identidad", + "description": "Elige uno de los siguientes factores.", + "noResults": "No hay factores secundarios disponibles para configurar." + }, + "set": { + "title": "Configurar autenticación de 2 factores", + "description": "Elige uno de los siguientes factores secundarios.", + "skip": "Omitir" + } + }, + "otp": { + "verify": { + "title": "Verificar autenticación de 2 factores", + "totpDescription": "Introduce el código de tu aplicación de autenticación.", + "smsDescription": "Introduce el código que recibiste por SMS.", + "emailDescription": "Introduce el código que recibiste por correo electrónico.", + "noCodeReceived": "¿No recibiste un código?", + "resendCode": "Reenviar código", + "submit": "Continuar", + "required": { + "code": "Este campo es obligatorio" + } + }, + "set": { + "title": "Configurar autenticación de 2 factores", + "totpDescription": "Escanea el código QR con tu aplicación de autenticación.", + "smsDescription": "Introduce tu número de teléfono para recibir un código por SMS.", + "emailDescription": "Introduce tu dirección de correo electrónico para recibir un código por correo electrónico.", + "totpRegisterDescription": "Escanea el código QR o navega manualmente a la URL.", + "submit": "Continuar", + "required": { + "code": "Este campo es obligatorio" + } + } + }, + "passkey": { + "verify": { + "title": "Autenticar con una clave de acceso", + "description": "Tu dispositivo pedirá tu huella digital, rostro o bloqueo de pantalla", + "usePassword": "Usar contraseña", + "submit": "Continuar" + }, + "set": { + "title": "Configurar una clave de acceso", + "description": "Tu dispositivo pedirá tu huella digital, rostro o bloqueo de pantalla", + "info": { + "description": "Una clave de acceso es un método de autenticación en un dispositivo como tu huella digital, Apple FaceID o similar.", + "link": "Autenticación sin contraseña" + }, + "skip": "Omitir", + "submit": "Continuar" + } + }, + "u2f": { + "verify": { + "title": "Verificar autenticación de 2 factores", + "description": "Verifica tu cuenta con tu dispositivo." + }, + "set": { + "title": "Configurar autenticación de 2 factores", + "description": "Configura un dispositivo como segundo factor.", + "submit": "Continuar" + } + }, + "register": { + "methods": { + "passkey": "Clave de acceso", + "password": "Contraseña" + }, + "disabled": { + "title": "Registro deshabilitado", + "description": "Registrarse está deshabilitado en este momento." + }, + "missingdata": { + "title": "Datos faltantes", + "description": "No se proporcionaron datos suficientes para el registro." + }, + "title": "Registrarse", + "description": "Crea tu cuenta ZITADEL.", + "noMethodAvailableWarning": "No hay métodos de autenticación disponibles. Por favor, contacta a tu administrador.", + "selectMethod": "Selecciona el método con el que deseas autenticarte", + "agreeTo": "Para registrarte debes aceptar los términos y condiciones", + "termsOfService": "Términos de Servicio", + "privacyPolicy": "Política de Privacidad", + "submit": "Continuar", + "orUseIDP": "o usa un Proveedor de Identidad", + "password": { + "title": "Establecer Contraseña", + "description": "Establece la contraseña para tu cuenta", + "submit": "Continuar", + "required": { + "password": "¡Debes proporcionar una contraseña!", + "confirmPassword": "Este campo es obligatorio" + } + }, + "required": { + "firstname": "Este campo es obligatorio", + "lastname": "Este campo es obligatorio", + "email": "Este campo es obligatorio" + } + }, + "invite": { + "title": "Invitar usuario", + "description": "Introduce el correo electrónico del usuario que deseas invitar.", + "info": "El usuario recibirá un correo electrónico con un enlace para completar el registro.", + "notAllowed": "No tienes permiso para invitar usuarios.", + "submit": "Invitar usuario", + "success": { + "title": "¡Usuario invitado!", + "description": "El usuario ha sido invitado.", + "verified": "El usuario ha sido invitado y ya ha verificado su correo electrónico.", + "notVerifiedYet": "El usuario ha sido invitado. Recibirá un correo electrónico con más instrucciones.", + "submit": "Invitar a otro usuario" + } + }, + "signedin": { + "title": "¡Bienvenido {user}!", + "description": "Has iniciado sesión.", + "continue": "Continuar", + "error": { + "title": "Error", + "description": "Ocurrió un error al iniciar sesión." + } + }, + "verify": { + "userIdMissing": "¡No se proporcionó userId!", + "successTitle": "Usuario verificado", + "successDescription": "El usuario ha sido verificado con éxito.", + "setupAuthenticator": "Configurar autenticador", + "verify": { + "title": "Verificar usuario", + "description": "Introduce el código proporcionado en el correo electrónico de verificación.", + "noCodeReceived": "¿No recibiste un código?", + "resendCode": "Reenviar código", + "codeSent": "Se ha enviado un código a tu dirección de correo electrónico.", + "submit": "Continuar", + "required": { + "code": "Este campo es obligatorio" + } + } + }, + "authenticator": { + "title": "Seleccionar método de autenticación", + "description": "Selecciona el método con el que deseas autenticarte", + "noMethodsAvailable": "No hay métodos de autenticación disponibles", + "allSetup": "¡Ya has configurado un autenticador!", + "linkWithIDP": "o vincúlalo con un proveedor de identidad" + }, + "device": { + "usercode": { + "title": "Código del dispositivo", + "description": "Introduce el código.", + "submit": "Continuar", + "required": { + "code": "Este campo es obligatorio" + } + }, + "request": { + "title": "{appName} desea conectarse:", + "description": "{appName} tendrá acceso a:", + "disclaimer": "Al hacer clic en Permitir, autorizas a {appName} y a Zitadel a usar tu información de acuerdo con sus respectivos términos de servicio y políticas de privacidad. Puedes revocar este acceso en cualquier momento.", + "submit": "Permitir", + "deny": "Denegar" + }, + "scope": { + "openid": "Verifica tu identidad.", + "email": "Accede a tu dirección de correo electrónico.", + "profile": "Accede a la información completa de tu perfil.", + "offline_access": "Permitir acceso sin conexión a tu cuenta." + } + }, + "error": { + "noUserCode": "¡No se proporcionó código de usuario!", + "noDeviceRequest": "No se encontró ninguna solicitud de dispositivo.", + "unknownContext": "No se pudo obtener el contexto del usuario. Asegúrate de ingresar primero el nombre de usuario o proporcionar un loginName como parámetro de búsqueda.", + "sessionExpired": "Tu sesión actual ha expirado. Por favor, inicia sesión de nuevo.", + "failedLoading": "No se pudieron cargar los datos. Por favor, inténtalo de nuevo.", + "tryagain": "Intentar de nuevo" + } +} diff --git a/apps/login/locales/it.json b/apps/login/locales/it.json new file mode 100644 index 0000000000..a71b48ed04 --- /dev/null +++ b/apps/login/locales/it.json @@ -0,0 +1,292 @@ +{ + "common": { + "back": "Indietro" + }, + "accounts": { + "title": "Account", + "description": "Seleziona l'account che vuoi utilizzare.", + "addAnother": "Aggiungi un altro account", + "noResults": "Nessun account trovato", + "verified": "verificato", + "expired": "scaduto" + }, + "logout": { + "title": "Esci", + "description": "Seleziona l'account che desideri uscire", + "noResults": "Nessun account trovato", + "clear": "Elimina sessione", + "verifiedAt": "Ultima attività: {time}", + "success": { + "title": "Uscita riuscita", + "description": "Hai effettuato l'uscita con successo." + } + }, + "loginname": { + "title": "Bentornato!", + "description": "Inserisci i tuoi dati di accesso.", + "register": "Registrati come nuovo utente", + "submit": "Continua", + "required": { + "loginName": "Questo campo è obbligatorio" + } + }, + "password": { + "verify": { + "title": "Password", + "description": "Inserisci la tua password.", + "resetPassword": "Reimposta Password", + "submit": "Continua", + "required": { + "password": "Questo campo è obbligatorio" + } + }, + "set": { + "title": "Imposta Password", + "description": "Imposta la password per il tuo account", + "codeSent": "Un codice è stato inviato al tuo indirizzo email.", + "noCodeReceived": "Non hai ricevuto un codice?", + "resend": "Invia di nuovo", + "submit": "Continua", + "required": { + "code": "Questo campo è obbligatorio", + "newPassword": "Devi fornire una password!", + "confirmPassword": "Questo campo è obbligatorio" + } + }, + "change": { + "title": "Cambia Password", + "description": "Imposta la password per il tuo account", + "submit": "Continua", + "required": { + "newPassword": "Devi fornire una nuova password!", + "confirmPassword": "Questo campo è obbligatorio" + } + } + }, + "idp": { + "title": "Accedi con SSO", + "description": "Seleziona uno dei seguenti provider per accedere", + "orSignInWith": "o accedi con", + "signInWithApple": "Accedi con Apple", + "signInWithGoogle": "Accedi con Google", + "signInWithAzureAD": "Accedi con AzureAD", + "signInWithGithub": "Accedi con GitHub", + "signInWithGitlab": "Accedi con GitLab", + "loginSuccess": { + "title": "Accesso riuscito", + "description": "Accesso effettuato con successo!" + }, + "linkingSuccess": { + "title": "Account collegato", + "description": "Hai collegato con successo il tuo account!" + }, + "registerSuccess": { + "title": "Registrazione riuscita", + "description": "Registrazione effettuata con successo!" + }, + "loginError": { + "title": "Accesso fallito", + "description": "Si è verificato un errore durante il tentativo di accesso." + }, + "linkingError": { + "title": "Collegamento account fallito", + "description": "Si è verificato un errore durante il tentativo di collegare il tuo account." + }, + "completeRegister": { + "title": "Completa la registrazione", + "description": "Completa la registrazione del tuo account." + } + }, + "ldap": { + "title": "Accedi con LDAP", + "description": "Inserisci le tue credenziali LDAP.", + "username": "Nome utente", + "password": "Password", + "submit": "Continua", + "required": { + "username": "Questo campo è obbligatorio", + "password": "Questo campo è obbligatorio" + } + }, + "mfa": { + "verify": { + "title": "Verifica la tua identità", + "description": "Scegli uno dei seguenti fattori.", + "noResults": "Nessun secondo fattore disponibile per la configurazione." + }, + "set": { + "title": "Configura l'autenticazione a 2 fattori", + "description": "Scegli uno dei seguenti secondi fattori.", + "skip": "Salta" + } + }, + "otp": { + "verify": { + "title": "Verifica l'autenticazione a 2 fattori", + "totpDescription": "Inserisci il codice dalla tua app di autenticazione.", + "smsDescription": "Inserisci il codice ricevuto via SMS.", + "emailDescription": "Inserisci il codice ricevuto via email.", + "noCodeReceived": "Non hai ricevuto un codice?", + "resendCode": "Invia di nuovo il codice", + "submit": "Continua", + "required": { + "code": "Questo campo è obbligatorio" + } + }, + "set": { + "title": "Configura l'autenticazione a 2 fattori", + "totpDescription": "Scansiona il codice QR con la tua app di autenticazione.", + "smsDescription": "Inserisci il tuo numero di telefono per ricevere un codice via SMS.", + "emailDescription": "Inserisci il tuo indirizzo email per ricevere un codice via email.", + "totpRegisterDescription": "Scansiona il codice QR o naviga manualmente all'URL.", + "submit": "Continua", + "required": { + "code": "Questo campo è obbligatorio" + } + } + }, + "passkey": { + "verify": { + "title": "Autenticati con una passkey", + "description": "Il tuo dispositivo chiederà la tua impronta digitale, il volto o il blocco schermo", + "usePassword": "Usa password", + "submit": "Continua" + }, + "set": { + "title": "Configura una passkey", + "description": "Il tuo dispositivo chiederà la tua impronta digitale, il volto o il blocco schermo", + "info": { + "description": "Una passkey è un metodo di autenticazione su un dispositivo come la tua impronta digitale, Apple FaceID o simili.", + "link": "Autenticazione senza password" + }, + "skip": "Salta", + "submit": "Continua" + } + }, + "u2f": { + "verify": { + "title": "Verifica l'autenticazione a 2 fattori", + "description": "Verifica il tuo account con il tuo dispositivo." + }, + "set": { + "title": "Configura l'autenticazione a 2 fattori", + "description": "Configura un dispositivo come secondo fattore.", + "submit": "Continua" + } + }, + "register": { + "methods": { + "passkey": "Passkey", + "password": "Password" + }, + "disabled": { + "title": "Registration disabled", + "description": "Registrazione disabilitata. Contatta l'amministratore di sistema per assistenza." + }, + "missingdata": { + "title": "Registrazione", + "description": "Inserisci i tuoi dati per registrarti." + }, + "title": "Registrati", + "description": "Crea il tuo account ZITADEL.", + "noMethodAvailableWarning": "Nessun metodo di autenticazione disponibile. Contatta l'amministratore di sistema per assistenza.", + "selectMethod": "Seleziona il metodo con cui desideri autenticarti", + "agreeTo": "Per registrarti devi accettare i termini e le condizioni", + "termsOfService": "Termini di Servizio", + "privacyPolicy": "Informativa sulla Privacy", + "submit": "Continua", + "orUseIDP": "o usa un Identity Provider", + "password": { + "title": "Imposta Password", + "description": "Imposta la password per il tuo account", + "submit": "Continua", + "required": { + "password": "Devi fornire una password!", + "confirmPassword": "Questo campo è obbligatorio" + } + }, + "required": { + "firstname": "Questo campo è obbligatorio", + "lastname": "Questo campo è obbligatorio", + "email": "Questo campo è obbligatorio" + } + }, + "invite": { + "title": "Invita Utente", + "description": "Inserisci l'indirizzo email dell'utente che desideri invitare.", + "info": "L'utente riceverà un'email con ulteriori istruzioni.", + "notAllowed": "Non hai i permessi per invitare un utente.", + "submit": "Invita Utente", + "success": { + "title": "Invito inviato", + "description": "L'utente è stato invitato con successo.", + "verified": "L'utente è stato invitato e ha già verificato la sua email.", + "notVerifiedYet": "L'utente è stato invitato. Riceverà un'email con ulteriori istruzioni.", + "submit": "Invita un altro utente" + } + }, + "signedin": { + "title": "Benvenuto {user}!", + "description": "Sei connesso.", + "continue": "Continua", + "error": { + "title": "Errore", + "description": "Si è verificato un errore durante il tentativo di accesso." + } + }, + "verify": { + "userIdMissing": "Nessun userId fornito!", + "successTitle": "Utente verificato", + "successDescription": "L'utente è stato verificato con successo.", + "setupAuthenticator": "Configura autenticatore", + "verify": { + "title": "Verifica utente", + "description": "Inserisci il codice fornito nell'email di verifica.", + "noCodeReceived": "Non hai ricevuto un codice?", + "resendCode": "Invia di nuovo il codice", + "codeSent": "Un codice è stato appena inviato al tuo indirizzo email.", + "submit": "Continua", + "required": { + "code": "Questo campo è obbligatorio" + } + } + }, + "authenticator": { + "title": "Seleziona metodo di autenticazione", + "description": "Seleziona il metodo con cui desideri autenticarti", + "noMethodsAvailable": "Nessun metodo di autenticazione disponibile", + "allSetup": "Hai già configurato un autenticatore!", + "linkWithIDP": "o collega con un Identity Provider" + }, + "device": { + "usercode": { + "title": "Codice dispositivo", + "description": "Inserisci il codice.", + "submit": "Continua", + "required": { + "code": "Questo campo è obbligatorio" + } + }, + "request": { + "title": "{appName} desidera connettersi:", + "description": "{appName} avrà accesso a:", + "disclaimer": "Cliccando su Consenti, autorizzi {appName} e Zitadel a utilizzare le tue informazioni in conformità con i rispettivi termini di servizio e politiche sulla privacy. Puoi revocare questo accesso in qualsiasi momento.", + "submit": "Consenti", + "deny": "Nega" + }, + "scope": { + "openid": "Verifica la tua identità.", + "email": "Accedi al tuo indirizzo email.", + "profile": "Accedi alle informazioni complete del tuo profilo.", + "offline_access": "Consenti l'accesso offline al tuo account." + } + }, + "error": { + "noUserCode": "Nessun codice utente fornito!", + "noDeviceRequest": "Nessuna richiesta di dispositivo trovata.", + "unknownContext": "Impossibile ottenere il contesto dell'utente. Assicurati di inserire prima il nome utente o di fornire un loginName come parametro di ricerca.", + "sessionExpired": "La tua sessione attuale è scaduta. Effettua nuovamente l'accesso.", + "failedLoading": "Impossibile caricare i dati. Riprova.", + "tryagain": "Riprova" + } +} diff --git a/apps/login/locales/pl.json b/apps/login/locales/pl.json new file mode 100644 index 0000000000..3e8562a220 --- /dev/null +++ b/apps/login/locales/pl.json @@ -0,0 +1,292 @@ +{ + "common": { + "back": "Powrót" + }, + "accounts": { + "title": "Konta", + "description": "Wybierz konto, którego chcesz użyć.", + "addAnother": "Dodaj kolejne konto", + "noResults": "Nie znaleziono kont", + "verified": "zweryfikowany", + "expired": "wygasł" + }, + "logout": { + "title": "Wyloguj się", + "description": "Wybierz konto, które chcesz usunąć", + "noResults": "Nie znaleziono kont", + "clear": "Usuń sesję", + "verifiedAt": "Ostatnia aktywność: {time}", + "success": { + "title": "Wylogowanie udane", + "description": "Pomyślnie się wylogowałeś." + } + }, + "loginname": { + "title": "Witamy ponownie!", + "description": "Wprowadź dane logowania.", + "register": "Zarejestruj nowego użytkownika", + "submit": "Kontynuuj", + "required": { + "loginName": "To pole jest wymagane" + } + }, + "password": { + "verify": { + "title": "Hasło", + "description": "Wprowadź swoje hasło.", + "resetPassword": "Zresetuj hasło", + "submit": "Kontynuuj", + "required": { + "password": "To pole jest wymagane" + } + }, + "set": { + "title": "Ustaw hasło", + "description": "Ustaw hasło dla swojego konta", + "codeSent": "Kod został wysłany na twój adres e-mail.", + "noCodeReceived": "Nie otrzymałeś kodu?", + "resend": "Wyślij kod ponownie", + "submit": "Kontynuuj", + "required": { + "code": "To pole jest wymagane", + "newPassword": "Musisz podać hasło!", + "confirmPassword": "To pole jest wymagane" + } + }, + "change": { + "title": "Zmień hasło", + "description": "Ustaw nowe hasło dla swojego konta", + "submit": "Kontynuuj", + "required": { + "newPassword": "Musisz podać nowe hasło!", + "confirmPassword": "To pole jest wymagane" + } + } + }, + "idp": { + "title": "Zaloguj się za pomocą SSO", + "description": "Wybierz jednego z poniższych dostawców, aby się zalogować", + "orSignInWith": "lub zaloguj się przez", + "signInWithApple": "Zaloguj się przez Apple", + "signInWithGoogle": "Zaloguj się przez Google", + "signInWithAzureAD": "Zaloguj się przez AzureAD", + "signInWithGithub": "Zaloguj się przez GitHub", + "signInWithGitlab": "Zaloguj się przez GitLab", + "loginSuccess": { + "title": "Logowanie udane", + "description": "Zostałeś pomyślnie zalogowany!" + }, + "linkingSuccess": { + "title": "Konto powiązane", + "description": "Pomyślnie powiązałeś swoje konto!" + }, + "registerSuccess": { + "title": "Rejestracja udana", + "description": "Pomyślnie się zarejestrowałeś!" + }, + "loginError": { + "title": "Logowanie nieudane", + "description": "Wystąpił błąd podczas próby logowania." + }, + "linkingError": { + "title": "Powiązanie konta nie powiodło się", + "description": "Wystąpił błąd podczas próby powiązania konta." + }, + "completeRegister": { + "title": "Ukończ rejestrację", + "description": "Ukończ rejestrację swojego konta." + } + }, + "ldap": { + "title": "Zaloguj się przez LDAP", + "description": "Wprowadź swoje dane logowania LDAP.", + "username": "Nazwa użytkownika", + "password": "Hasło", + "submit": "Kontynuuj", + "required": { + "username": "To pole jest wymagane", + "password": "To pole jest wymagane" + } + }, + "mfa": { + "verify": { + "title": "Zweryfikuj swoją tożsamość", + "description": "Wybierz jeden z poniższych sposobów weryfikacji.", + "noResults": "Nie znaleziono dostępnych metod uwierzytelniania dwuskładnikowego." + }, + "set": { + "title": "Skonfiguruj uwierzytelnianie dwuskładnikowe", + "description": "Wybierz jedną z poniższych metod drugiego czynnika.", + "skip": "Pomiń" + } + }, + "otp": { + "verify": { + "title": "Zweryfikuj uwierzytelnianie dwuskładnikowe", + "totpDescription": "Wprowadź kod z aplikacji uwierzytelniającej.", + "smsDescription": "Wprowadź kod otrzymany SMS-em.", + "emailDescription": "Wprowadź kod otrzymany e-mailem.", + "noCodeReceived": "Nie otrzymałeś kodu?", + "resendCode": "Wyślij kod ponownie", + "submit": "Kontynuuj", + "required": { + "code": "To pole jest wymagane" + } + }, + "set": { + "title": "Skonfiguruj uwierzytelnianie dwuskładnikowe", + "totpDescription": "Zeskanuj kod QR za pomocą aplikacji uwierzytelniającej.", + "smsDescription": "Wprowadź swój numer telefonu, aby otrzymać kod SMS-em.", + "emailDescription": "Wprowadź swój adres e-mail, aby otrzymać kod e-mailem.", + "totpRegisterDescription": "Zeskanuj kod QR lub otwórz adres URL ręcznie.", + "submit": "Kontynuuj", + "required": { + "code": "To pole jest wymagane" + } + } + }, + "passkey": { + "verify": { + "title": "Uwierzytelnij się za pomocą klucza dostępu", + "description": "Twoje urządzenie poprosi o użycie odcisku palca, rozpoznawania twarzy lub blokady ekranu.", + "usePassword": "Użyj hasła", + "submit": "Kontynuuj" + }, + "set": { + "title": "Skonfiguruj klucz dostępu", + "description": "Twoje urządzenie poprosi o użycie odcisku palca, rozpoznawania twarzy lub blokady ekranu.", + "info": { + "description": "Klucz dostępu to metoda uwierzytelniania na urządzeniu, wykorzystująca np. odcisk palca, Apple FaceID lub podobne rozwiązania.", + "link": "Uwierzytelnianie bez hasła" + }, + "skip": "Pomiń", + "submit": "Kontynuuj" + } + }, + "u2f": { + "verify": { + "title": "Zweryfikuj uwierzytelnianie dwuskładnikowe", + "description": "Zweryfikuj swoje konto za pomocą urządzenia." + }, + "set": { + "title": "Skonfiguruj uwierzytelnianie dwuskładnikowe", + "description": "Skonfiguruj urządzenie jako dodatkowy czynnik uwierzytelniania.", + "submit": "Kontynuuj" + } + }, + "register": { + "methods": { + "passkey": "Klucz dostępu", + "password": "Hasło" + }, + "disabled": { + "title": "Rejestracja wyłączona", + "description": "Rejestracja jest wyłączona. Skontaktuj się z administratorem." + }, + "missingdata": { + "title": "Brak danych", + "description": "Podaj e-mail, imię i nazwisko, aby się zarejestrować." + }, + "title": "Rejestracja", + "description": "Utwórz konto ZITADEL.", + "noMethodAvailableWarning": "Brak dostępnych metod uwierzytelniania. Skontaktuj się z administratorem.", + "selectMethod": "Wybierz metodę uwierzytelniania, której chcesz użyć", + "agreeTo": "Aby się zarejestrować, musisz zaakceptować warunki korzystania", + "termsOfService": "Regulamin", + "privacyPolicy": "Polityka prywatności", + "submit": "Kontynuuj", + "orUseIDP": "lub użyj dostawcy tożsamości", + "password": { + "title": "Ustaw hasło", + "description": "Ustaw hasło dla swojego konta", + "submit": "Kontynuuj", + "required": { + "password": "Musisz podać hasło!", + "confirmPassword": "To pole jest wymagane" + } + }, + "required": { + "firstname": "To pole jest wymagane", + "lastname": "To pole jest wymagane", + "email": "To pole jest wymagane" + } + }, + "invite": { + "title": "Zaproś użytkownika", + "description": "Podaj adres e-mail oraz imię i nazwisko użytkownika, którego chcesz zaprosić.", + "info": "Użytkownik otrzyma e-mail z dalszymi instrukcjami.", + "notAllowed": "Twoje ustawienia nie pozwalają na zapraszanie użytkowników.", + "submit": "Kontynuuj", + "success": { + "title": "Użytkownik zaproszony", + "description": "E-mail został pomyślnie wysłany.", + "verified": "Użytkownik został zaproszony i już zweryfikował swój e-mail.", + "notVerifiedYet": "Użytkownik został zaproszony. Otrzyma e-mail z dalszymi instrukcjami.", + "submit": "Zaproś kolejnego użytkownika" + } + }, + "signedin": { + "title": "Witaj {user}!", + "description": "Jesteś zalogowany.", + "continue": "Kontynuuj", + "error": { + "title": "Błąd", + "description": "Nie można załadować danych. Sprawdź połączenie z internetem lub spróbuj ponownie później." + } + }, + "verify": { + "userIdMissing": "Nie podano identyfikatora użytkownika!", + "successTitle": "Weryfikacja zakończona", + "successDescription": "Użytkownik został pomyślnie zweryfikowany.", + "setupAuthenticator": "Skonfiguruj uwierzytelnianie", + "verify": { + "title": "Zweryfikuj użytkownika", + "description": "Wprowadź kod z wiadomości weryfikacyjnej.", + "noCodeReceived": "Nie otrzymałeś kodu?", + "resendCode": "Wyślij kod ponownie", + "codeSent": "Kod został właśnie wysłany na twój adres e-mail.", + "submit": "Kontynuuj", + "required": { + "code": "To pole jest wymagane" + } + } + }, + "authenticator": { + "title": "Wybierz metodę uwierzytelniania", + "description": "Wybierz metodę, której chcesz użyć do uwierzytelnienia.", + "noMethodsAvailable": "Brak dostępnych metod uwierzytelniania", + "allSetup": "Już skonfigurowałeś metodę uwierzytelniania!", + "linkWithIDP": "lub połącz z dostawcą tożsamości" + }, + "device": { + "usercode": { + "title": "Kod urządzenia", + "description": "Wprowadź kod.", + "submit": "Kontynuuj", + "required": { + "code": "To pole jest wymagane" + } + }, + "request": { + "title": "{appName} chce się połączyć:", + "description": "{appName} będzie miało dostęp do:", + "disclaimer": "Klikając Zezwól, pozwalasz tej aplikacji i Zitadel na korzystanie z Twoich informacji zgodnie z ich odpowiednimi warunkami użytkowania i politykami prywatności. Możesz cofnąć ten dostęp w dowolnym momencie.", + "submit": "Zezwól", + "deny": "Odmów" + }, + "scope": { + "openid": "Zweryfikuj swoją tożsamość.", + "email": "Uzyskaj dostęp do swojego adresu e-mail.", + "profile": "Uzyskaj dostęp do pełnych informacji o swoim profilu.", + "offline_access": "Zezwól na dostęp offline do swojego konta." + } + }, + "error": { + "noUserCode": "Nie podano kodu użytkownika!", + "noDeviceRequest": "Nie znaleziono żądania urządzenia.", + "unknownContext": "Nie udało się pobrać kontekstu użytkownika. Upewnij się, że najpierw wprowadziłeś nazwę użytkownika lub podałeś login jako parametr wyszukiwania.", + "sessionExpired": "Twoja sesja wygasła. Zaloguj się ponownie.", + "failedLoading": "Nie udało się załadować danych. Spróbuj ponownie.", + "tryagain": "Spróbuj ponownie" + } +} diff --git a/apps/login/locales/ru.json b/apps/login/locales/ru.json new file mode 100644 index 0000000000..6ba2917e16 --- /dev/null +++ b/apps/login/locales/ru.json @@ -0,0 +1,292 @@ +{ + "common": { + "back": "Назад" + }, + "accounts": { + "title": "Аккаунты", + "description": "Выберите аккаунт, который хотите использовать.", + "addAnother": "Добавить другой аккаунт", + "noResults": "Аккаунты не найдены", + "verified": "проверенный", + "expired": "истёк" + }, + "logout": { + "title": "Выход", + "description": "Выберите аккаунт, который хотите удалить", + "noResults": "Аккаунты не найдены", + "clear": "Удалить сессию", + "verifiedAt": "Последняя активность: {time}", + "success": { + "title": "Выход выполнен успешно", + "description": "Вы успешно вышли из системы." + } + }, + "loginname": { + "title": "С возвращением!", + "description": "Введите свои данные для входа.", + "register": "Зарегистрировать нового пользователя", + "submit": "Продолжить", + "required": { + "loginName": "Это поле обязательно для заполнения" + } + }, + "password": { + "verify": { + "title": "Пароль", + "description": "Введите ваш пароль.", + "resetPassword": "Сбросить пароль", + "submit": "Продолжить", + "required": { + "password": "Это поле обязательно для заполнения" + } + }, + "set": { + "title": "Установить пароль", + "description": "Установите пароль для вашего аккаунта", + "codeSent": "Код отправлен на ваш адрес электронной почты.", + "noCodeReceived": "Не получили код?", + "resend": "Отправить код повторно", + "submit": "Продолжить", + "required": { + "code": "Это поле обязательно для заполнения", + "newPassword": "Вы должны указать пароль!", + "confirmPassword": "Это поле обязательно для заполнения" + } + }, + "change": { + "title": "Изменить пароль", + "description": "Установите пароль для вашего аккаунта", + "submit": "Продолжить", + "required": { + "newPassword": "Вы должны указать новый пароль!", + "confirmPassword": "Это поле обязательно для заполнения" + } + } + }, + "idp": { + "title": "Войти через SSO", + "description": "Выберите одного из провайдеров для входа", + "orSignInWith": "или войти через", + "signInWithApple": "Войти через Apple", + "signInWithGoogle": "Войти через Google", + "signInWithAzureAD": "Войти через AzureAD", + "signInWithGithub": "Войти через GitHub", + "signInWithGitlab": "Войти через GitLab", + "loginSuccess": { + "title": "Вход выполнен успешно", + "description": "Вы успешно вошли в систему!" + }, + "linkingSuccess": { + "title": "Аккаунт привязан", + "description": "Аккаунт успешно привязан!" + }, + "registerSuccess": { + "title": "Регистрация завершена", + "description": "Вы успешно зарегистрировались!" + }, + "loginError": { + "title": "Ошибка входа", + "description": "Произошла ошибка при попытке входа." + }, + "linkingError": { + "title": "Ошибка привязки аккаунта", + "description": "Произошла ошибка при попытке привязать аккаунт." + }, + "completeRegister": { + "title": "Завершите регистрацию", + "description": "Завершите регистрацию вашего аккаунта." + } + }, + "ldap": { + "title": "Войти через LDAP", + "description": "Введите ваши учетные данные LDAP.", + "username": "Имя пользователя", + "password": "Пароль", + "submit": "Продолжить", + "required": { + "username": "Это поле обязательно для заполнения", + "password": "Это поле обязательно для заполнения" + } + }, + "mfa": { + "verify": { + "title": "Подтвердите вашу личность", + "description": "Выберите один из следующих факторов.", + "noResults": "Нет доступных методов двухфакторной аутентификации" + }, + "set": { + "title": "Настройка двухфакторной аутентификации", + "description": "Выберите один из следующих методов.", + "skip": "Пропустить" + } + }, + "otp": { + "verify": { + "title": "Подтверждение 2FA", + "totpDescription": "Введите код из приложения-аутентификатора.", + "smsDescription": "Введите код, полученный по SMS.", + "emailDescription": "Введите код, полученный по email.", + "noCodeReceived": "Не получили код?", + "resendCode": "Отправить код повторно", + "submit": "Продолжить", + "required": { + "code": "Это поле обязательно для заполнения" + } + }, + "set": { + "title": "Настройка двухфакторной аутентификации", + "totpDescription": "Отсканируйте QR-код в приложении-аутентификаторе.", + "smsDescription": "Введите номер телефона для получения кода по SMS.", + "emailDescription": "Введите email для получения кода.", + "totpRegisterDescription": "Отсканируйте QR-код или перейдите по ссылке вручную.", + "submit": "Продолжить", + "required": { + "code": "Это поле обязательно для заполнения" + } + } + }, + "passkey": { + "verify": { + "title": "Аутентификация с помощью пасскей", + "description": "Устройство запросит отпечаток пальца, лицо или экранный замок", + "usePassword": "Использовать пароль", + "submit": "Продолжить" + }, + "set": { + "title": "Настройка пасскей", + "description": "Устройство запросит отпечаток пальца, лицо или экранный замок", + "info": { + "description": "Пасскей — метод аутентификации через устройство (отпечаток пальца, Apple FaceID и аналоги).", + "link": "Аутентификация без пароля" + }, + "skip": "Пропустить", + "submit": "Продолжить" + } + }, + "u2f": { + "verify": { + "title": "Подтверждение 2FA", + "description": "Подтвердите аккаунт с помощью устройства." + }, + "set": { + "title": "Настройка двухфакторной аутентификации", + "description": "Настройте устройство как второй фактор.", + "submit": "Продолжить" + } + }, + "register": { + "methods": { + "passkey": "Пасскей", + "password": "Пароль" + }, + "disabled": { + "title": "Регистрация отключена", + "description": "Регистрация недоступна. Обратитесь к администратору." + }, + "missingdata": { + "title": "Недостаточно данных", + "description": "Укажите email, имя и фамилию для регистрации." + }, + "title": "Регистрация", + "description": "Создайте свой аккаунт ZITADEL.", + "noMethodAvailableWarning": "Нет доступных методов аутентификации. Обратитесь к администратору.", + "selectMethod": "Выберите метод аутентификации", + "agreeTo": "Для регистрации необходимо принять условия:", + "termsOfService": "Условия использования", + "privacyPolicy": "Политика конфиденциальности", + "submit": "Продолжить", + "orUseIDP": "или используйте Identity Provider", + "password": { + "title": "Установить пароль", + "description": "Установите пароль для вашего аккаунта", + "submit": "Продолжить", + "required": { + "password": "Вы должны указать пароль!", + "confirmPassword": "Это поле обязательно для заполнения" + } + }, + "required": { + "firstname": "Это поле обязательно для заполнения", + "lastname": "Это поле обязательно для заполнения", + "email": "Это поле обязательно для заполнения" + } + }, + "invite": { + "title": "Пригласить пользователя", + "description": "Укажите email и имя пользователя для приглашения.", + "info": "Пользователь получит email с инструкциями.", + "notAllowed": "Ваши настройки не позволяют приглашать пользователей.", + "submit": "Продолжить", + "success": { + "title": "Пользователь приглашён", + "description": "Письмо успешно отправлено.", + "verified": "Пользователь приглашён и уже подтвердил email.", + "notVerifiedYet": "Пользователь приглашён. Он получит email с инструкциями.", + "submit": "Пригласить другого пользователя" + } + }, + "signedin": { + "title": "Добро пожаловать, {user}!", + "description": "Вы вошли в систему.", + "continue": "Продолжить", + "error": { + "title": "Ошибка", + "description": "Не удалось войти в систему. Проверьте свои данные и попробуйте снова." + } + }, + "verify": { + "userIdMissing": "Не указан userId!", + "successTitle": "Пользователь подтверждён", + "successDescription": "Пользователь успешно подтверждён.", + "setupAuthenticator": "Настроить аутентификатор", + "verify": { + "title": "Подтверждение пользователя", + "description": "Введите код из письма подтверждения.", + "noCodeReceived": "Не получили код?", + "resendCode": "Отправить код повторно", + "codeSent": "Код отправлен на ваш email.", + "submit": "Продолжить", + "required": { + "code": "Это поле обязательно для заполнения" + } + } + }, + "authenticator": { + "title": "Выбор метода аутентификации", + "description": "Выберите предпочитаемый метод аутентификации", + "noMethodsAvailable": "Нет доступных методов аутентификации", + "allSetup": "Аутентификатор уже настроен!", + "linkWithIDP": "или привязать через Identity Provider" + }, + "device": { + "usercode": { + "title": "Код устройства", + "description": "Введите код.", + "submit": "Продолжить", + "required": { + "code": "Это поле обязательно для заполнения" + } + }, + "request": { + "title": "{appName} хочет подключиться:", + "description": "{appName} получит доступ к:", + "disclaimer": "Нажимая «Разрешить», вы разрешаете этому приложению и Zitadel использовать вашу информацию в соответствии с их условиями использования и политиками конфиденциальности. Вы можете отозвать этот доступ в любое время.", + "submit": "Разрешить", + "deny": "Запретить" + }, + "scope": { + "openid": "Проверка вашей личности.", + "email": "Доступ к вашему адресу электронной почты.", + "profile": "Доступ к полной информации вашего профиля.", + "offline_access": "Разрешить офлайн-доступ к вашему аккаунту." + } + }, + "error": { + "noUserCode": "Не указан код пользователя!", + "noDeviceRequest": "Не найдена ни одна заявка на устройство.", + "unknownContext": "Не удалось получить контекст пользователя. Укажите имя пользователя или loginName в параметрах поиска.", + "sessionExpired": "Ваша сессия истекла. Войдите снова.", + "failedLoading": "Ошибка загрузки данных. Попробуйте ещё раз.", + "tryagain": "Попробовать снова" + } +} diff --git a/apps/login/locales/zh.json b/apps/login/locales/zh.json new file mode 100644 index 0000000000..fe5f2d1867 --- /dev/null +++ b/apps/login/locales/zh.json @@ -0,0 +1,292 @@ +{ + "common": { + "back": "返回" + }, + "accounts": { + "title": "账户", + "description": "选择您要使用的账户。", + "addAnother": "添加另一个账户", + "noResults": "未找到账户", + "verified": "已验证", + "expired": "已过期" + }, + "logout": { + "title": "注销", + "description": "选择您想注销的账户", + "noResults": "未找到账户", + "clear": "注销会话", + "verifiedAt": "最后活动时间:{time}", + "success": { + "title": "注销成功", + "description": "您已成功注销。" + } + }, + "loginname": { + "title": "欢迎回来!", + "description": "请输入您的登录信息。", + "register": "注册新用户", + "submit": "继续", + "required": { + "loginName": "此字段为必填项" + } + }, + "password": { + "verify": { + "title": "密码", + "description": "请输入您的密码。", + "resetPassword": "重置密码", + "submit": "继续", + "required": { + "password": "此字段为必填项" + } + }, + "set": { + "title": "设置密码", + "description": "为您的账户设置密码", + "codeSent": "验证码已发送到您的邮箱。", + "noCodeReceived": "没有收到验证码?", + "resend": "重发验证码", + "submit": "继续", + "required": { + "code": "此字段为必填项", + "newPassword": "必须提供密码!", + "confirmPassword": "此字段为必填项" + } + }, + "change": { + "title": "更改密码", + "description": "为您的账户设置密码", + "submit": "继续", + "required": { + "newPassword": "必须提供新密码!", + "confirmPassword": "此字段为必填项" + } + } + }, + "idp": { + "title": "使用 SSO 登录", + "description": "选择以下提供商中的一个进行登录", + "orSignInWith": "或使用以下方式登录", + "signInWithApple": "用 Apple 登录", + "signInWithGoogle": "用 Google 登录", + "signInWithAzureAD": "用 AzureAD 登录", + "signInWithGithub": "用 GitHub 登录", + "signInWithGitlab": "用 GitLab 登录", + "loginSuccess": { + "title": "登录成功", + "description": "您已成功登录!" + }, + "linkingSuccess": { + "title": "账户已链接", + "description": "您已成功链接您的账户!" + }, + "registerSuccess": { + "title": "注册成功", + "description": "您已成功注册!" + }, + "loginError": { + "title": "登录失败", + "description": "登录时发生错误。" + }, + "linkingError": { + "title": "账户链接失败", + "description": "链接账户时发生错误。" + }, + "completeRegister": { + "title": "完成注册", + "description": "完成您的账户注册。" + } + }, + "ldap": { + "title": "使用 LDAP 登录", + "description": "请输入您的 LDAP 凭据。", + "username": "用户名", + "password": "密码", + "submit": "继续", + "required": { + "username": "此字段为必填项", + "password": "此字段为必填项" + } + }, + "mfa": { + "verify": { + "title": "验证您的身份", + "description": "选择以下的一个因素。", + "noResults": "没有可设置的第二因素。" + }, + "set": { + "title": "设置双因素认证", + "description": "选择以下的一个第二因素。", + "skip": "跳过" + } + }, + "otp": { + "verify": { + "title": "验证双因素", + "totpDescription": "请输入认证应用程序中的验证码。", + "smsDescription": "输入通过短信收到的验证码。", + "emailDescription": "输入通过电子邮件收到的验证码。", + "noCodeReceived": "没有收到验证码?", + "resendCode": "重发验证码", + "submit": "继续", + "required": { + "code": "此字段为必填项" + } + }, + "set": { + "title": "设置双因素认证", + "totpDescription": "使用认证应用程序扫描二维码。", + "smsDescription": "输入您的电话号码以接收短信验证码。", + "emailDescription": "输入您的电子邮箱地址以接收电子邮件验证码。", + "totpRegisterDescription": "扫描二维码或手动导航到URL。", + "submit": "继续", + "required": { + "code": "此字段为必填项" + } + } + }, + "passkey": { + "verify": { + "title": "使用密钥认证", + "description": "您的设备将请求指纹、面部识别或屏幕锁", + "usePassword": "使用密码", + "submit": "继续" + }, + "set": { + "title": "设置密钥", + "description": "您的设备将请求指纹、面部识别或屏幕锁", + "info": { + "description": "密钥是在设备上如指纹、Apple FaceID 或类似的认证方法。", + "link": "无密码认证" + }, + "skip": "跳过", + "submit": "继续" + } + }, + "u2f": { + "verify": { + "title": "验证双因素", + "description": "使用您的设备验证帐户。" + }, + "set": { + "title": "设置双因素认证", + "description": "设置设备为第二因素。", + "submit": "继续" + } + }, + "register": { + "methods": { + "passkey": "密钥", + "password": "密码" + }, + "disabled": { + "title": "注册已禁用", + "description": "您的设置不允许注册新用户。" + }, + "missingdata": { + "title": "缺少数据", + "description": "请提供所有必需的数据。" + }, + "title": "注册", + "description": "创建您的 ZITADEL 账户。", + "noMethodAvailableWarning": "没有可用的认证方法。请联系您的系统管理员。", + "selectMethod": "选择您想使用的认证方法", + "agreeTo": "注册即表示您同意条款和条件", + "termsOfService": "服务条款", + "privacyPolicy": "隐私政策", + "submit": "继续", + "orUseIDP": "或使用身份提供者", + "password": { + "title": "设置密码", + "description": "为您的账户设置密码", + "submit": "继续", + "required": { + "password": "必须提供密码!", + "confirmPassword": "此字段为必填项" + } + }, + "required": { + "firstname": "此字段为必填项", + "lastname": "此字段为必填项", + "email": "此字段为必填项" + } + }, + "invite": { + "title": "邀请用户", + "description": "提供您想邀请的用户的电子邮箱地址和姓名。", + "info": "用户将收到一封包含进一步说明的电子邮件。", + "notAllowed": "您的设置不允许邀请用户。", + "submit": "继续", + "success": { + "title": "用户已邀请", + "description": "邮件已成功发送。", + "verified": "用户已被邀请并已验证其电子邮件。", + "notVerifiedYet": "用户已被邀请。他们将收到一封包含进一步说明的电子邮件。", + "submit": "邀请另一位用户" + } + }, + "signedin": { + "title": "欢迎 {user}!", + "description": "您已登录。", + "continue": "继续", + "error": { + "title": "错误", + "description": "登录时发生错误。" + } + }, + "verify": { + "userIdMissing": "未提供用户 ID!", + "successTitle": "用户已验证", + "successDescription": "用户已成功验证。", + "setupAuthenticator": "设置认证器", + "verify": { + "title": "验证用户", + "description": "输入验证邮件中的验证码。", + "noCodeReceived": "没有收到验证码?", + "resendCode": "重发验证码", + "codeSent": "刚刚发送了一封包含验证码的电子邮件。", + "submit": "继续", + "required": { + "code": "此字段为必填项" + } + } + }, + "authenticator": { + "title": "选择认证方式", + "description": "选择您想使用的认证方法", + "noMethodsAvailable": "没有可用的认证方法", + "allSetup": "您已经设置好了一个认证器!", + "linkWithIDP": "或将其与身份提供者关联" + }, + "device": { + "usercode": { + "title": "设备代码", + "description": "输入代码。", + "submit": "继续", + "required": { + "code": "此字段为必填项" + } + }, + "request": { + "title": "{appName} 想要连接:", + "description": "{appName} 将访问:", + "disclaimer": "点击“允许”即表示您允许此应用程序和 Zitadel 根据其各自的服务条款和隐私政策使用您的信息。您可以随时撤销此访问权限。", + "submit": "允许", + "deny": "拒绝" + }, + "scope": { + "openid": "验证您的身份。", + "email": "访问您的电子邮件地址。", + "profile": "访问您的完整个人资料信息。", + "offline_access": "允许离线访问您的账户。" + } + }, + "error": { + "noUserCode": "未提供用户代码!", + "noDeviceRequest": "没有找到设备请求。", + "unknownContext": "无法获取用户的上下文。请先输入用户名或提供 loginName 作为搜索参数。", + "sessionExpired": "当前会话已过期,请重新登录。", + "failedLoading": "加载数据失败,请再试一次。", + "tryagain": "重试" + } +} diff --git a/apps/login/next-env-vars.d.ts b/apps/login/next-env-vars.d.ts new file mode 100644 index 0000000000..b7a525858c --- /dev/null +++ b/apps/login/next-env-vars.d.ts @@ -0,0 +1,33 @@ +declare namespace NodeJS { + interface ProcessEnv { + // Allow any environment variable that matches the pattern + [key: `${string}_AUDIENCE`]: string; // The system api url + [key: `${string}_SYSTEM_USER_ID`]: string; // The service user id + [key: `${string}_SYSTEM_USER_PRIVATE_KEY`]: string; // The service user private key + + AUDIENCE: string; // The fallback system api url + SYSTEM_USER_ID: string; // The fallback service user id + SYSTEM_USER_PRIVATE_KEY: string; // The fallback service user private key + + /** + * The Zitadel API url + */ + ZITADEL_API_URL: string; + + /** + * The service user token + */ + ZITADEL_SERVICE_USER_TOKEN: string; + + /** + * Optional: wheter a user must have verified email + */ + EMAIL_VERIFICATION: string; + + /** + * Optional: custom request headers to be added to every request + * Split by comma, key value pairs separated by colon + */ + CUSTOM_REQUEST_HEADERS?: string; + } +} diff --git a/apps/login/next-env.d.ts b/apps/login/next-env.d.ts new file mode 100755 index 0000000000..1b3be0840f --- /dev/null +++ b/apps/login/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/apps/login/next.config.mjs b/apps/login/next.config.mjs new file mode 100755 index 0000000000..b84f11a230 --- /dev/null +++ b/apps/login/next.config.mjs @@ -0,0 +1,83 @@ +import createNextIntlPlugin from "next-intl/plugin"; +import { DEFAULT_CSP } from "./constants/csp.js"; + +const withNextIntl = createNextIntlPlugin(); + +/** @type {import('next').NextConfig} */ + +const secureHeaders = [ + { + key: "Strict-Transport-Security", + value: "max-age=63072000; includeSubDomains; preload", + }, + { + key: "Referrer-Policy", + value: "origin-when-cross-origin", + }, + { + key: "X-Frame-Options", + value: "SAMEORIGIN", + }, + { + key: "X-Content-Type-Options", + value: "nosniff", + }, + { + key: "X-XSS-Protection", + value: "1; mode=block", + }, + { + key: "Content-Security-Policy", + value: `${DEFAULT_CSP} frame-ancestors 'none'`, + }, + { key: "X-Frame-Options", value: "deny" }, +]; + +const imageRemotePatterns = [ + { + protocol: "http", + hostname: "localhost", + port: "8080", + pathname: "/**", + }, + { + protocol: "https", + hostname: "*.zitadel.*", + port: "", + pathname: "/**", + }, +]; + +if (process.env.ZITADEL_API_URL) { + imageRemotePatterns.push({ + protocol: "https", + hostname: process.env.ZITADEL_API_URL?.replace("https://", "") || "", + port: "", + pathname: "/**", + }); +} + +const nextConfig = { + basePath: process.env.NEXT_PUBLIC_BASE_PATH, + output: process.env.NEXT_OUTPUT_MODE || undefined, + reactStrictMode: true, // Recommended for the `pages` directory, default in `app`. + experimental: { + dynamicIO: true, + }, + images: { + remotePatterns: imageRemotePatterns, + }, + eslint: { + ignoreDuringBuilds: true, + }, + async headers() { + return [ + { + source: "/:path*", + headers: secureHeaders, + }, + ]; + }, +}; + +export default withNextIntl(nextConfig); diff --git a/apps/login/package.json b/apps/login/package.json new file mode 100644 index 0000000000..a4e02922ee --- /dev/null +++ b/apps/login/package.json @@ -0,0 +1,100 @@ +{ + "packageManager": "pnpm@9.1.2+sha256.19c17528f9ca20bd442e4ca42f00f1b9808a9cb419383cd04ba32ef19322aba7", + "name": "@zitadel/login", + "private": true, + "type": "module", + "scripts": { + "dev": "next dev", + "build": "next build", + "build:login:standalone": "NEXT_PUBLIC_BASE_PATH=/ui/v2/login NEXT_OUTPUT_MODE=standalone next build", + "start": "next start", + "lint": "pnpm run '/^lint:check:.*$/'", + "lint:check:next": "next lint", + "lint:check:prettier": "prettier --check .", + "lint:fix": "prettier --write .", + "test:unit": "vitest --run", + "lint-staged": "lint-staged", + "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf .next", + "test:integration:login": "cypress run", + "test:integration:login:debug": "cypress open", + "test:acceptance": "dotenv -e ../login/.env.test.local playwright", + "test:acceptance:setup": "cd ../.. && make login_test_acceptance_setup_env && NODE_ENV=test turbo run test:acceptance:setup:dev", + "test:acceptance:setup:dev": "cd ../.. && make login_test_acceptance_setup_dev" + }, + "git": { + "pre-commit": "lint-staged" + }, + "lint-staged": { + "*": "prettier --write --ignore-unknown" + }, + "dependencies": { + "@headlessui/react": "^2.1.9", + "@heroicons/react": "2.1.3", + "@radix-ui/react-tooltip": "^1.2.7", + "@tailwindcss/forms": "0.5.7", + "@vercel/analytics": "^1.2.2", + "@zitadel/client": "latest", + "@zitadel/proto": "latest", + "clsx": "1.2.1", + "copy-to-clipboard": "^3.3.3", + "deepmerge": "^4.3.1", + "lucide-react": "0.469.0", + "moment": "^2.29.4", + "next": "15.4.0-canary.86", + "next-intl": "^3.25.1", + "next-themes": "^0.2.1", + "nice-grpc": "2.0.1", + "qrcode.react": "^3.1.0", + "react": "19.1.0", + "react-dom": "19.1.0", + "react-hook-form": "7.39.5", + "tinycolor2": "1.4.2", + "uuid": "^11.1.0" + }, + "devDependencies": { + "@babel/eslint-parser": "^7.23.0", + "@bufbuild/buf": "^1.53.0", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.3.0", + "@types/ms": "2.1.0", + "@types/node": "^22.14.1", + "@types/react": "19.1.2", + "@types/react-dom": "19.1.2", + "@types/tinycolor2": "1.4.3", + "@types/uuid": "^10.0.0", + "@typescript-eslint/eslint-plugin": "^7.0.0", + "@typescript-eslint/parser": "^7.0.0", + "@vercel/git-hooks": "1.0.0", + "@vitejs/plugin-react": "^4.4.1", + "autoprefixer": "10.4.21", + "eslint": "^8.57.0", + "eslint-config-next": "15.4.0-canary.86", + "eslint-config-prettier": "^9.1.0", + "grpc-tools": "1.13.0", + "jsdom": "^26.1.0", + "lint-staged": "15.5.1", + "make-dir-cli": "4.0.0", + "postcss": "8.5.3", + "prettier": "^3.2.5", + "prettier-plugin-organize-imports": "^3.2.0", + "prettier-plugin-tailwindcss": "0.6.11", + "sass": "^1.87.0", + "tailwindcss": "3.4.14", + "ts-proto": "^2.7.0", + "typescript": "^5.8.3", + "vite-tsconfig-paths": "^5.1.4", + "vitest": "^2.0.0", + "concurrently": "^9.1.2", + "cypress": "^14.5.2", + "dotenv-cli": "^8.0.0", + "env-cmd": "^10.0.0", + "nodemon": "^3.1.9", + "start-server-and-test": "^2.0.11", + "@faker-js/faker": "^9.7.0", + "@otplib/core": "^12.0.0", + "@otplib/plugin-crypto": "^12.0.0", + "@otplib/plugin-thirty-two": "^12.0.0", + "@playwright/test": "^1.52.0", + "gaxios": "^7.1.0" + } +} diff --git a/apps/login/postcss.config.cjs b/apps/login/postcss.config.cjs new file mode 100644 index 0000000000..12a703d900 --- /dev/null +++ b/apps/login/postcss.config.cjs @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/apps/login/prettier.config.mjs b/apps/login/prettier.config.mjs new file mode 100644 index 0000000000..5e0f9b2584 --- /dev/null +++ b/apps/login/prettier.config.mjs @@ -0,0 +1,11 @@ +export default { + printWidth: 80, + tabWidth: 2, + useTabs: false, + semi: true, + singleQuote: false, + trailingComma: "all", + bracketSpacing: true, + arrowParens: "always", + plugins: ["prettier-plugin-organize-imports", "prettier-plugin-tailwindcss"], +}; diff --git a/apps/login/public/checkbox.svg b/apps/login/public/checkbox.svg new file mode 100644 index 0000000000..94a3298ae6 --- /dev/null +++ b/apps/login/public/checkbox.svg @@ -0,0 +1 @@ + diff --git a/apps/login/public/favicon.ico b/apps/login/public/favicon.ico new file mode 100644 index 0000000000..a901eddc34 Binary files /dev/null and b/apps/login/public/favicon.ico differ diff --git a/apps/login/public/favicon/android-chrome-192x192.png b/apps/login/public/favicon/android-chrome-192x192.png new file mode 100644 index 0000000000..f22bd442e6 Binary files /dev/null and b/apps/login/public/favicon/android-chrome-192x192.png differ diff --git a/apps/login/public/favicon/android-chrome-512x512.png b/apps/login/public/favicon/android-chrome-512x512.png new file mode 100644 index 0000000000..6987ed11b4 Binary files /dev/null and b/apps/login/public/favicon/android-chrome-512x512.png differ diff --git a/apps/login/public/favicon/apple-touch-icon.png b/apps/login/public/favicon/apple-touch-icon.png new file mode 100644 index 0000000000..4816102015 Binary files /dev/null and b/apps/login/public/favicon/apple-touch-icon.png differ diff --git a/apps/login/public/favicon/browserconfig.xml b/apps/login/public/favicon/browserconfig.xml new file mode 100644 index 0000000000..75efb24254 --- /dev/null +++ b/apps/login/public/favicon/browserconfig.xml @@ -0,0 +1,9 @@ + + + + + + #000000 + + + diff --git a/apps/login/public/favicon/favicon-16x16.png b/apps/login/public/favicon/favicon-16x16.png new file mode 100644 index 0000000000..4f45702ca9 Binary files /dev/null and b/apps/login/public/favicon/favicon-16x16.png differ diff --git a/apps/login/public/favicon/favicon-32x32.png b/apps/login/public/favicon/favicon-32x32.png new file mode 100644 index 0000000000..a598da05eb Binary files /dev/null and b/apps/login/public/favicon/favicon-32x32.png differ diff --git a/apps/login/public/favicon/favicon.ico b/apps/login/public/favicon/favicon.ico new file mode 100644 index 0000000000..af98450595 Binary files /dev/null and b/apps/login/public/favicon/favicon.ico differ diff --git a/apps/login/public/favicon/mstile-150x150.png b/apps/login/public/favicon/mstile-150x150.png new file mode 100644 index 0000000000..ab518480e6 Binary files /dev/null and b/apps/login/public/favicon/mstile-150x150.png differ diff --git a/apps/login/public/favicon/site.webmanifest b/apps/login/public/favicon/site.webmanifest new file mode 100644 index 0000000000..567c2c6549 --- /dev/null +++ b/apps/login/public/favicon/site.webmanifest @@ -0,0 +1,19 @@ +{ + "name": "ZITADEL Login", + "short_name": "ZITADEL Login", + "icons": [ + { + "src": "/favicon/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/favicon/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#000000", + "background_color": "#000000", + "display": "standalone" +} diff --git a/apps/login/public/grid-dark.svg b/apps/login/public/grid-dark.svg new file mode 100644 index 0000000000..d467ad6de0 --- /dev/null +++ b/apps/login/public/grid-dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/apps/login/public/grid-light.svg b/apps/login/public/grid-light.svg new file mode 100644 index 0000000000..114c1186fe --- /dev/null +++ b/apps/login/public/grid-light.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/apps/login/public/logo/zitadel-logo-solo-darkdesign.svg b/apps/login/public/logo/zitadel-logo-solo-darkdesign.svg new file mode 100644 index 0000000000..4a4e8be71b --- /dev/null +++ b/apps/login/public/logo/zitadel-logo-solo-darkdesign.svg @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/login/public/logo/zitadel-logo-solo-lightdesign.svg b/apps/login/public/logo/zitadel-logo-solo-lightdesign.svg new file mode 100644 index 0000000000..33ea6b583b --- /dev/null +++ b/apps/login/public/logo/zitadel-logo-solo-lightdesign.svg @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/login/public/zitadel-logo-dark.svg b/apps/login/public/zitadel-logo-dark.svg new file mode 100644 index 0000000000..6dcfe06e6d --- /dev/null +++ b/apps/login/public/zitadel-logo-dark.svg @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/login/public/zitadel-logo-light.svg b/apps/login/public/zitadel-logo-light.svg new file mode 100644 index 0000000000..d48a5eeb94 --- /dev/null +++ b/apps/login/public/zitadel-logo-light.svg @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/login/screenshots/accounts.png b/apps/login/screenshots/accounts.png new file mode 100644 index 0000000000..a8591141c6 Binary files /dev/null and b/apps/login/screenshots/accounts.png differ diff --git a/apps/login/screenshots/accounts_jumpto.png b/apps/login/screenshots/accounts_jumpto.png new file mode 100644 index 0000000000..0fd126bf4c Binary files /dev/null and b/apps/login/screenshots/accounts_jumpto.png differ diff --git a/apps/login/screenshots/collage.png b/apps/login/screenshots/collage.png new file mode 100644 index 0000000000..9d5a9c35c8 Binary files /dev/null and b/apps/login/screenshots/collage.png differ diff --git a/apps/login/screenshots/idp.png b/apps/login/screenshots/idp.png new file mode 100644 index 0000000000..9bf58c69b0 Binary files /dev/null and b/apps/login/screenshots/idp.png differ diff --git a/apps/login/screenshots/loginname.png b/apps/login/screenshots/loginname.png new file mode 100644 index 0000000000..342e60799e Binary files /dev/null and b/apps/login/screenshots/loginname.png differ diff --git a/apps/login/screenshots/mfa.png b/apps/login/screenshots/mfa.png new file mode 100644 index 0000000000..1fd73f205c Binary files /dev/null and b/apps/login/screenshots/mfa.png differ diff --git a/apps/login/screenshots/mfaset.png b/apps/login/screenshots/mfaset.png new file mode 100644 index 0000000000..c00ee4edf5 Binary files /dev/null and b/apps/login/screenshots/mfaset.png differ diff --git a/apps/login/screenshots/otp.png b/apps/login/screenshots/otp.png new file mode 100644 index 0000000000..3818a5ad5f Binary files /dev/null and b/apps/login/screenshots/otp.png differ diff --git a/apps/login/screenshots/otpset.png b/apps/login/screenshots/otpset.png new file mode 100644 index 0000000000..f75c2154c7 Binary files /dev/null and b/apps/login/screenshots/otpset.png differ diff --git a/apps/login/screenshots/passkey.png b/apps/login/screenshots/passkey.png new file mode 100644 index 0000000000..7a5686c736 Binary files /dev/null and b/apps/login/screenshots/passkey.png differ diff --git a/apps/login/screenshots/password.png b/apps/login/screenshots/password.png new file mode 100644 index 0000000000..05cf8747bb Binary files /dev/null and b/apps/login/screenshots/password.png differ diff --git a/apps/login/screenshots/password_change.png b/apps/login/screenshots/password_change.png new file mode 100644 index 0000000000..183de6df34 Binary files /dev/null and b/apps/login/screenshots/password_change.png differ diff --git a/apps/login/screenshots/password_set.png b/apps/login/screenshots/password_set.png new file mode 100644 index 0000000000..15b5ff49ad Binary files /dev/null and b/apps/login/screenshots/password_set.png differ diff --git a/apps/login/screenshots/register.png b/apps/login/screenshots/register.png new file mode 100644 index 0000000000..ba9f6951d8 Binary files /dev/null and b/apps/login/screenshots/register.png differ diff --git a/apps/login/screenshots/register_password.png b/apps/login/screenshots/register_password.png new file mode 100644 index 0000000000..31515bda9a Binary files /dev/null and b/apps/login/screenshots/register_password.png differ diff --git a/apps/login/screenshots/signedin.png b/apps/login/screenshots/signedin.png new file mode 100644 index 0000000000..f96ea1721f Binary files /dev/null and b/apps/login/screenshots/signedin.png differ diff --git a/apps/login/screenshots/u2f.png b/apps/login/screenshots/u2f.png new file mode 100644 index 0000000000..6b8eca087d Binary files /dev/null and b/apps/login/screenshots/u2f.png differ diff --git a/apps/login/screenshots/u2fset.png b/apps/login/screenshots/u2fset.png new file mode 100644 index 0000000000..37115548a5 Binary files /dev/null and b/apps/login/screenshots/u2fset.png differ diff --git a/apps/login/screenshots/verify.png b/apps/login/screenshots/verify.png new file mode 100644 index 0000000000..c13e6a3a88 Binary files /dev/null and b/apps/login/screenshots/verify.png differ diff --git a/apps/login/scripts/entrypoint.sh b/apps/login/scripts/entrypoint.sh new file mode 100755 index 0000000000..5bafc0012d --- /dev/null +++ b/apps/login/scripts/entrypoint.sh @@ -0,0 +1,11 @@ +#!/bin/sh +set -o allexport +. /.env-file/.env +set +o allexport + +if [ -n "${ZITADEL_SERVICE_USER_TOKEN_FILE}" ] && [ -f "${ZITADEL_SERVICE_USER_TOKEN_FILE}" ]; then + echo "ZITADEL_SERVICE_USER_TOKEN_FILE=${ZITADEL_SERVICE_USER_TOKEN_FILE} is set and file exists, setting ZITADEL_SERVICE_USER_TOKEN to the files content" + export ZITADEL_SERVICE_USER_TOKEN=$(cat "${ZITADEL_SERVICE_USER_TOKEN_FILE}") +fi + +exec node /runtime/apps/login/server.js diff --git a/apps/login/scripts/healthcheck.js b/apps/login/scripts/healthcheck.js new file mode 100644 index 0000000000..652524154a --- /dev/null +++ b/apps/login/scripts/healthcheck.js @@ -0,0 +1,14 @@ +const url = process.argv[2]; + +if (!url) { + console.error("❌ No URL provided as command line argument."); + process.exit(1); +} + +try { + const res = await fetch(url); + if (!res.ok) process.exit(1); + process.exit(0); +} catch (e) { + process.exit(1); +} diff --git a/apps/login/src/app/(login)/accounts/page.tsx b/apps/login/src/app/(login)/accounts/page.tsx new file mode 100644 index 0000000000..e4e6b387dc --- /dev/null +++ b/apps/login/src/app/(login)/accounts/page.tsx @@ -0,0 +1,96 @@ +import { DynamicTheme } from "@/components/dynamic-theme"; +import { SessionsList } from "@/components/sessions-list"; +import { Translated } from "@/components/translated"; +import { getAllSessionCookieIds } from "@/lib/cookies"; +import { getServiceUrlFromHeaders } from "@/lib/service-url"; +import { + getBrandingSettings, + getDefaultOrg, + listSessions, +} from "@/lib/zitadel"; +import { UserPlusIcon } from "@heroicons/react/24/outline"; +import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb"; +// import { getLocale } from "next-intl/server"; +import { headers } from "next/headers"; +import Link from "next/link"; + +async function loadSessions({ serviceUrl }: { serviceUrl: string }) { + const cookieIds = await getAllSessionCookieIds(); + + if (cookieIds && cookieIds.length) { + const response = await listSessions({ + serviceUrl, + ids: cookieIds.filter((id) => !!id) as string[], + }); + return response?.sessions ?? []; + } else { + console.info("No session cookie found."); + return []; + } +} + +export default async function Page(props: { + searchParams: Promise>; +}) { + const searchParams = await props.searchParams; + + const requestId = searchParams?.requestId; + const organization = searchParams?.organization; + + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + + let defaultOrganization; + if (!organization) { + const org: Organization | null = await getDefaultOrg({ + serviceUrl, + }); + if (org) { + defaultOrganization = org.id; + } + } + + let sessions = await loadSessions({ serviceUrl }); + + const branding = await getBrandingSettings({ + serviceUrl, + organization: organization ?? defaultOrganization, + }); + + const params = new URLSearchParams(); + + if (requestId) { + params.append("requestId", requestId); + } + + if (organization) { + params.append("organization", organization); + } + + return ( + +
+

+ +

+

+ +

+ +
+ + +
+
+ +
+ + + +
+ +
+
+
+ ); +} diff --git a/apps/login/src/app/(login)/authenticator/set/page.tsx b/apps/login/src/app/(login)/authenticator/set/page.tsx new file mode 100644 index 0000000000..e08367f589 --- /dev/null +++ b/apps/login/src/app/(login)/authenticator/set/page.tsx @@ -0,0 +1,217 @@ +import { Alert } from "@/components/alert"; +import { BackButton } from "@/components/back-button"; +import { ChooseAuthenticatorToSetup } from "@/components/choose-authenticator-to-setup"; +import { DynamicTheme } from "@/components/dynamic-theme"; +import { SignInWithIdp } from "@/components/sign-in-with-idp"; +import { Translated } from "@/components/translated"; +import { UserAvatar } from "@/components/user-avatar"; +import { getSessionCookieById } from "@/lib/cookies"; +import { getServiceUrlFromHeaders } from "@/lib/service-url"; +import { loadMostRecentSession } from "@/lib/session"; +import { checkUserVerification } from "@/lib/verify-helper"; +import { + getActiveIdentityProviders, + getBrandingSettings, + getLoginSettings, + getSession, + getUserByID, + listAuthenticationMethodTypes, +} from "@/lib/zitadel"; +import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; +// import { getLocale } from "next-intl/server"; +import { headers } from "next/headers"; +import { redirect } from "next/navigation"; + +export default async function Page(props: { + searchParams: Promise>; +}) { + const searchParams = await props.searchParams; + + const { loginName, requestId, organization, sessionId } = searchParams; + + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + + const sessionWithData = sessionId + ? await loadSessionById(sessionId, organization) + : await loadSessionByLoginname(loginName, organization); + + async function getAuthMethodsAndUser( + serviceUrl: string, + + session?: Session, + ) { + const userId = session?.factors?.user?.id; + + if (!userId) { + throw Error("Could not get user id from session"); + } + + return listAuthenticationMethodTypes({ + serviceUrl, + userId, + }).then((methods) => { + return getUserByID({ serviceUrl, userId }).then((user) => { + const humanUser = + user.user?.type.case === "human" ? user.user?.type.value : undefined; + + return { + factors: session?.factors, + authMethods: methods.authMethodTypes ?? [], + phoneVerified: humanUser?.phone?.isVerified ?? false, + emailVerified: humanUser?.email?.isVerified ?? false, + expirationDate: session?.expirationDate, + }; + }); + }); + } + + async function loadSessionByLoginname( + loginName?: string, + organization?: string, + ) { + return loadMostRecentSession({ + serviceUrl, + sessionParams: { + loginName, + organization, + }, + }).then((session) => { + return getAuthMethodsAndUser(serviceUrl, session); + }); + } + + async function loadSessionById(sessionId: string, organization?: string) { + const recent = await getSessionCookieById({ sessionId, organization }); + return getSession({ + serviceUrl, + sessionId: recent.id, + sessionToken: recent.token, + }).then((sessionResponse) => { + return getAuthMethodsAndUser(serviceUrl, sessionResponse.session); + }); + } + + if ( + !sessionWithData || + !sessionWithData.factors || + !sessionWithData.factors.user + ) { + return ( + + + + ); + } + + const branding = await getBrandingSettings({ + serviceUrl, + organization: sessionWithData.factors.user?.organizationId, + }); + + const loginSettings = await getLoginSettings({ + serviceUrl, + organization: sessionWithData.factors.user?.organizationId, + }); + + // check if user was verified recently + const isUserVerified = await checkUserVerification( + sessionWithData.factors.user?.id, + ); + + if (!isUserVerified) { + const params = new URLSearchParams({ + loginName: sessionWithData.factors.user.loginName as string, + invite: "true", + send: "true", // set this to true to request a new code immediately + }); + + if (requestId) { + params.append("requestId", requestId); + } + + if (organization || sessionWithData.factors.user.organizationId) { + params.append( + "organization", + organization ?? (sessionWithData.factors.user.organizationId as string), + ); + } + + redirect(`/verify?` + params); + } + + const identityProviders = await getActiveIdentityProviders({ + serviceUrl, + orgId: sessionWithData.factors?.user?.organizationId, + linking_allowed: true, + }).then((resp) => { + return resp.identityProviders; + }); + + const params = new URLSearchParams({ + initial: "true", // defines that a code is not required and is therefore not shown in the UI + }); + + if (sessionWithData.factors?.user?.loginName) { + params.set("loginName", sessionWithData.factors?.user?.loginName); + } + + if (sessionWithData.factors?.user?.organizationId) { + params.set("organization", sessionWithData.factors?.user?.organizationId); + } + + if (requestId) { + params.set("requestId", requestId); + } + + return ( + +
+

+ +

+ +

+ +

+ + + + {loginSettings && ( + + )} + + {loginSettings?.allowExternalIdp && !!identityProviders.length && ( + <> +
+

+ +

+
+ + + + )} + +
+ + +
+
+
+ ); +} diff --git a/apps/login/src/app/(login)/device/consent/page.tsx b/apps/login/src/app/(login)/device/consent/page.tsx new file mode 100644 index 0000000000..9f257bca8c --- /dev/null +++ b/apps/login/src/app/(login)/device/consent/page.tsx @@ -0,0 +1,99 @@ +import { ConsentScreen } from "@/components/consent"; +import { DynamicTheme } from "@/components/dynamic-theme"; +import { Translated } from "@/components/translated"; +import { getServiceUrlFromHeaders } from "@/lib/service-url"; +import { + getBrandingSettings, + getDefaultOrg, + getDeviceAuthorizationRequest, +} from "@/lib/zitadel"; +import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb"; +import { headers } from "next/headers"; + +export default async function Page(props: { + searchParams: Promise>; +}) { + const searchParams = await props.searchParams; + + const userCode = searchParams?.user_code; + const requestId = searchParams?.requestId; + const organization = searchParams?.organization; + + if (!userCode || !requestId) { + return ( +
+ +
+ ); + } + + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + + const { deviceAuthorizationRequest } = await getDeviceAuthorizationRequest({ + serviceUrl, + userCode, + }); + + if (!deviceAuthorizationRequest) { + return ( +
+ +
+ ); + } + + let defaultOrganization; + if (!organization) { + const org: Organization | null = await getDefaultOrg({ + serviceUrl, + }); + if (org) { + defaultOrganization = org.id; + } + } + + const branding = await getBrandingSettings({ + serviceUrl, + organization: organization ?? defaultOrganization, + }); + + const params = new URLSearchParams(); + + if (requestId) { + params.append("requestId", requestId); + } + + if (organization) { + params.append("organization", organization); + } + + return ( + +
+

+ +

+ +

+ +

+ + +
+
+ ); +} diff --git a/apps/login/src/app/(login)/device/page.tsx b/apps/login/src/app/(login)/device/page.tsx new file mode 100644 index 0000000000..e8761d25de --- /dev/null +++ b/apps/login/src/app/(login)/device/page.tsx @@ -0,0 +1,48 @@ +import { DeviceCodeForm } from "@/components/device-code-form"; +import { DynamicTheme } from "@/components/dynamic-theme"; +import { Translated } from "@/components/translated"; +import { getServiceUrlFromHeaders } from "@/lib/service-url"; +import { getBrandingSettings, getDefaultOrg } from "@/lib/zitadel"; +import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb"; +import { headers } from "next/headers"; + +export default async function Page(props: { + searchParams: Promise>; +}) { + const searchParams = await props.searchParams; + + const userCode = searchParams?.user_code; + const organization = searchParams?.organization; + + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + + let defaultOrganization; + if (!organization) { + const org: Organization | null = await getDefaultOrg({ + serviceUrl, + }); + if (org) { + defaultOrganization = org.id; + } + } + + const branding = await getBrandingSettings({ + serviceUrl, + organization: organization ?? defaultOrganization, + }); + + return ( + +
+

+ +

+

+ +

+ +
+
+ ); +} diff --git a/apps/login/src/app/(login)/error.tsx b/apps/login/src/app/(login)/error.tsx new file mode 100644 index 0000000000..d14150a4b4 --- /dev/null +++ b/apps/login/src/app/(login)/error.tsx @@ -0,0 +1,27 @@ +"use client"; + +import { Boundary } from "@/components/boundary"; +import { Button } from "@/components/button"; +import { Translated } from "@/components/translated"; +import { useEffect } from "react"; + +export default function Error({ error, reset }: any) { + useEffect(() => { + console.log("logging error:", error); + }, [error]); + + return ( + +
+
+ Error: {error?.message} +
+
+ +
+
+
+ ); +} diff --git a/apps/login/src/app/(login)/idp/[provider]/failure/page.tsx b/apps/login/src/app/(login)/idp/[provider]/failure/page.tsx new file mode 100644 index 0000000000..f2b7a19b91 --- /dev/null +++ b/apps/login/src/app/(login)/idp/[provider]/failure/page.tsx @@ -0,0 +1,105 @@ +import { Alert, AlertType } from "@/components/alert"; +import { ChooseAuthenticatorToLogin } from "@/components/choose-authenticator-to-login"; +import { DynamicTheme } from "@/components/dynamic-theme"; +import { Translated } from "@/components/translated"; +import { UserAvatar } from "@/components/user-avatar"; +import { getServiceUrlFromHeaders } from "@/lib/service-url"; +import { + getBrandingSettings, + getLoginSettings, + getUserByID, + listAuthenticationMethodTypes, +} from "@/lib/zitadel"; +import { HumanUser, User } from "@zitadel/proto/zitadel/user/v2/user_pb"; +import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; +import { headers } from "next/headers"; + +export default async function Page(props: { + searchParams: Promise>; + params: Promise<{ provider: string }>; +}) { + const searchParams = await props.searchParams; + + const { organization, userId } = searchParams; + + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + + const branding = await getBrandingSettings({ + serviceUrl, + organization, + }); + + const loginSettings = await getLoginSettings({ + serviceUrl, + organization, + }); + + let authMethods: AuthenticationMethodType[] = []; + let user: User | undefined = undefined; + let human: HumanUser | undefined = undefined; + + const params = new URLSearchParams({}); + if (organization) { + params.set("organization", organization); + } + if (userId) { + params.set("userId", userId); + } + + if (userId) { + const userResponse = await getUserByID({ + serviceUrl, + userId, + }); + if (userResponse) { + user = userResponse.user; + if (user?.type.case === "human") { + human = user.type.value as HumanUser; + } + + if (user?.preferredLoginName) { + params.set("loginName", user.preferredLoginName); + } + } + + const authMethodsResponse = await listAuthenticationMethodTypes({ + serviceUrl, + userId, + }); + if (authMethodsResponse.authMethodTypes) { + authMethods = authMethodsResponse.authMethodTypes; + } + } + + return ( + +
+

+ +

+ + + + + {userId && authMethods.length && ( + <> + {user && human && ( + + )} + + + + )} +
+
+ ); +} diff --git a/apps/login/src/app/(login)/idp/[provider]/success/page.tsx b/apps/login/src/app/(login)/idp/[provider]/success/page.tsx new file mode 100644 index 0000000000..ae9feff6b7 --- /dev/null +++ b/apps/login/src/app/(login)/idp/[provider]/success/page.tsx @@ -0,0 +1,340 @@ +import { DynamicTheme } from "@/components/dynamic-theme"; +import { IdpSignin } from "@/components/idp-signin"; +import { completeIDP } from "@/components/idps/pages/complete-idp"; +import { linkingFailed } from "@/components/idps/pages/linking-failed"; +import { linkingSuccess } from "@/components/idps/pages/linking-success"; +import { loginFailed } from "@/components/idps/pages/login-failed"; +import { loginSuccess } from "@/components/idps/pages/login-success"; +import { Translated } from "@/components/translated"; +import { getServiceUrlFromHeaders } from "@/lib/service-url"; +import { + addHuman, + addIDPLink, + getBrandingSettings, + getDefaultOrg, + getIDPByID, + getLoginSettings, + getOrgsByDomain, + listUsers, + retrieveIDPIntent, + updateHuman, +} from "@/lib/zitadel"; +import { ConnectError, create } from "@zitadel/client"; +import { AutoLinkingOption } from "@zitadel/proto/zitadel/idp/v2/idp_pb"; +import { OrganizationSchema } from "@zitadel/proto/zitadel/object/v2/object_pb"; +import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb"; +import { + AddHumanUserRequest, + AddHumanUserRequestSchema, + UpdateHumanUserRequestSchema, +} from "@zitadel/proto/zitadel/user/v2/user_service_pb"; +import { headers } from "next/headers"; + +const ORG_SUFFIX_REGEX = /(?<=@)(.+)/; + +async function resolveOrganizationForUser({ + organization, + addHumanUser, + serviceUrl, +}: { + organization?: string; + addHumanUser?: { username?: string }; + serviceUrl: string; +}): Promise { + if (organization) return organization; + + if (addHumanUser?.username && ORG_SUFFIX_REGEX.test(addHumanUser.username)) { + const matched = ORG_SUFFIX_REGEX.exec(addHumanUser.username); + const suffix = matched?.[1] ?? ""; + + const orgs = await getOrgsByDomain({ + serviceUrl, + domain: suffix, + }); + const orgToCheckForDiscovery = + orgs.result && orgs.result.length === 1 ? orgs.result[0].id : undefined; + + if (orgToCheckForDiscovery) { + const orgLoginSettings = await getLoginSettings({ + serviceUrl, + organization: orgToCheckForDiscovery, + }); + if (orgLoginSettings?.allowDomainDiscovery) { + return orgToCheckForDiscovery; + } + } + } + return undefined; +} + +export default async function Page(props: { + searchParams: Promise>; + params: Promise<{ provider: string }>; +}) { + const params = await props.params; + const searchParams = await props.searchParams; + let { id, token, requestId, organization, link } = searchParams; + const { provider } = params; + + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + + let branding = await getBrandingSettings({ + serviceUrl, + organization, + }); + + if (!organization) { + const org: Organization | null = await getDefaultOrg({ + serviceUrl, + }); + if (org) { + organization = org.id; + } + } + + if (!provider || !id || !token) { + return loginFailed(branding, "IDP context missing"); + } + + const intent = await retrieveIDPIntent({ + serviceUrl, + id, + token, + }); + + const { idpInformation, userId } = intent; + let { addHumanUser } = intent; + + if (!idpInformation) { + return loginFailed(branding, "IDP information missing"); + } + + const idp = await getIDPByID({ + serviceUrl, + id: idpInformation.idpId, + }); + + const options = idp?.config?.options; + + if (!idp) { + throw new Error("IDP not found"); + } + + // sign in user. If user should be linked continue + if (userId && !link) { + // if auto update is enabled, we will update the user with the new information + if (options?.isAutoUpdate && addHumanUser) { + try { + await updateHuman({ + serviceUrl, + request: create(UpdateHumanUserRequestSchema, { + userId: userId, + profile: addHumanUser.profile, + email: addHumanUser.email, + phone: addHumanUser.phone, + }), + }); + } catch (error: unknown) { + // Log the error and continue with the login process + console.warn("An error occurred while updating the user:", error); + } + } + + return loginSuccess( + userId, + { idpIntentId: id, idpIntentToken: token }, + requestId, + branding, + ); + } + + if (link) { + if (!options?.isLinkingAllowed) { + // linking was probably disallowed since the invitation was created + return linkingFailed(branding, "Linking is no longer allowed"); + } + + let idpLink; + try { + idpLink = await addIDPLink({ + serviceUrl, + idp: { + id: idpInformation.idpId, + userId: idpInformation.userId, + userName: idpInformation.userName, + }, + userId, + }); + } catch (error) { + console.error(error); + return linkingFailed(branding); + } + + if (!idpLink) { + return linkingFailed(branding); + } else { + return linkingSuccess( + userId, + { idpIntentId: id, idpIntentToken: token }, + requestId, + branding, + ); + } + } + + // search for potential user via username, then link + if (options?.autoLinking) { + let foundUser; + const email = addHumanUser?.email?.email; + + if (options.autoLinking === AutoLinkingOption.EMAIL && email) { + foundUser = await listUsers({ serviceUrl, email }).then((response) => { + return response.result ? response.result[0] : null; + }); + } else if (options.autoLinking === AutoLinkingOption.USERNAME) { + foundUser = await listUsers( + options.autoLinking === AutoLinkingOption.USERNAME + ? { serviceUrl, userName: idpInformation.userName } + : { serviceUrl, email }, + ).then((response) => { + return response.result ? response.result[0] : null; + }); + } else { + foundUser = await listUsers({ + serviceUrl, + userName: idpInformation.userName, + email, + }).then((response) => { + return response.result ? response.result[0] : null; + }); + } + + if (foundUser) { + let idpLink; + try { + idpLink = await addIDPLink({ + serviceUrl, + idp: { + id: idpInformation.idpId, + userId: idpInformation.userId, + userName: idpInformation.userName, + }, + userId: foundUser.userId, + }); + } catch (error) { + console.error(error); + return linkingFailed(branding); + } + + if (!idpLink) { + return linkingFailed(branding); + } else { + return linkingSuccess( + foundUser.userId, + { idpIntentId: id, idpIntentToken: token }, + requestId, + branding, + ); + } + } + } + + let newUser; + // automatic creation of a user is allowed and data is complete + if (options?.isAutoCreation && addHumanUser) { + const orgToRegisterOn = await resolveOrganizationForUser({ + organization, + addHumanUser, + serviceUrl, + }); + + let addHumanUserWithOrganization: AddHumanUserRequest; + if (orgToRegisterOn) { + const organizationSchema = create(OrganizationSchema, { + org: { case: "orgId", value: orgToRegisterOn }, + }); + + addHumanUserWithOrganization = create(AddHumanUserRequestSchema, { + ...addHumanUser, + organization: organizationSchema, + }); + } else { + addHumanUserWithOrganization = create( + AddHumanUserRequestSchema, + addHumanUser, + ); + } + + try { + newUser = await addHuman({ + serviceUrl, + request: addHumanUserWithOrganization, + }); + } catch (error: unknown) { + console.error( + "An error occurred while creating the user:", + error, + addHumanUser, + ); + return loginFailed( + branding, + (error as ConnectError).message + ? (error as ConnectError).message + : "Could not create user", + ); + } + } else if (options?.isCreationAllowed) { + // if no user was found, we will create a new user manually / redirect to the registration page + const orgToRegisterOn = await resolveOrganizationForUser({ + organization, + addHumanUser, + serviceUrl, + }); + + if (orgToRegisterOn) { + branding = await getBrandingSettings({ + serviceUrl, + organization: orgToRegisterOn, + }); + } + + if (!orgToRegisterOn) { + return loginFailed(branding, "No organization found for registration"); + } + + return completeIDP({ + branding, + idpIntent: { idpIntentId: id, idpIntentToken: token }, + addHumanUser, + organization: orgToRegisterOn, + requestId, + idpUserId: idpInformation?.userId, + idpId: idpInformation?.idpId, + idpUserName: idpInformation?.userName, + }); + } + + if (newUser) { + return ( + +
+

+ +

+

+ +

+ +
+
+ ); + } + + // return login failed if no linking or creation is allowed and no user was found + return loginFailed(branding, "No user found"); +} diff --git a/apps/login/src/app/(login)/idp/ldap/page.tsx b/apps/login/src/app/(login)/idp/ldap/page.tsx new file mode 100644 index 0000000000..372c814525 --- /dev/null +++ b/apps/login/src/app/(login)/idp/ldap/page.tsx @@ -0,0 +1,56 @@ +import { DynamicTheme } from "@/components/dynamic-theme"; +import { LDAPUsernamePasswordForm } from "@/components/ldap-username-password-form"; +import { Translated } from "@/components/translated"; +import { getServiceUrlFromHeaders } from "@/lib/service-url"; +import { getBrandingSettings, getDefaultOrg } from "@/lib/zitadel"; +import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb"; +import { headers } from "next/headers"; + +export default async function Page(props: { + searchParams: Promise>; + params: Promise<{ provider: string }>; +}) { + const searchParams = await props.searchParams; + const { idpId, organization, link } = searchParams; + + if (!idpId) { + throw new Error("No idpId provided in searchParams"); + } + + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + + let defaultOrganization; + if (!organization) { + const org: Organization | null = await getDefaultOrg({ + serviceUrl, + }); + if (org) { + defaultOrganization = org.id; + } + } + + const branding = await getBrandingSettings({ + serviceUrl, + organization: organization ?? defaultOrganization, + }); + + // return login failed if no linking or creation is allowed and no user was found + return ( + +
+

+ +

+

+ +

+ + +
+
+ ); +} diff --git a/apps/login/src/app/(login)/idp/page.tsx b/apps/login/src/app/(login)/idp/page.tsx new file mode 100644 index 0000000000..ab16e897e5 --- /dev/null +++ b/apps/login/src/app/(login)/idp/page.tsx @@ -0,0 +1,51 @@ +import { DynamicTheme } from "@/components/dynamic-theme"; +import { SignInWithIdp } from "@/components/sign-in-with-idp"; +import { Translated } from "@/components/translated"; +import { getServiceUrlFromHeaders } from "@/lib/service-url"; +import { getActiveIdentityProviders, getBrandingSettings } from "@/lib/zitadel"; +import { headers } from "next/headers"; + +export default async function Page(props: { + searchParams: Promise>; +}) { + const searchParams = await props.searchParams; + + const requestId = searchParams?.requestId; + const organization = searchParams?.organization; + + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + + const identityProviders = await getActiveIdentityProviders({ + serviceUrl, + orgId: organization, + }).then((resp) => { + return resp.identityProviders; + }); + + const branding = await getBrandingSettings({ + serviceUrl, + organization, + }); + + return ( + +
+

+ +

+

+ +

+ + {identityProviders && ( + + )} +
+
+ ); +} diff --git a/apps/login/src/app/(login)/layout.tsx b/apps/login/src/app/(login)/layout.tsx new file mode 100644 index 0000000000..dbce9804c9 --- /dev/null +++ b/apps/login/src/app/(login)/layout.tsx @@ -0,0 +1,65 @@ +import "@/styles/globals.scss"; + +import { LanguageProvider } from "@/components/language-provider"; +import { LanguageSwitcher } from "@/components/language-switcher"; +import { Skeleton } from "@/components/skeleton"; +import { Theme } from "@/components/theme"; +import { ThemeProvider } from "@/components/theme-provider"; +import * as Tooltip from "@radix-ui/react-tooltip"; +import { Analytics } from "@vercel/analytics/react"; +import { Lato } from "next/font/google"; +import { ReactNode, Suspense } from "react"; + +const lato = Lato({ + weight: ["400", "700", "900"], + subsets: ["latin"], +}); + +export default async function RootLayout({ + children, +}: { + children: ReactNode; +}) { + return ( + + + + + + +
+ +
+
+
+ +
+
+ + } + > + +
+
+ {children} +
+ + +
+
+
+
+
+
+
+ + + + ); +} diff --git a/apps/login/src/app/(login)/loginname/page.tsx b/apps/login/src/app/(login)/loginname/page.tsx new file mode 100644 index 0000000000..37b57ed1b9 --- /dev/null +++ b/apps/login/src/app/(login)/loginname/page.tsx @@ -0,0 +1,93 @@ +import { DynamicTheme } from "@/components/dynamic-theme"; +import { SignInWithIdp } from "@/components/sign-in-with-idp"; +import { Translated } from "@/components/translated"; +import { UsernameForm } from "@/components/username-form"; +import { getServiceUrlFromHeaders } from "@/lib/service-url"; +import { + getActiveIdentityProviders, + getBrandingSettings, + getDefaultOrg, + getLoginSettings, +} from "@/lib/zitadel"; +import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb"; +import { headers } from "next/headers"; + +export default async function Page(props: { + searchParams: Promise>; +}) { + const searchParams = await props.searchParams; + + const loginName = searchParams?.loginName; + const requestId = searchParams?.requestId; + const organization = searchParams?.organization; + const suffix = searchParams?.suffix; + const submit: boolean = searchParams?.submit === "true"; + + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + + let defaultOrganization; + if (!organization) { + const org: Organization | null = await getDefaultOrg({ + serviceUrl, + }); + if (org) { + defaultOrganization = org.id; + } + } + + const loginSettings = await getLoginSettings({ + serviceUrl, + organization: organization ?? defaultOrganization, + }); + + const contextLoginSettings = await getLoginSettings({ + serviceUrl, + organization, + }); + + const identityProviders = await getActiveIdentityProviders({ + serviceUrl, + orgId: organization ?? defaultOrganization, + }).then((resp) => { + return resp.identityProviders; + }); + + const branding = await getBrandingSettings({ + serviceUrl, + organization: organization ?? defaultOrganization, + }); + + return ( + +
+

+ +

+

+ +

+ + + + {identityProviders && loginSettings?.allowExternalIdp && ( +
+ +
+ )} +
+
+ ); +} diff --git a/apps/login/src/app/(login)/logout/page.tsx b/apps/login/src/app/(login)/logout/page.tsx new file mode 100644 index 0000000000..71371fc129 --- /dev/null +++ b/apps/login/src/app/(login)/logout/page.tsx @@ -0,0 +1,87 @@ +import { DynamicTheme } from "@/components/dynamic-theme"; +import { SessionsClearList } from "@/components/sessions-clear-list"; +import { Translated } from "@/components/translated"; +import { getAllSessionCookieIds } from "@/lib/cookies"; +import { getServiceUrlFromHeaders } from "@/lib/service-url"; +import { + getBrandingSettings, + getDefaultOrg, + listSessions, +} from "@/lib/zitadel"; +import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb"; +import { headers } from "next/headers"; + +async function loadSessions({ serviceUrl }: { serviceUrl: string }) { + const cookieIds = await getAllSessionCookieIds(); + + if (cookieIds && cookieIds.length) { + const response = await listSessions({ + serviceUrl, + ids: cookieIds.filter((id) => !!id) as string[], + }); + return response?.sessions ?? []; + } else { + console.info("No session cookie found."); + return []; + } +} + +export default async function Page(props: { + searchParams: Promise>; +}) { + const searchParams = await props.searchParams; + + const organization = searchParams?.organization; + const postLogoutRedirectUri = searchParams?.post_logout_redirect_uri; + const logoutHint = searchParams?.logout_hint; + // TODO implement with new translation service + // const UILocales = searchParams?.ui_locales; + + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + + let defaultOrganization; + if (!organization) { + const org: Organization | null = await getDefaultOrg({ + serviceUrl, + }); + if (org) { + defaultOrganization = org.id; + } + } + + let sessions = await loadSessions({ serviceUrl }); + + const branding = await getBrandingSettings({ + serviceUrl, + organization: organization ?? defaultOrganization, + }); + + const params = new URLSearchParams(); + + if (organization) { + params.append("organization", organization); + } + + return ( + +
+

+ +

+

+ +

+ +
+ +
+
+
+ ); +} diff --git a/apps/login/src/app/(login)/logout/success/page.tsx b/apps/login/src/app/(login)/logout/success/page.tsx new file mode 100644 index 0000000000..a13564664c --- /dev/null +++ b/apps/login/src/app/(login)/logout/success/page.tsx @@ -0,0 +1,32 @@ +import { DynamicTheme } from "@/components/dynamic-theme"; +import { Translated } from "@/components/translated"; +import { getServiceUrlFromHeaders } from "@/lib/service-url"; +import { getBrandingSettings } from "@/lib/zitadel"; +import { headers } from "next/headers"; + +export default async function Page(props: { searchParams: Promise }) { + const searchParams = await props.searchParams; + + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + + const { organization } = searchParams; + + const branding = await getBrandingSettings({ + serviceUrl, + organization, + }); + + return ( + +
+

+ +

+

+ +

+
+
+ ); +} diff --git a/apps/login/src/app/(login)/mfa/page.tsx b/apps/login/src/app/(login)/mfa/page.tsx new file mode 100644 index 0000000000..5543cdf66f --- /dev/null +++ b/apps/login/src/app/(login)/mfa/page.tsx @@ -0,0 +1,134 @@ +import { Alert } from "@/components/alert"; +import { BackButton } from "@/components/back-button"; +import { ChooseSecondFactor } from "@/components/choose-second-factor"; +import { DynamicTheme } from "@/components/dynamic-theme"; +import { Translated } from "@/components/translated"; +import { UserAvatar } from "@/components/user-avatar"; +import { getSessionCookieById } from "@/lib/cookies"; +import { getServiceUrlFromHeaders } from "@/lib/service-url"; +import { loadMostRecentSession } from "@/lib/session"; +import { + getBrandingSettings, + getSession, + listAuthenticationMethodTypes, +} from "@/lib/zitadel"; +import { headers } from "next/headers"; + +export default async function Page(props: { + searchParams: Promise>; +}) { + const searchParams = await props.searchParams; + + const { loginName, requestId, organization, sessionId } = searchParams; + + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + + const sessionFactors = sessionId + ? await loadSessionById(serviceUrl, sessionId, organization) + : await loadSessionByLoginname(serviceUrl, loginName, organization); + + async function loadSessionByLoginname( + serviceUrl: string, + loginName?: string, + organization?: string, + ) { + return loadMostRecentSession({ + serviceUrl, + sessionParams: { + loginName, + organization, + }, + }).then((session) => { + if (session && session.factors?.user?.id) { + return listAuthenticationMethodTypes({ + serviceUrl, + userId: session.factors.user.id, + }).then((methods) => { + return { + factors: session?.factors, + authMethods: methods.authMethodTypes ?? [], + }; + }); + } + }); + } + + async function loadSessionById( + host: string, + sessionId: string, + organization?: string, + ) { + const recent = await getSessionCookieById({ sessionId, organization }); + return getSession({ + serviceUrl, + sessionId: recent.id, + sessionToken: recent.token, + }).then((response) => { + if (response?.session && response.session.factors?.user?.id) { + return listAuthenticationMethodTypes({ + serviceUrl, + userId: response.session.factors.user.id, + }).then((methods) => { + return { + factors: response.session?.factors, + authMethods: methods.authMethodTypes ?? [], + }; + }); + } + }); + } + + const branding = await getBrandingSettings({ + serviceUrl, + organization, + }); + + return ( + +
+

+ +

+ +

+ +

+ + {sessionFactors && ( + + )} + + {!(loginName || sessionId) && ( + + + + )} + + {sessionFactors ? ( + + ) : ( + + + + )} + +
+ + +
+
+
+ ); +} diff --git a/apps/login/src/app/(login)/mfa/set/page.tsx b/apps/login/src/app/(login)/mfa/set/page.tsx new file mode 100644 index 0000000000..ebfa358d6d --- /dev/null +++ b/apps/login/src/app/(login)/mfa/set/page.tsx @@ -0,0 +1,174 @@ +import { Alert } from "@/components/alert"; +import { BackButton } from "@/components/back-button"; +import { ChooseSecondFactorToSetup } from "@/components/choose-second-factor-to-setup"; +import { DynamicTheme } from "@/components/dynamic-theme"; +import { Translated } from "@/components/translated"; +import { UserAvatar } from "@/components/user-avatar"; +import { getSessionCookieById } from "@/lib/cookies"; +import { getServiceUrlFromHeaders } from "@/lib/service-url"; +import { loadMostRecentSession } from "@/lib/session"; +import { + getBrandingSettings, + getLoginSettings, + getSession, + getUserByID, + listAuthenticationMethodTypes, +} from "@/lib/zitadel"; +import { Timestamp, timestampDate } from "@zitadel/client"; +import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; +import { headers } from "next/headers"; + +function isSessionValid(session: Partial): { + valid: boolean; + verifiedAt?: Timestamp; +} { + const validPassword = session?.factors?.password?.verifiedAt; + const validPasskey = session?.factors?.webAuthN?.verifiedAt; + const stillValid = session.expirationDate + ? timestampDate(session.expirationDate) > new Date() + : true; + + const verifiedAt = validPassword || validPasskey; + const valid = !!((validPassword || validPasskey) && stillValid); + + return { valid, verifiedAt }; +} + +export default async function Page(props: { + searchParams: Promise>; +}) { + const searchParams = await props.searchParams; + + const { loginName, checkAfter, force, requestId, organization, sessionId } = + searchParams; + + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + + const sessionWithData = sessionId + ? await loadSessionById(sessionId, organization) + : await loadSessionByLoginname(loginName, organization); + + async function getAuthMethodsAndUser(session?: Session) { + const userId = session?.factors?.user?.id; + + if (!userId) { + throw Error("Could not get user id from session"); + } + + return listAuthenticationMethodTypes({ + serviceUrl, + userId, + }).then((methods) => { + return getUserByID({ serviceUrl, userId }).then((user) => { + const humanUser = + user.user?.type.case === "human" ? user.user?.type.value : undefined; + + return { + id: session.id, + factors: session?.factors, + authMethods: methods.authMethodTypes ?? [], + phoneVerified: humanUser?.phone?.isVerified ?? false, + emailVerified: humanUser?.email?.isVerified ?? false, + expirationDate: session?.expirationDate, + }; + }); + }); + } + + async function loadSessionByLoginname( + loginName?: string, + organization?: string, + ) { + return loadMostRecentSession({ + serviceUrl, + sessionParams: { + loginName, + organization, + }, + }).then((session) => { + return getAuthMethodsAndUser(session); + }); + } + + async function loadSessionById(sessionId: string, organization?: string) { + const recent = await getSessionCookieById({ sessionId, organization }); + return getSession({ + serviceUrl, + sessionId: recent.id, + sessionToken: recent.token, + }).then((sessionResponse) => { + return getAuthMethodsAndUser(sessionResponse.session); + }); + } + + const branding = await getBrandingSettings({ + serviceUrl, + organization, + }); + const loginSettings = await getLoginSettings({ + serviceUrl, + organization: sessionWithData.factors?.user?.organizationId, + }); + + const { valid } = isSessionValid(sessionWithData); + + return ( + +
+

+ +

+ +

+ +

+ + {sessionWithData && ( + + )} + + {!(loginName || sessionId) && ( + + + + )} + + {!valid && ( + + + + )} + + {isSessionValid(sessionWithData).valid && + loginSettings && + sessionWithData && + sessionWithData.factors?.user?.id && ( + + )} + +
+ + +
+
+
+ ); +} diff --git a/apps/login/src/app/(login)/otp/[method]/page.tsx b/apps/login/src/app/(login)/otp/[method]/page.tsx new file mode 100644 index 0000000000..1b1356315a --- /dev/null +++ b/apps/login/src/app/(login)/otp/[method]/page.tsx @@ -0,0 +1,128 @@ +import { Alert } from "@/components/alert"; +import { DynamicTheme } from "@/components/dynamic-theme"; +import { LoginOTP } from "@/components/login-otp"; +import { Translated } from "@/components/translated"; +import { UserAvatar } from "@/components/user-avatar"; +import { getSessionCookieById } from "@/lib/cookies"; +import { getServiceUrlFromHeaders } from "@/lib/service-url"; +import { loadMostRecentSession } from "@/lib/session"; +import { + getBrandingSettings, + getLoginSettings, + getSession, +} from "@/lib/zitadel"; +import { headers } from "next/headers"; + +export default async function Page(props: { + searchParams: Promise>; + params: Promise>; +}) { + const params = await props.params; + const searchParams = await props.searchParams; + + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + const host = _headers.get("host"); + + if (!host || typeof host !== "string") { + throw new Error("No host found"); + } + + const { + loginName, // send from password page + requestId, + sessionId, + organization, + code, + } = searchParams; + + const { method } = params; + + const session = sessionId + ? await loadSessionById(sessionId, organization) + : await loadMostRecentSession({ + serviceUrl, + sessionParams: { loginName, organization }, + }); + + async function loadSessionById(sessionId: string, organization?: string) { + const recent = await getSessionCookieById({ sessionId, organization }); + return getSession({ + serviceUrl, + sessionId: recent.id, + sessionToken: recent.token, + }).then((response) => { + if (response?.session) { + return response.session; + } + }); + } + + // email links do not come with organization, thus we need to use the session's organization + const branding = await getBrandingSettings({ + serviceUrl, + organization: organization ?? session?.factors?.user?.organizationId, + }); + + const loginSettings = await getLoginSettings({ + serviceUrl, + organization: organization ?? session?.factors?.user?.organizationId, + }); + + return ( + +
+

+ +

+ {method === "time-based" && ( +

+ +

+ )} + {method === "sms" && ( +

+ +

+ )} + {method === "email" && ( +

+ +

+ )} + + {!session && ( +
+ + + +
+ )} + + {session && ( + + )} + + {method && session && ( + + )} +
+
+ ); +} diff --git a/apps/login/src/app/(login)/otp/[method]/set/page.tsx b/apps/login/src/app/(login)/otp/[method]/set/page.tsx new file mode 100644 index 0000000000..4624dc9eba --- /dev/null +++ b/apps/login/src/app/(login)/otp/[method]/set/page.tsx @@ -0,0 +1,205 @@ +import { Alert } from "@/components/alert"; +import { BackButton } from "@/components/back-button"; +import { Button, ButtonVariants } from "@/components/button"; +import { DynamicTheme } from "@/components/dynamic-theme"; +import { TotpRegister } from "@/components/totp-register"; +import { Translated } from "@/components/translated"; +import { UserAvatar } from "@/components/user-avatar"; +import { getServiceUrlFromHeaders } from "@/lib/service-url"; +import { loadMostRecentSession } from "@/lib/session"; +import { + addOTPEmail, + addOTPSMS, + getBrandingSettings, + getLoginSettings, + registerTOTP, +} from "@/lib/zitadel"; +import { RegisterTOTPResponse } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; +import { headers } from "next/headers"; +import Link from "next/link"; +import { redirect } from "next/navigation"; + +export default async function Page(props: { + searchParams: Promise>; + params: Promise>; +}) { + const params = await props.params; + const searchParams = await props.searchParams; + + const { loginName, organization, sessionId, requestId, checkAfter } = + searchParams; + const { method } = params; + + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + + const branding = await getBrandingSettings({ + serviceUrl, + organization, + }); + const loginSettings = await getLoginSettings({ + serviceUrl, + organization, + }); + + const session = await loadMostRecentSession({ + serviceUrl, + sessionParams: { + loginName, + organization, + }, + }); + + let totpResponse: RegisterTOTPResponse | undefined, error: Error | undefined; + if (session && session.factors?.user?.id) { + if (method === "time-based") { + await registerTOTP({ + serviceUrl, + userId: session.factors.user.id, + }) + .then((resp) => { + if (resp) { + totpResponse = resp; + } + }) + .catch((err) => { + error = err; + }); + } else if (method === "sms") { + await addOTPSMS({ + serviceUrl, + userId: session.factors.user.id, + }).catch((_error) => { + // TODO: Throw this error? + new Error("Could not add OTP via SMS"); + }); + } else if (method === "email") { + await addOTPEmail({ + serviceUrl, + userId: session.factors.user.id, + }).catch((_error) => { + // TODO: Throw this error? + new Error("Could not add OTP via Email"); + }); + } else { + throw new Error("Invalid method"); + } + } else { + throw new Error("No session found"); + } + + const paramsToContinue = new URLSearchParams({}); + let urlToContinue = "/accounts"; + + if (sessionId) { + paramsToContinue.append("sessionId", sessionId); + } + if (loginName) { + paramsToContinue.append("loginName", loginName); + } + if (organization) { + paramsToContinue.append("organization", organization); + } + + if (checkAfter) { + if (requestId) { + paramsToContinue.append("requestId", requestId); + } + urlToContinue = `/otp/${method}?` + paramsToContinue; + + // immediately check the OTP on the next page if sms or email was set up + if (["email", "sms"].includes(method)) { + return redirect(urlToContinue); + } + } else if (requestId && sessionId) { + if (requestId) { + paramsToContinue.append("authRequest", requestId); + } + urlToContinue = `/login?` + paramsToContinue; + } else if (loginName) { + if (requestId) { + paramsToContinue.append("requestId", requestId); + } + urlToContinue = `/signedin?` + paramsToContinue; + } + + return ( + +
+

+ +

+ {!session && ( +
+ + + +
+ )} + + {error && ( +
+ {error?.message} +
+ )} + + {session && ( + + )} + + {totpResponse && "uri" in totpResponse && "secret" in totpResponse ? ( + <> +

+ +

+
+ +
{" "} + + ) : ( + <> +

+ {method === "email" + ? "Code via email was successfully added." + : method === "sms" + ? "Code via SMS was successfully added." + : ""} +

+ +
+ + + + + + +
+ + )} +
+
+ ); +} diff --git a/apps/login/src/app/(login)/page.tsx b/apps/login/src/app/(login)/page.tsx new file mode 100644 index 0000000000..f1fce50f90 --- /dev/null +++ b/apps/login/src/app/(login)/page.tsx @@ -0,0 +1,8 @@ +import { redirect } from "next/navigation"; + +export default function Page() { + // automatically redirect to loginname + if (process.env.DEBUG !== "true") { + redirect("/loginname"); + } +} diff --git a/apps/login/src/app/(login)/passkey/page.tsx b/apps/login/src/app/(login)/passkey/page.tsx new file mode 100644 index 0000000000..bef71986f3 --- /dev/null +++ b/apps/login/src/app/(login)/passkey/page.tsx @@ -0,0 +1,89 @@ +import { Alert } from "@/components/alert"; +import { DynamicTheme } from "@/components/dynamic-theme"; +import { LoginPasskey } from "@/components/login-passkey"; +import { Translated } from "@/components/translated"; +import { UserAvatar } from "@/components/user-avatar"; +import { getSessionCookieById } from "@/lib/cookies"; +import { getServiceUrlFromHeaders } from "@/lib/service-url"; +import { loadMostRecentSession } from "@/lib/session"; +import { getBrandingSettings, getSession } from "@/lib/zitadel"; +import { headers } from "next/headers"; + +export default async function Page(props: { + searchParams: Promise>; +}) { + const searchParams = await props.searchParams; + + const { loginName, altPassword, requestId, organization, sessionId } = + searchParams; + + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + + const sessionFactors = sessionId + ? await loadSessionById(serviceUrl, sessionId, organization) + : await loadMostRecentSession({ + serviceUrl, + sessionParams: { loginName, organization }, + }); + + async function loadSessionById( + serviceUrl: string, + sessionId: string, + organization?: string, + ) { + const recent = await getSessionCookieById({ sessionId, organization }); + return getSession({ + serviceUrl, + sessionId: recent.id, + sessionToken: recent.token, + }).then((response) => { + if (response?.session) { + return response.session; + } + }); + } + + const branding = await getBrandingSettings({ + serviceUrl, + organization, + }); + + return ( + +
+

+ +

+ + {sessionFactors && ( + + )} +

+ +

+ + {!(loginName || sessionId) && ( + + + + )} + + {(loginName || sessionId) && ( + + )} +
+
+ ); +} diff --git a/apps/login/src/app/(login)/passkey/set/page.tsx b/apps/login/src/app/(login)/passkey/set/page.tsx new file mode 100644 index 0000000000..52c195e6cf --- /dev/null +++ b/apps/login/src/app/(login)/passkey/set/page.tsx @@ -0,0 +1,85 @@ +import { Alert, AlertType } from "@/components/alert"; +import { DynamicTheme } from "@/components/dynamic-theme"; +import { RegisterPasskey } from "@/components/register-passkey"; +import { Translated } from "@/components/translated"; +import { UserAvatar } from "@/components/user-avatar"; +import { getServiceUrlFromHeaders } from "@/lib/service-url"; +import { loadMostRecentSession } from "@/lib/session"; +import { getBrandingSettings } from "@/lib/zitadel"; +import { headers } from "next/headers"; + +export default async function Page(props: { + searchParams: Promise>; +}) { + const searchParams = await props.searchParams; + + const { loginName, prompt, organization, requestId } = searchParams; + + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + + const session = await loadMostRecentSession({ + serviceUrl, + sessionParams: { + loginName, + organization, + }, + }); + + const branding = await getBrandingSettings({ + serviceUrl, + organization, + }); + + return ( + +
+

+ +

+ + {session && ( + + )} +

+ +

+ + + + + + + + + + + {!session && ( +
+ + + +
+ )} + + {session?.id && ( + + )} +
+
+ ); +} diff --git a/apps/login/src/app/(login)/password/change/page.tsx b/apps/login/src/app/(login)/password/change/page.tsx new file mode 100644 index 0000000000..78ba88d282 --- /dev/null +++ b/apps/login/src/app/(login)/password/change/page.tsx @@ -0,0 +1,100 @@ +import { Alert } from "@/components/alert"; +import { ChangePasswordForm } from "@/components/change-password-form"; +import { DynamicTheme } from "@/components/dynamic-theme"; +import { Translated } from "@/components/translated"; +import { UserAvatar } from "@/components/user-avatar"; +import { getServiceUrlFromHeaders } from "@/lib/service-url"; +import { loadMostRecentSession } from "@/lib/session"; +import { + getBrandingSettings, + getLoginSettings, + getPasswordComplexitySettings, +} from "@/lib/zitadel"; +import { headers } from "next/headers"; + +export default async function Page(props: { + searchParams: Promise>; +}) { + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + + const searchParams = await props.searchParams; + + const { loginName, organization, requestId } = searchParams; + + // also allow no session to be found (ignoreUnkownUsername) + const sessionFactors = await loadMostRecentSession({ + serviceUrl, + sessionParams: { + loginName, + organization, + }, + }); + + const branding = await getBrandingSettings({ + serviceUrl, + organization, + }); + + const passwordComplexity = await getPasswordComplexitySettings({ + serviceUrl, + organization: sessionFactors?.factors?.user?.organizationId, + }); + + const loginSettings = await getLoginSettings({ + serviceUrl, + organization: sessionFactors?.factors?.user?.organizationId, + }); + + return ( + +
+

+ {sessionFactors?.factors?.user?.displayName ?? ( + + )} +

+

+ +

+ + {/* show error only if usernames should be shown to be unknown */} + {(!sessionFactors || !loginName) && + !loginSettings?.ignoreUnknownUsernames && ( +
+ + + +
+ )} + + {sessionFactors && ( + + )} + + {passwordComplexity && + loginName && + sessionFactors?.factors?.user?.id ? ( + + ) : ( +
+ + + +
+ )} +
+
+ ); +} diff --git a/apps/login/src/app/(login)/password/page.tsx b/apps/login/src/app/(login)/password/page.tsx new file mode 100644 index 0000000000..a9ab4091f9 --- /dev/null +++ b/apps/login/src/app/(login)/password/page.tsx @@ -0,0 +1,102 @@ +import { Alert } from "@/components/alert"; +import { DynamicTheme } from "@/components/dynamic-theme"; +import { PasswordForm } from "@/components/password-form"; +import { Translated } from "@/components/translated"; +import { UserAvatar } from "@/components/user-avatar"; +import { getServiceUrlFromHeaders } from "@/lib/service-url"; +import { loadMostRecentSession } from "@/lib/session"; +import { + getBrandingSettings, + getDefaultOrg, + getLoginSettings, +} from "@/lib/zitadel"; +import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb"; +import { headers } from "next/headers"; + +export default async function Page(props: { + searchParams: Promise>; +}) { + const searchParams = await props.searchParams; + let { loginName, organization, requestId } = searchParams; + + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + + let defaultOrganization; + if (!organization) { + const org: Organization | null = await getDefaultOrg({ + serviceUrl, + }); + + if (org) { + defaultOrganization = org.id; + } + } + + // also allow no session to be found (ignoreUnkownUsername) + let sessionFactors; + try { + sessionFactors = await loadMostRecentSession({ + serviceUrl, + sessionParams: { + loginName, + organization, + }, + }); + } catch (error) { + // ignore error to continue to show the password form + console.warn(error); + } + + const branding = await getBrandingSettings({ + serviceUrl, + organization: organization ?? defaultOrganization, + }); + const loginSettings = await getLoginSettings({ + serviceUrl, + organization: organization ?? defaultOrganization, + }); + + return ( + +
+

+ {sessionFactors?.factors?.user?.displayName ?? ( + + )} +

+

+ +

+ + {/* show error only if usernames should be shown to be unknown */} + {(!sessionFactors || !loginName) && + !loginSettings?.ignoreUnknownUsernames && ( +
+ + + +
+ )} + + {sessionFactors && ( + + )} + + {loginName && ( + + )} +
+
+ ); +} diff --git a/apps/login/src/app/(login)/password/set/page.tsx b/apps/login/src/app/(login)/password/set/page.tsx new file mode 100644 index 0000000000..c47305929a --- /dev/null +++ b/apps/login/src/app/(login)/password/set/page.tsx @@ -0,0 +1,135 @@ +import { Alert, AlertType } from "@/components/alert"; +import { DynamicTheme } from "@/components/dynamic-theme"; +import { SetPasswordForm } from "@/components/set-password-form"; +import { Translated } from "@/components/translated"; +import { UserAvatar } from "@/components/user-avatar"; +import { getServiceUrlFromHeaders } from "@/lib/service-url"; +import { loadMostRecentSession } from "@/lib/session"; +import { + getBrandingSettings, + getLoginSettings, + getPasswordComplexitySettings, + getUserByID, +} from "@/lib/zitadel"; +import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; +import { HumanUser, User } from "@zitadel/proto/zitadel/user/v2/user_pb"; +import { headers } from "next/headers"; + +export default async function Page(props: { + searchParams: Promise>; +}) { + const searchParams = await props.searchParams; + + const { userId, loginName, organization, requestId, code, initial } = + searchParams; + + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + + // also allow no session to be found (ignoreUnkownUsername) + let session: Session | undefined; + if (loginName) { + session = await loadMostRecentSession({ + serviceUrl, + sessionParams: { + loginName, + organization, + }, + }); + } + + const branding = await getBrandingSettings({ + serviceUrl, + organization, + }); + + const passwordComplexity = await getPasswordComplexitySettings({ + serviceUrl, + organization: session?.factors?.user?.organizationId, + }); + + const loginSettings = await getLoginSettings({ + serviceUrl, + organization, + }); + + let user: User | undefined; + let displayName: string | undefined; + if (userId) { + const userResponse = await getUserByID({ + serviceUrl, + userId, + }); + user = userResponse.user; + + if (user?.type.case === "human") { + displayName = (user.type.value as HumanUser).profile?.displayName; + } + } + + return ( + +
+

+ {session?.factors?.user?.displayName ?? ( + + )} +

+

+ +

+ + {/* show error only if usernames should be shown to be unknown */} + {loginName && !session && !loginSettings?.ignoreUnknownUsernames && ( +
+ + + +
+ )} + + {session ? ( + + ) : user ? ( + + ) : null} + + {!initial && ( + + + + )} + + {passwordComplexity && + (loginName ?? user?.preferredLoginName) && + (userId ?? session?.factors?.user?.id) ? ( + + ) : ( +
+ + + +
+ )} +
+
+ ); +} diff --git a/apps/login/src/app/(login)/register/page.tsx b/apps/login/src/app/(login)/register/page.tsx new file mode 100644 index 0000000000..221679ef07 --- /dev/null +++ b/apps/login/src/app/(login)/register/page.tsx @@ -0,0 +1,134 @@ +import { Alert } from "@/components/alert"; +import { DynamicTheme } from "@/components/dynamic-theme"; +import { RegisterForm } from "@/components/register-form"; +import { SignInWithIdp } from "@/components/sign-in-with-idp"; +import { Translated } from "@/components/translated"; +import { getServiceUrlFromHeaders } from "@/lib/service-url"; +import { + getActiveIdentityProviders, + getBrandingSettings, + getDefaultOrg, + getLegalAndSupportSettings, + getLoginSettings, + getPasswordComplexitySettings, +} from "@/lib/zitadel"; +import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb"; +import { PasskeysType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; +import { headers } from "next/headers"; + +export default async function Page(props: { + searchParams: Promise>; +}) { + const searchParams = await props.searchParams; + + let { firstname, lastname, email, organization, requestId } = searchParams; + + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + + if (!organization) { + const org: Organization | null = await getDefaultOrg({ + serviceUrl, + }); + if (org) { + organization = org.id; + } + } + + const legal = await getLegalAndSupportSettings({ + serviceUrl, + organization, + }); + const passwordComplexitySettings = await getPasswordComplexitySettings({ + serviceUrl, + organization, + }); + + const branding = await getBrandingSettings({ + serviceUrl, + organization, + }); + + const loginSettings = await getLoginSettings({ + serviceUrl, + organization, + }); + + const identityProviders = await getActiveIdentityProviders({ + serviceUrl, + orgId: organization, + }).then((resp) => { + return resp.identityProviders.filter((idp) => { + return idp.options?.isAutoCreation || idp.options?.isCreationAllowed; // check if IDP allows to create account automatically or manual creation is allowed + }); + }); + + if (!loginSettings?.allowRegister) { + return ( + +
+

+ +

+

+ +

+
+
+ ); + } + + return ( + +
+

+ +

+

+ +

+ + {!organization && ( + + + + )} + + {legal && + passwordComplexitySettings && + organization && + (loginSettings.allowUsernamePassword || + loginSettings.passkeysType == PasskeysType.ALLOWED) && ( + + )} + + {loginSettings?.allowExternalIdp && !!identityProviders.length && ( + <> +
+

+ +

+
+ + + + )} +
+
+ ); +} diff --git a/apps/login/src/app/(login)/register/password/page.tsx b/apps/login/src/app/(login)/register/password/page.tsx new file mode 100644 index 0000000000..e9689f0f5e --- /dev/null +++ b/apps/login/src/app/(login)/register/password/page.tsx @@ -0,0 +1,100 @@ +import { DynamicTheme } from "@/components/dynamic-theme"; +import { SetRegisterPasswordForm } from "@/components/set-register-password-form"; +import { Translated } from "@/components/translated"; +import { getServiceUrlFromHeaders } from "@/lib/service-url"; +import { + getBrandingSettings, + getDefaultOrg, + getLegalAndSupportSettings, + getLoginSettings, + getPasswordComplexitySettings, +} from "@/lib/zitadel"; +import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb"; +import { headers } from "next/headers"; + +export default async function Page(props: { + searchParams: Promise>; +}) { + const searchParams = await props.searchParams; + + let { firstname, lastname, email, organization, requestId } = searchParams; + + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + + if (!organization) { + const org: Organization | null = await getDefaultOrg({ + serviceUrl, + }); + if (org) { + organization = org.id; + } + } + + const missingData = !firstname || !lastname || !email || !organization; + + const legal = await getLegalAndSupportSettings({ + serviceUrl, + organization, + }); + const passwordComplexitySettings = await getPasswordComplexitySettings({ + serviceUrl, + organization, + }); + + const branding = await getBrandingSettings({ + serviceUrl, + organization, + }); + + const loginSettings = await getLoginSettings({ + serviceUrl, + organization, + }); + + return missingData ? ( + +
+

+ +

+

+ +

+
+
+ ) : loginSettings?.allowRegister && loginSettings.allowUsernamePassword ? ( + +
+

+ +

+

+ +

+ + {legal && passwordComplexitySettings && ( + + )} +
+
+ ) : ( + +
+

+ +

+

+ +

+
+
+ ); +} diff --git a/apps/login/src/app/(login)/saml-post/route.ts b/apps/login/src/app/(login)/saml-post/route.ts new file mode 100644 index 0000000000..a2061a18e2 --- /dev/null +++ b/apps/login/src/app/(login)/saml-post/route.ts @@ -0,0 +1,49 @@ +import { getSAMLFormCookie } from "@/lib/saml"; +import { NextRequest, NextResponse } from "next/server"; + +export async function GET(request: NextRequest) { + const searchParams = request.nextUrl.searchParams; + const url = searchParams.get("url"); + const id = searchParams.get("id"); + + if (!url) { + return new NextResponse("Missing url parameter", { status: 400 }); + } + + if (!id) { + return new NextResponse("Missing id parameter", { status: 400 }); + } + + const formData = await getSAMLFormCookie(id); + + const formDataParsed = formData ? JSON.parse(formData) : null; + + if (!formDataParsed) { + return new NextResponse("SAML form data not found", { status: 404 }); + } + + // Generate hidden input fields for all key-value pairs in formDataParsed + const hiddenInputs = Object.entries(formDataParsed) + .map( + ([key, value]) => + ``, + ) + .join("\n "); + + // Respond with an HTML form that auto-submits via POST + const html = ` + + +
+ ${hiddenInputs} + +
+ + + `; + return new NextResponse(html, { + headers: { "Content-Type": "text/html" }, + }); +} diff --git a/apps/login/src/app/(login)/signedin/page.tsx b/apps/login/src/app/(login)/signedin/page.tsx new file mode 100644 index 0000000000..5b2ed5fbf4 --- /dev/null +++ b/apps/login/src/app/(login)/signedin/page.tsx @@ -0,0 +1,141 @@ +import { Alert, AlertType } from "@/components/alert"; +import { Button, ButtonVariants } from "@/components/button"; +import { DynamicTheme } from "@/components/dynamic-theme"; +import { Translated } from "@/components/translated"; +import { UserAvatar } from "@/components/user-avatar"; +import { + getMostRecentCookieWithLoginname, + getSessionCookieById, +} from "@/lib/cookies"; +import { completeDeviceAuthorization } from "@/lib/server/device"; +import { getServiceUrlFromHeaders } from "@/lib/service-url"; +import { loadMostRecentSession } from "@/lib/session"; +import { + getBrandingSettings, + getLoginSettings, + getSession, +} from "@/lib/zitadel"; +import { headers } from "next/headers"; +import Link from "next/link"; + +async function loadSessionById( + serviceUrl: string, + sessionId: string, + organization?: string, +) { + const recent = await getSessionCookieById({ sessionId, organization }); + return getSession({ + serviceUrl, + sessionId: recent.id, + sessionToken: recent.token, + }).then((response) => { + if (response?.session) { + return response.session; + } + }); +} + +export default async function Page(props: { searchParams: Promise }) { + const searchParams = await props.searchParams; + + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + + const { loginName, requestId, organization, sessionId } = searchParams; + + const branding = await getBrandingSettings({ + serviceUrl, + organization, + }); + + // complete device authorization flow if device requestId is present + if (requestId && requestId.startsWith("device_")) { + const cookie = sessionId + ? await getSessionCookieById({ sessionId, organization }) + : await getMostRecentCookieWithLoginname({ + loginName: loginName, + organization: organization, + }); + + await completeDeviceAuthorization(requestId.replace("device_", ""), { + sessionId: cookie.id, + sessionToken: cookie.token, + }).catch((err) => { + return ( + +
+

+ +

+

+ +

+ {err.message} +
+
+ ); + }); + } + + const sessionFactors = sessionId + ? await loadSessionById(serviceUrl, sessionId, organization) + : await loadMostRecentSession({ + serviceUrl, + sessionParams: { loginName, organization }, + }); + + let loginSettings; + if (!requestId) { + loginSettings = await getLoginSettings({ + serviceUrl, + organization, + }); + } + + return ( + +
+

+ +

+

+ +

+ + + + {requestId && requestId.startsWith("device_") && ( + + You can now close this window and return to the device where you + started the authorization process to continue. + + )} + + {loginSettings?.defaultRedirectUri && ( +
+ + + + + +
+ )} +
+
+ ); +} diff --git a/apps/login/src/app/(login)/u2f/page.tsx b/apps/login/src/app/(login)/u2f/page.tsx new file mode 100644 index 0000000000..c54b45103f --- /dev/null +++ b/apps/login/src/app/(login)/u2f/page.tsx @@ -0,0 +1,94 @@ +import { Alert } from "@/components/alert"; +import { DynamicTheme } from "@/components/dynamic-theme"; +import { LoginPasskey } from "@/components/login-passkey"; +import { Translated } from "@/components/translated"; +import { UserAvatar } from "@/components/user-avatar"; +import { getSessionCookieById } from "@/lib/cookies"; +import { getServiceUrlFromHeaders } from "@/lib/service-url"; +import { loadMostRecentSession } from "@/lib/session"; +import { getBrandingSettings, getSession } from "@/lib/zitadel"; +import { headers } from "next/headers"; + +export default async function Page(props: { + searchParams: Promise>; +}) { + const searchParams = await props.searchParams; + + const { loginName, requestId, sessionId, organization } = searchParams; + + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + const host = _headers.get("host"); + + if (!host || typeof host !== "string") { + throw new Error("No host found"); + } + + const branding = await getBrandingSettings({ + serviceUrl, + organization, + }); + + const sessionFactors = sessionId + ? await loadSessionById(serviceUrl, sessionId, organization) + : await loadMostRecentSession({ + serviceUrl, + sessionParams: { loginName, organization }, + }); + + async function loadSessionById( + host: string, + sessionId: string, + organization?: string, + ) { + const recent = await getSessionCookieById({ sessionId, organization }); + return getSession({ + serviceUrl, + sessionId: recent.id, + sessionToken: recent.token, + }).then((response) => { + if (response?.session) { + return response.session; + } + }); + } + + return ( + +
+

+ +

+ + {sessionFactors && ( + + )} +

+ +

+ + {!(loginName || sessionId) && ( + + + + )} + + {(loginName || sessionId) && ( + + )} +
+
+ ); +} diff --git a/apps/login/src/app/(login)/u2f/set/page.tsx b/apps/login/src/app/(login)/u2f/set/page.tsx new file mode 100644 index 0000000000..79f64bf67d --- /dev/null +++ b/apps/login/src/app/(login)/u2f/set/page.tsx @@ -0,0 +1,74 @@ +import { Alert } from "@/components/alert"; +import { DynamicTheme } from "@/components/dynamic-theme"; +import { RegisterU2f } from "@/components/register-u2f"; +import { Translated } from "@/components/translated"; +import { UserAvatar } from "@/components/user-avatar"; +import { getServiceUrlFromHeaders } from "@/lib/service-url"; +import { loadMostRecentSession } from "@/lib/session"; +import { getBrandingSettings } from "@/lib/zitadel"; +import { headers } from "next/headers"; + +export default async function Page(props: { + searchParams: Promise>; +}) { + const searchParams = await props.searchParams; + + const { loginName, organization, requestId, checkAfter } = searchParams; + + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + + const sessionFactors = await loadMostRecentSession({ + serviceUrl, + sessionParams: { + loginName, + organization, + }, + }); + + const branding = await getBrandingSettings({ + serviceUrl, + organization, + }); + + return ( + +
+

+ +

+ + {sessionFactors && ( + + )} +

+ {" "} + +

+ + {!sessionFactors && ( +
+ + + +
+ )} + + {sessionFactors?.id && ( + + )} +
+
+ ); +} diff --git a/apps/login/src/app/(login)/verify/page.tsx b/apps/login/src/app/(login)/verify/page.tsx new file mode 100644 index 0000000000..7497698222 --- /dev/null +++ b/apps/login/src/app/(login)/verify/page.tsx @@ -0,0 +1,172 @@ +import { Alert, AlertType } from "@/components/alert"; +import { DynamicTheme } from "@/components/dynamic-theme"; +import { Translated } from "@/components/translated"; +import { UserAvatar } from "@/components/user-avatar"; +import { VerifyForm } from "@/components/verify-form"; +import { sendEmailCode, sendInviteEmailCode } from "@/lib/server/verify"; +import { getServiceUrlFromHeaders } from "@/lib/service-url"; +import { loadMostRecentSession } from "@/lib/session"; +import { getBrandingSettings, getUserByID } from "@/lib/zitadel"; +import { HumanUser, User } from "@zitadel/proto/zitadel/user/v2/user_pb"; +import { headers } from "next/headers"; + +export default async function Page(props: { searchParams: Promise }) { + const searchParams = await props.searchParams; + + const { userId, loginName, code, organization, requestId, invite, send } = + searchParams; + + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + + const branding = await getBrandingSettings({ + serviceUrl, + organization, + }); + + let sessionFactors; + let user: User | undefined; + let human: HumanUser | undefined; + let id: string | undefined; + + const doSend = send === "true"; + + const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? ""; + + async function sendEmail(userId: string) { + const host = _headers.get("host"); + + if (!host || typeof host !== "string") { + throw new Error("No host found"); + } + + if (invite === "true") { + await sendInviteEmailCode({ + userId, + urlTemplate: + `${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}&invite=true` + + (requestId ? `&requestId=${requestId}` : ""), + }).catch((error) => { + console.error("Could not send invitation email", error); + throw Error("Failed to send invitation email"); + }); + } else { + await sendEmailCode({ + userId, + urlTemplate: + `${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` + + (requestId ? `&requestId=${requestId}` : ""), + }).catch((error) => { + console.error("Could not send verification email", error); + throw Error("Failed to send verification email"); + }); + } + } + + if ("loginName" in searchParams) { + sessionFactors = await loadMostRecentSession({ + serviceUrl, + sessionParams: { + loginName, + organization, + }, + }); + + if (doSend && sessionFactors?.factors?.user?.id) { + await sendEmail(sessionFactors.factors.user.id); + } + } else if ("userId" in searchParams && userId) { + if (doSend) { + await sendEmail(userId); + } + + const userResponse = await getUserByID({ + serviceUrl, + userId, + }); + if (userResponse) { + user = userResponse.user; + if (user?.type.case === "human") { + human = user.type.value as HumanUser; + } + } + } + + id = userId ?? sessionFactors?.factors?.user?.id; + + if (!id) { + throw Error("Failed to get user id"); + } + + const params = new URLSearchParams({ + userId: userId, + initial: "true", // defines that a code is not required and is therefore not shown in the UI + }); + + if (loginName) { + params.set("loginName", loginName); + } + + if (organization) { + params.set("organization", organization); + } + + if (requestId) { + params.set("requestId", requestId); + } + + return ( + +
+

+ +

+

+ +

+ + {!id && ( +
+ + + +
+ )} + + {id && send && ( +
+ + + +
+ )} + + {sessionFactors ? ( + + ) : ( + user && ( + + ) + )} + + +
+
+ ); +} diff --git a/apps/login/src/app/(login)/verify/success/page.tsx b/apps/login/src/app/(login)/verify/success/page.tsx new file mode 100644 index 0000000000..5427ecd90e --- /dev/null +++ b/apps/login/src/app/(login)/verify/success/page.tsx @@ -0,0 +1,80 @@ +import { DynamicTheme } from "@/components/dynamic-theme"; +import { Translated } from "@/components/translated"; +import { UserAvatar } from "@/components/user-avatar"; +import { getServiceUrlFromHeaders } from "@/lib/service-url"; +import { loadMostRecentSession } from "@/lib/session"; +import { getBrandingSettings, getUserByID } from "@/lib/zitadel"; +import { HumanUser, User } from "@zitadel/proto/zitadel/user/v2/user_pb"; +import { headers } from "next/headers"; + +export default async function Page(props: { searchParams: Promise }) { + const searchParams = await props.searchParams; + + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + + const { loginName, organization, userId } = searchParams; + + const branding = await getBrandingSettings({ + serviceUrl, + organization, + }); + + const sessionFactors = await loadMostRecentSession({ + serviceUrl, + sessionParams: { loginName, organization }, + }).catch((error) => { + console.warn("Error loading session:", error); + }); + + const id = userId ?? sessionFactors?.factors?.user?.id; + + if (!id) { + throw Error("Failed to get user id"); + } + + const userResponse = await getUserByID({ + serviceUrl, + userId: id, + }); + + let user: User | undefined; + let human: HumanUser | undefined; + + if (userResponse) { + user = userResponse.user; + if (user?.type.case === "human") { + human = user.type.value as HumanUser; + } + } + + return ( + +
+

+ +

+

+ +

+ + {sessionFactors ? ( + + ) : ( + user && ( + + ) + )} +
+
+ ); +} diff --git a/apps/login/src/app/global-error.tsx b/apps/login/src/app/global-error.tsx new file mode 100644 index 0000000000..5111a65e8d --- /dev/null +++ b/apps/login/src/app/global-error.tsx @@ -0,0 +1,36 @@ +"use client"; + +import { Boundary } from "@/components/boundary"; +import { Button } from "@/components/button"; +import { ThemeWrapper } from "@/components/theme-wrapper"; +import { Translated } from "@/components/translated"; + +export default function GlobalError({ + error, + reset, +}: { + error: Error & { digest?: string }; + reset: () => void; +}) { + return ( + // global-error must include html and body tags + + + + +
+
+ Error: {error?.message} +
+
+ +
+
+
+
+ + + ); +} diff --git a/apps/login/src/app/healthy/route.ts b/apps/login/src/app/healthy/route.ts new file mode 100644 index 0000000000..da41c2cca8 --- /dev/null +++ b/apps/login/src/app/healthy/route.ts @@ -0,0 +1,5 @@ +import { NextResponse } from "next/server"; + +export async function GET() { + return NextResponse.json({}, { status: 200 }); +} diff --git a/apps/login/src/app/login/route.ts b/apps/login/src/app/login/route.ts new file mode 100644 index 0000000000..7b57e1a5e9 --- /dev/null +++ b/apps/login/src/app/login/route.ts @@ -0,0 +1,565 @@ +import { getAllSessions } from "@/lib/cookies"; +import { idpTypeToSlug } from "@/lib/idp"; +import { loginWithOIDCAndSession } from "@/lib/oidc"; +import { loginWithSAMLAndSession } from "@/lib/saml"; +import { sendLoginname, SendLoginnameCommand } from "@/lib/server/loginname"; +import { constructUrl, getServiceUrlFromHeaders } from "@/lib/service-url"; +import { findValidSession } from "@/lib/session"; +import { + createCallback, + createResponse, + getActiveIdentityProviders, + getAuthRequest, + getOrgsByDomain, + getSAMLRequest, + getSecuritySettings, + listSessions, + startIdentityProviderFlow, +} from "@/lib/zitadel"; +import { create } from "@zitadel/client"; +import { Prompt } from "@zitadel/proto/zitadel/oidc/v2/authorization_pb"; +import { + CreateCallbackRequestSchema, + SessionSchema, +} from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb"; +import { CreateResponseRequestSchema } from "@zitadel/proto/zitadel/saml/v2/saml_service_pb"; +import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; +import { IdentityProviderType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; +import { headers } from "next/headers"; +import { NextRequest, NextResponse } from "next/server"; +import { DEFAULT_CSP } from "../../../constants/csp"; + +export const dynamic = "force-dynamic"; +export const revalidate = false; +export const fetchCache = "default-no-store"; + +const gotoAccounts = ({ + request, + requestId, + organization, +}: { + request: NextRequest; + requestId: string; + organization?: string; +}): NextResponse => { + const accountsUrl = constructUrl(request, "/accounts"); + + if (requestId) { + accountsUrl.searchParams.set("requestId", requestId); + } + if (organization) { + accountsUrl.searchParams.set("organization", organization); + } + + return NextResponse.redirect(accountsUrl); +}; + +async function loadSessions({ + serviceUrl, + ids, +}: { + serviceUrl: string; + ids: string[]; +}): Promise { + const response = await listSessions({ + serviceUrl, + ids: ids.filter((id: string | undefined) => !!id), + }); + + return response?.sessions ?? []; +} + +const ORG_SCOPE_REGEX = /urn:zitadel:iam:org:id:([0-9]+)/; +const ORG_DOMAIN_SCOPE_REGEX = /urn:zitadel:iam:org:domain:primary:(.+)/; // TODO: check regex for all domain character options +const IDP_SCOPE_REGEX = /urn:zitadel:iam:org:idp:id:(.+)/; + +export async function GET(request: NextRequest) { + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + + const searchParams = request.nextUrl.searchParams; + + const oidcRequestId = searchParams.get("authRequest"); // oidc initiated request + const samlRequestId = searchParams.get("samlRequest"); // saml initiated request + + // internal request id which combines authRequest and samlRequest with the prefix oidc_ or saml_ + let requestId = + searchParams.get("requestId") ?? + (oidcRequestId + ? `oidc_${oidcRequestId}` + : samlRequestId + ? `saml_${samlRequestId}` + : undefined); + + const sessionId = searchParams.get("sessionId"); + + // TODO: find a better way to handle _rsc (react server components) requests and block them to avoid conflicts when creating oidc callback + const _rsc = searchParams.get("_rsc"); + if (_rsc) { + return NextResponse.json({ error: "No _rsc supported" }, { status: 500 }); + } + + const sessionCookies = await getAllSessions(); + const ids = sessionCookies.map((s) => s.id); + let sessions: Session[] = []; + if (ids && ids.length) { + sessions = await loadSessions({ serviceUrl, ids }); + } + + // complete flow if session and request id are provided + if (requestId && sessionId) { + if (requestId.startsWith("oidc_")) { + // this finishes the login process for OIDC + return loginWithOIDCAndSession({ + serviceUrl, + authRequest: requestId.replace("oidc_", ""), + sessionId, + sessions, + sessionCookies, + request, + }); + } else if (requestId.startsWith("saml_")) { + // this finishes the login process for SAML + return loginWithSAMLAndSession({ + serviceUrl, + samlRequest: requestId.replace("saml_", ""), + sessionId, + sessions, + sessionCookies, + request, + }); + } + } + + // continue with OIDC + if (requestId && requestId.startsWith("oidc_")) { + const { authRequest } = await getAuthRequest({ + serviceUrl, + authRequestId: requestId.replace("oidc_", ""), + }); + + let organization = ""; + let suffix = ""; + let idpId = ""; + + if (authRequest?.scope) { + const orgScope = authRequest.scope.find((s: string) => + ORG_SCOPE_REGEX.test(s), + ); + + const idpScope = authRequest.scope.find((s: string) => + IDP_SCOPE_REGEX.test(s), + ); + + if (orgScope) { + const matched = ORG_SCOPE_REGEX.exec(orgScope); + organization = matched?.[1] ?? ""; + } else { + const orgDomainScope = authRequest.scope.find((s: string) => + ORG_DOMAIN_SCOPE_REGEX.test(s), + ); + + if (orgDomainScope) { + const matched = ORG_DOMAIN_SCOPE_REGEX.exec(orgDomainScope); + const orgDomain = matched?.[1] ?? ""; + if (orgDomain) { + const orgs = await getOrgsByDomain({ + serviceUrl, + domain: orgDomain, + }); + if (orgs.result && orgs.result.length === 1) { + organization = orgs.result[0].id ?? ""; + suffix = orgDomain; + } + } + } + } + + if (idpScope) { + const matched = IDP_SCOPE_REGEX.exec(idpScope); + idpId = matched?.[1] ?? ""; + + const identityProviders = await getActiveIdentityProviders({ + serviceUrl, + orgId: organization ? organization : undefined, + }).then((resp) => { + return resp.identityProviders; + }); + + const idp = identityProviders.find((idp) => idp.id === idpId); + + if (idp) { + const origin = request.nextUrl.origin; + + const identityProviderType = identityProviders[0].type; + + if (identityProviderType === IdentityProviderType.LDAP) { + const ldapUrl = constructUrl(request, "/ldap"); + if (authRequest.id) { + ldapUrl.searchParams.set("requestId", `oidc_${authRequest.id}`); + } + if (organization) { + ldapUrl.searchParams.set("organization", organization); + } + + return NextResponse.redirect(ldapUrl); + } + + let provider = idpTypeToSlug(identityProviderType); + + const params = new URLSearchParams(); + + if (requestId) { + params.set("requestId", requestId); + } + + if (organization) { + params.set("organization", organization); + } + + let url: string | null = await startIdentityProviderFlow({ + serviceUrl, + idpId, + urls: { + successUrl: + `${origin}/idp/${provider}/success?` + + new URLSearchParams(params), + failureUrl: + `${origin}/idp/${provider}/failure?` + + new URLSearchParams(params), + }, + }); + + if (!url) { + return NextResponse.json( + { error: "Could not start IDP flow" }, + { status: 500 }, + ); + } + + if (url.startsWith("/")) { + // if the url is a relative path, construct the absolute url + url = constructUrl(request, url).toString(); + } + + return NextResponse.redirect(url); + } + } + } + + if (authRequest && authRequest.prompt.includes(Prompt.CREATE)) { + const registerUrl = constructUrl(request, "/register"); + if (authRequest.id) { + registerUrl.searchParams.set("requestId", `oidc_${authRequest.id}`); + } + if (organization) { + registerUrl.searchParams.set("organization", organization); + } + + return NextResponse.redirect(registerUrl); + } + + // use existing session and hydrate it for oidc + if (authRequest && sessions.length) { + // if some accounts are available for selection and select_account is set + if (authRequest.prompt.includes(Prompt.SELECT_ACCOUNT)) { + return gotoAccounts({ + request, + requestId: `oidc_${authRequest.id}`, + organization, + }); + } else if (authRequest.prompt.includes(Prompt.LOGIN)) { + /** + * The login prompt instructs the authentication server to prompt the user for re-authentication, regardless of whether the user is already authenticated + */ + + // if a hint is provided, skip loginname page and jump to the next page + if (authRequest.loginHint) { + try { + let command: SendLoginnameCommand = { + loginName: authRequest.loginHint, + requestId: authRequest.id, + }; + + if (organization) { + command = { ...command, organization }; + } + + const res = await sendLoginname(command); + + if (res && "redirect" in res && res?.redirect) { + const absoluteUrl = constructUrl(request, res.redirect); + return NextResponse.redirect(absoluteUrl.toString()); + } + } catch (error) { + console.error("Failed to execute sendLoginname:", error); + } + } + + const loginNameUrl = constructUrl(request, "/loginname"); + if (authRequest.id) { + loginNameUrl.searchParams.set("requestId", `oidc_${authRequest.id}`); + } + if (authRequest.loginHint) { + loginNameUrl.searchParams.set("loginName", authRequest.loginHint); + } + if (organization) { + loginNameUrl.searchParams.set("organization", organization); + } + if (suffix) { + loginNameUrl.searchParams.set("suffix", suffix); + } + return NextResponse.redirect(loginNameUrl); + } else if (authRequest.prompt.includes(Prompt.NONE)) { + /** + * With an OIDC none prompt, the authentication server must not display any authentication or consent user interface pages. + * This means that the user should not be prompted to enter their password again. + * Instead, the server attempts to silently authenticate the user using an existing session or other authentication mechanisms that do not require user interaction + **/ + const securitySettings = await getSecuritySettings({ + serviceUrl, + }); + + const selectedSession = await findValidSession({ + serviceUrl, + sessions, + authRequest, + }); + + const noSessionResponse = NextResponse.json( + { error: "No active session found" }, + { status: 400 }, + ); + + if (securitySettings?.embeddedIframe?.enabled) { + securitySettings.embeddedIframe.allowedOrigins; + noSessionResponse.headers.set( + "Content-Security-Policy", + `${DEFAULT_CSP} frame-ancestors ${securitySettings.embeddedIframe.allowedOrigins.join(" ")};`, + ); + noSessionResponse.headers.delete("X-Frame-Options"); + } + + if (!selectedSession || !selectedSession.id) { + return noSessionResponse; + } + + const cookie = sessionCookies.find( + (cookie) => cookie.id === selectedSession.id, + ); + + if (!cookie || !cookie.id || !cookie.token) { + return noSessionResponse; + } + + const session = { + sessionId: cookie.id, + sessionToken: cookie.token, + }; + + const { callbackUrl } = await createCallback({ + serviceUrl, + req: create(CreateCallbackRequestSchema, { + authRequestId: requestId.replace("oidc_", ""), + callbackKind: { + case: "session", + value: create(SessionSchema, session), + }, + }), + }); + + const callbackResponse = NextResponse.redirect(callbackUrl); + + if (securitySettings?.embeddedIframe?.enabled) { + securitySettings.embeddedIframe.allowedOrigins; + callbackResponse.headers.set( + "Content-Security-Policy", + `${DEFAULT_CSP} frame-ancestors ${securitySettings.embeddedIframe.allowedOrigins.join(" ")};`, + ); + callbackResponse.headers.delete("X-Frame-Options"); + } + + return callbackResponse; + } else { + // check for loginHint, userId hint and valid sessions + let selectedSession = await findValidSession({ + serviceUrl, + sessions, + authRequest, + }); + + if (!selectedSession || !selectedSession.id) { + return gotoAccounts({ + request, + requestId: `oidc_${authRequest.id}`, + organization, + }); + } + + const cookie = sessionCookies.find( + (cookie) => cookie.id === selectedSession.id, + ); + + if (!cookie || !cookie.id || !cookie.token) { + return gotoAccounts({ + request, + requestId: `oidc_${authRequest.id}`, + organization, + }); + } + + const session = { + sessionId: cookie.id, + sessionToken: cookie.token, + }; + + try { + const { callbackUrl } = await createCallback({ + serviceUrl, + req: create(CreateCallbackRequestSchema, { + authRequestId: requestId.replace("oidc_", ""), + callbackKind: { + case: "session", + value: create(SessionSchema, session), + }, + }), + }); + if (callbackUrl) { + return NextResponse.redirect(callbackUrl); + } else { + console.log( + "could not create callback, redirect user to choose other account", + ); + return gotoAccounts({ + request, + organization, + requestId: `oidc_${authRequest.id}`, + }); + } + } catch (error) { + console.error(error); + return gotoAccounts({ + request, + requestId: `oidc_${authRequest.id}`, + organization, + }); + } + } + } else { + const loginNameUrl = constructUrl(request, "/loginname"); + + loginNameUrl.searchParams.set("requestId", requestId); + if (authRequest?.loginHint) { + loginNameUrl.searchParams.set("loginName", authRequest.loginHint); + loginNameUrl.searchParams.set("submit", "true"); // autosubmit + } + + if (organization) { + loginNameUrl.searchParams.append("organization", organization); + // loginNameUrl.searchParams.set("organization", organization); + } + + return NextResponse.redirect(loginNameUrl); + } + } + // continue with SAML + else if (requestId && requestId.startsWith("saml_")) { + const { samlRequest } = await getSAMLRequest({ + serviceUrl, + samlRequestId: requestId.replace("saml_", ""), + }); + + if (!samlRequest) { + return NextResponse.json( + { error: "No samlRequest found" }, + { status: 400 }, + ); + } + + let selectedSession = await findValidSession({ + serviceUrl, + sessions, + samlRequest, + }); + + if (!selectedSession || !selectedSession.id) { + return gotoAccounts({ + request, + requestId: `saml_${samlRequest.id}`, + }); + } + + const cookie = sessionCookies.find( + (cookie) => cookie.id === selectedSession.id, + ); + + if (!cookie || !cookie.id || !cookie.token) { + return gotoAccounts({ + request, + requestId: `saml_${samlRequest.id}`, + // organization, + }); + } + + const session = { + sessionId: cookie.id, + sessionToken: cookie.token, + }; + + try { + const { url, binding } = await createResponse({ + serviceUrl, + req: create(CreateResponseRequestSchema, { + samlRequestId: requestId.replace("saml_", ""), + responseKind: { + case: "session", + value: session, + }, + }), + }); + if (url && binding.case === "redirect") { + return NextResponse.redirect(url); + } else if (url && binding.case === "post") { + // Create HTML form that auto-submits via POST and escape the SAML cookie + const html = ` + + +
+ + + +
+ + + `; + + return new NextResponse(html, { + headers: { "Content-Type": "text/html" }, + }); + } else { + console.log( + "could not create response, redirect user to choose other account", + ); + return gotoAccounts({ + request, + requestId: `saml_${samlRequest.id}`, + }); + } + } catch (error) { + console.error(error); + return gotoAccounts({ + request, + requestId: `saml_${samlRequest.id}`, + }); + } + } + // Device Authorization does not need to start here as it is handled on the /device endpoint + else { + return NextResponse.json( + { error: "No authRequest nor samlRequest provided" }, + { status: 500 }, + ); + } +} diff --git a/apps/login/src/app/security/route.ts b/apps/login/src/app/security/route.ts new file mode 100644 index 0000000000..4a2b6d4854 --- /dev/null +++ b/apps/login/src/app/security/route.ts @@ -0,0 +1,28 @@ +import { createServiceForHost } from "@/lib/service"; +import { getServiceUrlFromHeaders } from "@/lib/service-url"; +import { Client } from "@zitadel/client"; +import { SettingsService } from "@zitadel/proto/zitadel/settings/v2/settings_service_pb"; +import { headers } from "next/headers"; +import { NextResponse } from "next/server"; + +export async function GET() { + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + + const settingsService: Client = + await createServiceForHost(SettingsService, serviceUrl); + + const settings = await settingsService + .getSecuritySettings({}) + .then((resp) => (resp.settings ? resp.settings : undefined)); + + const response = NextResponse.json({ settings }, { status: 200 }); + + // Add Cache-Control header to cache the response for up to 1 hour + response.headers.set( + "Cache-Control", + "public, max-age=3600, stale-while-revalidate=86400", + ); + + return response; +} diff --git a/apps/login/src/components/address-bar.tsx b/apps/login/src/components/address-bar.tsx new file mode 100644 index 0000000000..67417649dc --- /dev/null +++ b/apps/login/src/components/address-bar.tsx @@ -0,0 +1,61 @@ +"use client"; + +import { usePathname } from "next/navigation"; +import { Fragment } from "react"; + +type Props = { + domain: string; +}; + +export function AddressBar({ domain }: Props) { + const pathname = usePathname(); + + return ( +
+
+ + + +
+
+
+ {domain} +
+ {pathname ? ( + <> + / + {pathname + .split("/") + .slice(1) + .filter((s) => !!s) + .map((segment) => { + return ( + + + + {segment} + + + + / + + ); + })} + + ) : null} +
+
+ ); +} diff --git a/apps/login/src/components/alert.tsx b/apps/login/src/components/alert.tsx new file mode 100644 index 0000000000..01f82627cd --- /dev/null +++ b/apps/login/src/components/alert.tsx @@ -0,0 +1,45 @@ +import { + ExclamationTriangleIcon, + InformationCircleIcon, +} from "@heroicons/react/24/outline"; +import { clsx } from "clsx"; +import { ReactNode } from "react"; + +type Props = { + children: ReactNode; + type?: AlertType; +}; + +export enum AlertType { + ALERT, + INFO, +} + +const yellow = + "border-yellow-600/40 dark:border-yellow-500/20 bg-yellow-200/30 text-yellow-600 dark:bg-yellow-700/20 dark:text-yellow-200"; +// const red = + "border-red-600/40 dark:border-red-500/20 bg-red-200/30 text-red-600 dark:bg-red-700/20 dark:text-red-200"; +const neutral = + "border-divider-light dark:border-divider-dark bg-black/5 text-gray-600 dark:bg-white/10 dark:text-gray-200"; + +export function Alert({ children, type = AlertType.ALERT }: Props) { + return ( +
+ {type === AlertType.ALERT && ( + + )} + {type === AlertType.INFO && ( + + )} + {children} +
+ ); +} diff --git a/apps/login/src/components/app-avatar.tsx b/apps/login/src/components/app-avatar.tsx new file mode 100644 index 0000000000..84879d08ac --- /dev/null +++ b/apps/login/src/components/app-avatar.tsx @@ -0,0 +1,48 @@ +import { ColorShade, getColorHash } from "@/helpers/colors"; +import { useTheme } from "next-themes"; +import Image from "next/image"; +import { getInitials } from "./avatar"; + +interface AvatarProps { + appName: string; + imageUrl?: string; + shadow?: boolean; +} + +export function AppAvatar({ appName, imageUrl, shadow }: AvatarProps) { + const { resolvedTheme } = useTheme(); + const credentials = getInitials(appName, appName); + + const color: ColorShade = getColorHash(appName); + + const avatarStyleDark = { + backgroundColor: color[900], + color: color[200], + }; + + const avatarStyleLight = { + backgroundColor: color[200], + color: color[900], + }; + + return ( +
+ {imageUrl ? ( + avatar + ) : ( + {credentials} + )} +
+ ); +} diff --git a/apps/login/src/components/auth-methods.tsx b/apps/login/src/components/auth-methods.tsx new file mode 100644 index 0000000000..82271c93a6 --- /dev/null +++ b/apps/login/src/components/auth-methods.tsx @@ -0,0 +1,234 @@ +import { CheckIcon } from "@heroicons/react/24/solid"; +import { clsx } from "clsx"; +import Link from "next/link"; +import { ReactNode } from "react"; +import { BadgeState, StateBadge } from "./state-badge"; + +const cardClasses = (alreadyAdded: boolean) => + clsx( + "relative bg-background-light-400 dark:bg-background-dark-400 group block space-y-1.5 rounded-md px-5 py-3 border border-divider-light dark:border-divider-dark transition-all ", + alreadyAdded + ? "opacity-50 cursor-default" + : "hover:shadow-lg hover:dark:bg-white/10", + ); + +const LinkWrapper = ({ + alreadyAdded, + children, + link, +}: { + alreadyAdded: boolean; + children: ReactNode; + link: string; +}) => { + return !alreadyAdded ? ( + + {children} + + ) : ( +
{children}
+ ); +}; + +export const TOTP = (alreadyAdded: boolean, link: string) => { + return ( + +
+ + timer-lock-outline + + {" "} + Authenticator App +
+ {alreadyAdded && ( + <> + + + )} +
+ ); +}; + +export const U2F = (alreadyAdded: boolean, link: string) => { + return ( + +
+ + + + Universal Second Factor +
+ {alreadyAdded && ( + <> + + + )} +
+ ); +}; + +export const EMAIL = (alreadyAdded: boolean, link: string) => { + return ( + +
+ + + + + Code via Email +
+ {alreadyAdded && ( + <> + + + )} +
+ ); +}; + +export const SMS = (alreadyAdded: boolean, link: string) => { + return ( + +
+ + + + Code via SMS +
+ {alreadyAdded && ( + <> + + + )} +
+ ); +}; + +export const PASSKEYS = (alreadyAdded: boolean, link: string) => { + return ( + +
+ + + + Passkeys +
+ {alreadyAdded && ( + <> + + + )} +
+ ); +}; + +export const PASSWORD = (alreadyAdded: boolean, link: string) => { + return ( + +
+ + form-textbox-password + + + Password +
+ {alreadyAdded && ( + <> + + + )} +
+ ); +}; + +function Setup() { + return ( +
+ + + +
+ ); +} diff --git a/apps/login/src/components/authentication-method-radio.tsx b/apps/login/src/components/authentication-method-radio.tsx new file mode 100644 index 0000000000..93a42ab5f4 --- /dev/null +++ b/apps/login/src/components/authentication-method-radio.tsx @@ -0,0 +1,102 @@ +"use client"; + +import { RadioGroup } from "@headlessui/react"; +import { Translated } from "./translated"; + +export enum AuthenticationMethod { + Passkey = "passkey", + Password = "password", +} + +export const methods = [ + AuthenticationMethod.Passkey, + AuthenticationMethod.Password, +]; + +export function AuthenticationMethodRadio({ + selected, + selectionChanged, +}: { + selected: any; + selectionChanged: (value: any) => void; +}) { + return ( +
+
+ + Server size +
+ {methods.map((method) => ( + + `${ + active + ? "ring-2 ring-primary-light-500 ring-opacity-60 dark:ring-white/20" + : "" + } ${ + checked + ? "bg-background-light-400 ring-2 ring-primary-light-500 dark:bg-background-dark-400 dark:ring-primary-dark-500" + : "bg-background-light-400 dark:bg-background-dark-400" + } boder-divider-light relative flex h-full flex-1 cursor-pointer rounded-lg border px-5 py-4 hover:shadow-lg focus:outline-none dark:border-divider-dark dark:hover:bg-white/10` + } + > + {({ checked }) => ( + <> +
+ {method === "passkey" && ( + + + + )} + {method === "password" && ( + + form-textbox-password + + + )} + + {method === AuthenticationMethod.Passkey && ( + + )} + {method === AuthenticationMethod.Password && ( + + )} + +
+ + )} +
+ ))} +
+
+
+
+ ); +} diff --git a/apps/login/src/components/avatar.tsx b/apps/login/src/components/avatar.tsx new file mode 100644 index 0000000000..306e201821 --- /dev/null +++ b/apps/login/src/components/avatar.tsx @@ -0,0 +1,97 @@ +"use client"; + +import { ColorShade, getColorHash } from "@/helpers/colors"; +import { useTheme } from "next-themes"; +import Image from "next/image"; + +interface AvatarProps { + name: string | null | undefined; + loginName: string; + imageUrl?: string; + size?: "small" | "base" | "large"; + shadow?: boolean; +} + +export function getInitials(name: string, loginName: string) { + let credentials = ""; + if (name) { + const split = name.split(" "); + if (split) { + const initials = + split[0].charAt(0) + (split[1] ? split[1].charAt(0) : ""); + credentials = initials; + } else { + credentials = name.charAt(0); + } + } else { + const username = loginName.split("@")[0]; + let separator = "_"; + if (username.includes("-")) { + separator = "-"; + } + if (username.includes(".")) { + separator = "."; + } + const split = username.split(separator); + const initials = split[0].charAt(0) + (split[1] ? split[1].charAt(0) : ""); + credentials = initials; + } + + return credentials; +} + +export function Avatar({ + size = "base", + name, + loginName, + imageUrl, + shadow, +}: AvatarProps) { + const { resolvedTheme } = useTheme(); + const credentials = getInitials(name ?? loginName, loginName); + + const color: ColorShade = getColorHash(loginName); + + const avatarStyleDark = { + backgroundColor: color[900], + color: color[200], + }; + + const avatarStyleLight = { + backgroundColor: color[200], + color: color[900], + }; + + return ( +
+ {imageUrl ? ( + avatar + ) : ( + + {credentials} + + )} +
+ ); +} diff --git a/apps/login/src/components/back-button.tsx b/apps/login/src/components/back-button.tsx new file mode 100644 index 0000000000..31d4a880ad --- /dev/null +++ b/apps/login/src/components/back-button.tsx @@ -0,0 +1,18 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { Button, ButtonVariants } from "./button"; +import { Translated } from "./translated"; + +export function BackButton() { + const router = useRouter(); + return ( + + ); +} diff --git a/apps/login/src/components/boundary.tsx b/apps/login/src/components/boundary.tsx new file mode 100644 index 0000000000..354d920960 --- /dev/null +++ b/apps/login/src/components/boundary.tsx @@ -0,0 +1,83 @@ +import { clsx } from "clsx"; +import { ReactNode } from "react"; + +const Label = ({ + children, + animateRerendering, + color, +}: { + children: ReactNode; + animateRerendering?: boolean; + color?: "default" | "pink" | "blue" | "violet" | "cyan" | "orange" | "red"; +}) => { + return ( +
+ {children} +
+ ); +}; +export const Boundary = ({ + children, + labels = ["children"], + size = "default", + color = "default", + animateRerendering = true, +}: { + children: ReactNode; + labels?: string[]; + size?: "small" | "default"; + color?: "default" | "pink" | "blue" | "violet" | "cyan" | "orange" | "red"; + animateRerendering?: boolean; +}) => { + return ( +
+
+ {labels.map((label) => { + return ( + + ); + })} +
+ + {children} +
+ ); +}; diff --git a/apps/login/src/components/button.tsx b/apps/login/src/components/button.tsx new file mode 100644 index 0000000000..59f7af39d1 --- /dev/null +++ b/apps/login/src/components/button.tsx @@ -0,0 +1,73 @@ +import { clsx } from "clsx"; +import { ButtonHTMLAttributes, DetailedHTMLProps, forwardRef } from "react"; + +export enum ButtonSizes { + Small = "Small", + Large = "Large", +} + +export enum ButtonVariants { + Primary = "Primary", + Secondary = "Secondary", + Destructive = "Destructive", +} + +export enum ButtonColors { + Neutral = "Neutral", + Primary = "Primary", + Warn = "Warn", +} + +export type ButtonProps = DetailedHTMLProps< + ButtonHTMLAttributes, + HTMLButtonElement +> & { + size?: ButtonSizes; + variant?: ButtonVariants; + color?: ButtonColors; +}; + +export const getButtonClasses = ( + size: ButtonSizes, + variant: ButtonVariants, + color: ButtonColors, +) => + clsx({ + "box-border font-normal leading-36px text-14px inline-flex items-center rounded-md focus:outline-none transition-colors transition-shadow duration-300": true, + "shadow hover:shadow-xl active:shadow-xl disabled:border-none disabled:bg-gray-300 disabled:text-gray-600 disabled:shadow-none disabled:cursor-not-allowed disabled:dark:bg-gray-800 disabled:dark:text-gray-900": + variant === ButtonVariants.Primary, + "bg-primary-light-500 dark:bg-primary-dark-500 hover:bg-primary-light-400 hover:dark:bg-primary-dark-400 text-primary-light-contrast-500 dark:text-primary-dark-contrast-500": + variant === ButtonVariants.Primary && color !== ButtonColors.Warn, + "bg-warn-light-500 dark:bg-warn-dark-500 hover:bg-warn-light-400 hover:dark:bg-warn-dark-400 text-white dark:text-white": + variant === ButtonVariants.Primary && color === ButtonColors.Warn, + "border border-button-light-border dark:border-button-dark-border text-gray-950 hover:bg-gray-500 hover:bg-opacity-20 hover:dark:bg-white hover:dark:bg-opacity-10 focus:bg-gray-500 focus:bg-opacity-20 focus:dark:bg-white focus:dark:bg-opacity-10 dark:text-white disabled:text-gray-600 disabled:hover:bg-transparent disabled:dark:hover:bg-transparent disabled:cursor-not-allowed disabled:dark:text-gray-900": + variant === ButtonVariants.Secondary, + "border border-button-light-border dark:border-button-dark-border text-warn-light-500 dark:text-warn-dark-500 hover:bg-warn-light-500 hover:bg-opacity-10 dark:hover:bg-warn-light-500 dark:hover:bg-opacity-10 focus:bg-warn-light-500 focus:bg-opacity-20 dark:focus:bg-warn-light-500 dark:focus:bg-opacity-20": + color === ButtonColors.Warn && variant !== ButtonVariants.Primary, + "px-16 py-2": size === ButtonSizes.Large, + "px-4 h-[36px]": size === ButtonSizes.Small, + }); + +// eslint-disable-next-line react/display-name +export const Button = forwardRef( + ( + { + children, + className = "", + variant = ButtonVariants.Primary, + size = ButtonSizes.Small, + color = ButtonColors.Primary, + ...props + }, + ref, + ) => ( + + ), +); diff --git a/apps/login/src/components/change-password-form.tsx b/apps/login/src/components/change-password-form.tsx new file mode 100644 index 0000000000..6124812e27 --- /dev/null +++ b/apps/login/src/components/change-password-form.tsx @@ -0,0 +1,214 @@ +"use client"; + +import { + lowerCaseValidator, + numberValidator, + symbolValidator, + upperCaseValidator, +} from "@/helpers/validators"; +import { + checkSessionAndSetPassword, + sendPassword, +} from "@/lib/server/password"; +import { create } from "@zitadel/client"; +import { ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; +import { PasswordComplexitySettings } from "@zitadel/proto/zitadel/settings/v2/password_settings_pb"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; +import { useTranslations } from "next-intl"; +import { FieldValues, useForm } from "react-hook-form"; +import { Alert } from "./alert"; +import { BackButton } from "./back-button"; +import { Button, ButtonVariants } from "./button"; +import { TextInput } from "./input"; +import { PasswordComplexity } from "./password-complexity"; +import { Spinner } from "./spinner"; +import { Translated } from "./translated"; + +type Inputs = + | { + password: string; + confirmPassword: string; + } + | FieldValues; + +type Props = { + passwordComplexitySettings: PasswordComplexitySettings; + sessionId: string; + loginName: string; + requestId?: string; + organization?: string; +}; + +export function ChangePasswordForm({ + passwordComplexitySettings, + sessionId, + loginName, + requestId, + organization, +}: Props) { + const router = useRouter(); + + const { register, handleSubmit, watch, formState } = useForm({ + mode: "onBlur", + defaultValues: { + password: "", + comfirmPassword: "", + }, + }); + + const t = useTranslations("password"); + + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + + async function submitChange(values: Inputs) { + setLoading(true); + + const changeResponse = checkSessionAndSetPassword({ + sessionId, + password: values.password, + }) + .catch(() => { + setError("Could not change password"); + return; + }) + .finally(() => { + setLoading(false); + }); + + if (changeResponse && "error" in changeResponse && changeResponse.error) { + setError( + typeof changeResponse.error === "string" + ? changeResponse.error + : "Unknown error", + ); + return; + } + + if (!changeResponse) { + setError("Could not change password"); + return; + } + + await new Promise((resolve) => setTimeout(resolve, 1000)); // wait for a second, to prevent eventual consistency issues + + const passwordResponse = await sendPassword({ + loginName, + organization, + checks: create(ChecksSchema, { + password: { password: values.password }, + }), + requestId, + }) + .catch(() => { + setError("Could not verify password"); + return; + }) + .finally(() => { + setLoading(false); + }); + + if ( + passwordResponse && + "error" in passwordResponse && + passwordResponse.error + ) { + setError(passwordResponse.error); + return; + } + + if ( + passwordResponse && + "redirect" in passwordResponse && + passwordResponse.redirect + ) { + return router.push(passwordResponse.redirect); + } + + return; + } + + const { errors } = formState; + + const watchPassword = watch("password", ""); + const watchConfirmPassword = watch("confirmPassword", ""); + + const hasMinLength = + passwordComplexitySettings && + watchPassword?.length >= passwordComplexitySettings.minLength; + const hasSymbol = symbolValidator(watchPassword); + const hasNumber = numberValidator(watchPassword); + const hasUppercase = upperCaseValidator(watchPassword); + const hasLowercase = lowerCaseValidator(watchPassword); + + const policyIsValid = + passwordComplexitySettings && + (passwordComplexitySettings.requiresLowercase ? hasLowercase : true) && + (passwordComplexitySettings.requiresNumber ? hasNumber : true) && + (passwordComplexitySettings.requiresUppercase ? hasUppercase : true) && + (passwordComplexitySettings.requiresSymbol ? hasSymbol : true) && + hasMinLength; + + return ( +
+
+
+ +
+
+ +
+
+ + {passwordComplexitySettings && ( + + )} + + {error && {error}} + +
+ + +
+ + ); +} diff --git a/apps/login/src/components/checkbox.tsx b/apps/login/src/components/checkbox.tsx new file mode 100644 index 0000000000..aa0c066f24 --- /dev/null +++ b/apps/login/src/components/checkbox.tsx @@ -0,0 +1,62 @@ +import classNames from "clsx"; +import { + DetailedHTMLProps, + forwardRef, + InputHTMLAttributes, + useEffect, + useState, +} from "react"; + +export type CheckboxProps = DetailedHTMLProps< + InputHTMLAttributes, + HTMLInputElement +> & { + checked: boolean; + disabled?: boolean; + onChangeVal?: (checked: boolean) => void; +}; + +export const Checkbox = forwardRef( + function Checkbox( + { + className = "", + checked = false, + disabled = false, + onChangeVal, + children, + ...props + }, + ref, + ) { + const [enabled, setEnabled] = useState(checked); + + useEffect(() => { + setEnabled(checked); + }, [checked]); + + return ( +
+
+
+ { + setEnabled(event.target?.checked); + onChangeVal && onChangeVal(event.target?.checked); + }} + disabled={disabled} + type="checkbox" + className={classNames( + "form-checkbox rounded border-gray-300 text-primary-light-500 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 focus:ring-offset-0 dark:text-primary-dark-500", + className, + )} + {...props} + /> +
+
+ {children} +
+ ); + }, +); diff --git a/apps/login/src/components/choose-authenticator-to-login.tsx b/apps/login/src/components/choose-authenticator-to-login.tsx new file mode 100644 index 0000000000..ccebf83e6d --- /dev/null +++ b/apps/login/src/components/choose-authenticator-to-login.tsx @@ -0,0 +1,38 @@ +import { + LoginSettings, + PasskeysType, +} from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; +import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; +import { PASSKEYS, PASSWORD } from "./auth-methods"; +import { Translated } from "./translated"; + +type Props = { + authMethods: AuthenticationMethodType[]; + params: URLSearchParams; + loginSettings: LoginSettings | undefined; +}; + +export function ChooseAuthenticatorToLogin({ + authMethods, + params, + loginSettings, +}: Props) { + return ( + <> + {authMethods.includes(AuthenticationMethodType.PASSWORD) && + loginSettings?.allowUsernamePassword && ( +
+ +
+ )} +
+ {authMethods.includes(AuthenticationMethodType.PASSWORD) && + loginSettings?.allowUsernamePassword && + PASSWORD(false, "/password?" + params)} + {authMethods.includes(AuthenticationMethodType.PASSKEY) && + loginSettings?.passkeysType == PasskeysType.ALLOWED && + PASSKEYS(false, "/passkey?" + params)} +
+ + ); +} diff --git a/apps/login/src/components/choose-authenticator-to-setup.tsx b/apps/login/src/components/choose-authenticator-to-setup.tsx new file mode 100644 index 0000000000..9eb744f3a5 --- /dev/null +++ b/apps/login/src/components/choose-authenticator-to-setup.tsx @@ -0,0 +1,51 @@ +import { + LoginSettings, + PasskeysType, +} from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; +import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; +import { Alert, AlertType } from "./alert"; +import { PASSKEYS, PASSWORD } from "./auth-methods"; +import { Translated } from "./translated"; + +type Props = { + authMethods: AuthenticationMethodType[]; + params: URLSearchParams; + loginSettings: LoginSettings; +}; + +export function ChooseAuthenticatorToSetup({ + authMethods, + params, + loginSettings, +}: Props) { + if (authMethods.length !== 0) { + return ( + + + + ); + } else { + return ( + <> + {loginSettings.passkeysType == PasskeysType.NOT_ALLOWED && + !loginSettings.allowUsernamePassword && ( + + + + )} + +
+ {!authMethods.includes(AuthenticationMethodType.PASSWORD) && + loginSettings.allowUsernamePassword && + PASSWORD(false, "/password/set?" + params)} + {!authMethods.includes(AuthenticationMethodType.PASSKEY) && + loginSettings.passkeysType == PasskeysType.ALLOWED && + PASSKEYS(false, "/passkey/set?" + params)} +
+ + ); + } +} diff --git a/apps/login/src/components/choose-second-factor-to-setup.tsx b/apps/login/src/components/choose-second-factor-to-setup.tsx new file mode 100644 index 0000000000..38599053ff --- /dev/null +++ b/apps/login/src/components/choose-second-factor-to-setup.tsx @@ -0,0 +1,119 @@ +"use client"; + +import { skipMFAAndContinueWithNextUrl } from "@/lib/server/session"; +import { + LoginSettings, + SecondFactorType, +} from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; +import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; +import { useRouter } from "next/navigation"; +import { EMAIL, SMS, TOTP, U2F } from "./auth-methods"; +import { Translated } from "./translated"; + +type Props = { + userId: string; + loginName?: string; + sessionId?: string; + requestId?: string; + organization?: string; + loginSettings: LoginSettings; + userMethods: AuthenticationMethodType[]; + checkAfter: boolean; + phoneVerified: boolean; + emailVerified: boolean; + force: boolean; +}; + +export function ChooseSecondFactorToSetup({ + userId, + loginName, + sessionId, + requestId, + organization, + loginSettings, + userMethods, + checkAfter, + phoneVerified, + emailVerified, + force, +}: Props) { + const router = useRouter(); + const params = new URLSearchParams({}); + + if (loginName) { + params.append("loginName", loginName); + } + if (sessionId) { + params.append("sessionId", sessionId); + } + if (requestId) { + params.append("requestId", requestId); + } + if (organization) { + params.append("organization", organization); + } + if (checkAfter) { + params.append("checkAfter", "true"); + } + + return ( + <> +
+ {loginSettings.secondFactors.map((factor) => { + switch (factor) { + case SecondFactorType.OTP: + return TOTP( + userMethods.includes(AuthenticationMethodType.TOTP), + "/otp/time-based/set?" + params, + ); + case SecondFactorType.U2F: + return U2F( + userMethods.includes(AuthenticationMethodType.U2F), + "/u2f/set?" + params, + ); + case SecondFactorType.OTP_EMAIL: + return ( + emailVerified && + EMAIL( + userMethods.includes(AuthenticationMethodType.OTP_EMAIL), + "/otp/email/set?" + params, + ) + ); + case SecondFactorType.OTP_SMS: + return ( + phoneVerified && + SMS( + userMethods.includes(AuthenticationMethodType.OTP_SMS), + "/otp/sms/set?" + params, + ) + ); + default: + return null; + } + })} +
+ {!force && ( + + )} + + ); +} diff --git a/apps/login/src/components/choose-second-factor.tsx b/apps/login/src/components/choose-second-factor.tsx new file mode 100644 index 0000000000..be4c67c4c9 --- /dev/null +++ b/apps/login/src/components/choose-second-factor.tsx @@ -0,0 +1,54 @@ +"use client"; + +import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; +import { EMAIL, SMS, TOTP, U2F } from "./auth-methods"; + +type Props = { + loginName?: string; + sessionId?: string; + requestId?: string; + organization?: string; + userMethods: AuthenticationMethodType[]; +}; + +export function ChooseSecondFactor({ + loginName, + sessionId, + requestId, + organization, + userMethods, +}: Props) { + const params = new URLSearchParams({}); + + if (loginName) { + params.append("loginName", loginName); + } + if (sessionId) { + params.append("sessionId", sessionId); + } + if (requestId) { + params.append("requestId", requestId); + } + if (organization) { + params.append("organization", organization); + } + + return ( +
+ {userMethods.map((method, i) => { + return ( +
+ {method === AuthenticationMethodType.TOTP && + TOTP(false, "/otp/time-based?" + params)} + {method === AuthenticationMethodType.U2F && + U2F(false, "/u2f?" + params)} + {method === AuthenticationMethodType.OTP_EMAIL && + EMAIL(false, "/otp/email?" + params)} + {method === AuthenticationMethodType.OTP_SMS && + SMS(false, "/otp/sms?" + params)} +
+ ); + })} +
+ ); +} diff --git a/apps/login/src/components/consent.tsx b/apps/login/src/components/consent.tsx new file mode 100644 index 0000000000..579438162b --- /dev/null +++ b/apps/login/src/components/consent.tsx @@ -0,0 +1,116 @@ +"use client"; + +import { completeDeviceAuthorization } from "@/lib/server/device"; +import { useTranslations } from "next-intl"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; +import { Alert } from "./alert"; +import { Button, ButtonVariants } from "./button"; +import { Spinner } from "./spinner"; +import { Translated } from "./translated"; + +export function ConsentScreen({ + scope, + nextUrl, + deviceAuthorizationRequestId, + appName, +}: { + scope?: string[]; + nextUrl: string; + deviceAuthorizationRequestId: string; + appName?: string; +}) { + const t = useTranslations(); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + const router = useRouter(); + + async function denyDeviceAuth() { + setLoading(true); + const response = await completeDeviceAuthorization( + deviceAuthorizationRequestId, + ) + .catch(() => { + setError("Could not register user"); + return; + }) + .finally(() => { + setLoading(false); + }); + + if (response) { + return router.push("/device"); + } + } + + const scopes = scope?.filter((s) => !!s); + + return ( +
+
    + {scopes?.length === 0 && ( + + + + )} + {scopes?.map((s) => { + const translationKey = `device.scope.${s}`; + const description = t(translationKey); + + // Check if the key itself is returned and provide a fallback + const resolvedDescription = + description === translationKey ? "" : description; + + return ( +
  • + {resolvedDescription} +
  • + ); + })} +
+ +

+ +

+ + {error && ( +
+ {error} +
+ )} + +
+ + + + + + +
+
+ ); +} diff --git a/apps/login/src/components/copy-to-clipboard.tsx b/apps/login/src/components/copy-to-clipboard.tsx new file mode 100644 index 0000000000..615af7884f --- /dev/null +++ b/apps/login/src/components/copy-to-clipboard.tsx @@ -0,0 +1,41 @@ +"use client"; + +import { + ClipboardDocumentCheckIcon, + ClipboardIcon, +} from "@heroicons/react/20/solid"; +import copy from "copy-to-clipboard"; +import { useEffect, useState } from "react"; + +type Props = { + value: string; +}; + +export function CopyToClipboard({ value }: Props) { + const [copied, setCopied] = useState(false); + + useEffect(() => { + if (copied) { + copy(value); + const to = setTimeout(setCopied, 1000, false); + return () => clearTimeout(to); + } + }, [copied]); + + return ( +
+ +
+ ); +} diff --git a/apps/login/src/components/default-tags.tsx b/apps/login/src/components/default-tags.tsx new file mode 100644 index 0000000000..dc14f1bc1e --- /dev/null +++ b/apps/login/src/components/default-tags.tsx @@ -0,0 +1,32 @@ +// Default tags we want shared across the app +export function DefaultTags() { + return ( + <> + + + + + + {/* */} + + + ); +} diff --git a/apps/login/src/components/device-code-form.tsx b/apps/login/src/components/device-code-form.tsx new file mode 100644 index 0000000000..9c5a28ca4f --- /dev/null +++ b/apps/login/src/components/device-code-form.tsx @@ -0,0 +1,98 @@ +"use client"; + +import { Alert } from "@/components/alert"; +import { getDeviceAuthorizationRequest } from "@/lib/server/oidc"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; +import { useTranslations } from "next-intl"; +import { useForm } from "react-hook-form"; +import { BackButton } from "./back-button"; +import { Button, ButtonVariants } from "./button"; +import { TextInput } from "./input"; +import { Spinner } from "./spinner"; +import { Translated } from "./translated"; + +type Inputs = { + userCode: string; +}; + +export function DeviceCodeForm({ userCode }: { userCode?: string }) { + const router = useRouter(); + + const { register, handleSubmit, formState } = useForm({ + mode: "onBlur", + defaultValues: { + userCode: userCode || "", + }, + }); + + const t = useTranslations("device"); + + const [error, setError] = useState(""); + + const [loading, setLoading] = useState(false); + + async function submitCodeAndContinue(value: Inputs): Promise { + setLoading(true); + + const response = await getDeviceAuthorizationRequest(value.userCode) + .catch(() => { + setError("Could not continue the request"); + return; + }) + .finally(() => { + setLoading(false); + }); + + if (!response || !response.deviceAuthorizationRequest?.id) { + setError("Could not continue the request"); + return; + } + + return router.push( + `/device/consent?` + + new URLSearchParams({ + requestId: `device_${response.deviceAuthorizationRequest.id}`, + user_code: value.userCode, + }).toString(), + ); + } + + return ( + <> +
+
+ +
+ + {error && ( +
+ {error} +
+ )} + +
+ + + +
+
+ + ); +} diff --git a/apps/login/src/components/dynamic-theme.tsx b/apps/login/src/components/dynamic-theme.tsx new file mode 100644 index 0000000000..ec92b1c627 --- /dev/null +++ b/apps/login/src/components/dynamic-theme.tsx @@ -0,0 +1,43 @@ +"use client"; + +import { Logo } from "@/components/logo"; +import { BrandingSettings } from "@zitadel/proto/zitadel/settings/v2/branding_settings_pb"; +import { ReactNode } from "react"; +import { AppAvatar } from "./app-avatar"; +import { ThemeWrapper } from "./theme-wrapper"; + +export function DynamicTheme({ + branding, + children, + appName, +}: { + children: ReactNode; + branding?: BrandingSettings; + appName?: string; +}) { + return ( + +
+
+
+ {branding && ( + <> + + + {appName && } + + )} +
+ +
{children}
+
+
+
+
+ ); +} diff --git a/apps/login/src/components/external-link.tsx b/apps/login/src/components/external-link.tsx new file mode 100644 index 0000000000..a52164d35d --- /dev/null +++ b/apps/login/src/components/external-link.tsx @@ -0,0 +1,21 @@ +import { ArrowRightIcon } from "@heroicons/react/24/solid"; +import { ReactNode } from "react"; + +export const ExternalLink = ({ + children, + href, +}: { + children: ReactNode; + href: string; +}) => { + return ( + +
{children}
+ + +
+ ); +}; diff --git a/apps/login/src/components/idp-signin.tsx b/apps/login/src/components/idp-signin.tsx new file mode 100644 index 0000000000..a7c938e90c --- /dev/null +++ b/apps/login/src/components/idp-signin.tsx @@ -0,0 +1,67 @@ +"use client"; + +import { createNewSessionFromIdpIntent } from "@/lib/server/idp"; +import { useRouter } from "next/navigation"; +import { useEffect, useState } from "react"; +import { Alert } from "./alert"; +import { Spinner } from "./spinner"; + +type Props = { + userId: string; + // organization: string; + idpIntent: { + idpIntentId: string; + idpIntentToken: string; + }; + requestId?: string; +}; + +export function IdpSignin({ + userId, + idpIntent: { idpIntentId, idpIntentToken }, + requestId, +}: Props) { + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const router = useRouter(); + + useEffect(() => { + createNewSessionFromIdpIntent({ + userId, + idpIntent: { + idpIntentId, + idpIntentToken, + }, + requestId, + }) + .then((response) => { + if (response && "error" in response && response?.error) { + setError(response?.error); + return; + } + + if (response && "redirect" in response && response?.redirect) { + return router.push(response.redirect); + } + }) + .catch(() => { + setError("An internal error occurred"); + return; + }) + .finally(() => { + setLoading(false); + }); + }, []); + + return ( +
+ {loading && } + {error && ( +
+ {error} +
+ )} +
+ ); +} diff --git a/apps/login/src/components/idps/base-button.tsx b/apps/login/src/components/idps/base-button.tsx new file mode 100644 index 0000000000..a38278e7cb --- /dev/null +++ b/apps/login/src/components/idps/base-button.tsx @@ -0,0 +1,41 @@ +"use client"; + +import { clsx } from "clsx"; +import { Loader2Icon } from "lucide-react"; +import { ButtonHTMLAttributes, DetailedHTMLProps, forwardRef } from "react"; +import { useFormStatus } from "react-dom"; + +export type SignInWithIdentityProviderProps = DetailedHTMLProps< + ButtonHTMLAttributes, + HTMLButtonElement +> & { + name?: string; + e2e?: string; +}; + +export const BaseButton = forwardRef< + HTMLButtonElement, + SignInWithIdentityProviderProps +>(function BaseButton(props, ref) { + const formStatus = useFormStatus(); + + return ( + + ); +}); diff --git a/apps/login/src/components/idps/pages/complete-idp.tsx b/apps/login/src/components/idps/pages/complete-idp.tsx new file mode 100644 index 0000000000..2061a28e3e --- /dev/null +++ b/apps/login/src/components/idps/pages/complete-idp.tsx @@ -0,0 +1,55 @@ +import { RegisterFormIDPIncomplete } from "@/components/register-form-idp-incomplete"; +import { BrandingSettings } from "@zitadel/proto/zitadel/settings/v2/branding_settings_pb"; +import { AddHumanUserRequest } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; +import { DynamicTheme } from "../../dynamic-theme"; +import { Translated } from "../../translated"; + +export async function completeIDP({ + idpUserId, + idpId, + idpUserName, + addHumanUser, + requestId, + organization, + branding, + idpIntent, +}: { + idpUserId: string; + idpId: string; + idpUserName: string; + addHumanUser?: AddHumanUserRequest; + requestId?: string; + organization: string; + branding?: BrandingSettings; + idpIntent: { + idpIntentId: string; + idpIntentToken: string; + }; +}) { + return ( + +
+

+ +

+

+ +

+ + +
+
+ ); +} diff --git a/apps/login/src/components/idps/pages/linking-failed.tsx b/apps/login/src/components/idps/pages/linking-failed.tsx new file mode 100644 index 0000000000..0c5a8264c4 --- /dev/null +++ b/apps/login/src/components/idps/pages/linking-failed.tsx @@ -0,0 +1,27 @@ +import { BrandingSettings } from "@zitadel/proto/zitadel/settings/v2/branding_settings_pb"; +import { Alert, AlertType } from "../../alert"; +import { DynamicTheme } from "../../dynamic-theme"; +import { Translated } from "../../translated"; + +export async function linkingFailed( + branding?: BrandingSettings, + error?: string, +) { + return ( + +
+

+ +

+

+ +

+ {error && ( +
+ {{error}} +
+ )} +
+
+ ); +} diff --git a/apps/login/src/components/idps/pages/linking-success.tsx b/apps/login/src/components/idps/pages/linking-success.tsx new file mode 100644 index 0000000000..8d41cd8c32 --- /dev/null +++ b/apps/login/src/components/idps/pages/linking-success.tsx @@ -0,0 +1,30 @@ +import { BrandingSettings } from "@zitadel/proto/zitadel/settings/v2/branding_settings_pb"; +import { DynamicTheme } from "../../dynamic-theme"; +import { IdpSignin } from "../../idp-signin"; +import { Translated } from "../../translated"; + +export async function linkingSuccess( + userId: string, + idpIntent: { idpIntentId: string; idpIntentToken: string }, + requestId?: string, + branding?: BrandingSettings, +) { + return ( + +
+

+ +

+

+ +

+ + +
+
+ ); +} diff --git a/apps/login/src/components/idps/pages/login-failed.tsx b/apps/login/src/components/idps/pages/login-failed.tsx new file mode 100644 index 0000000000..70c46919bf --- /dev/null +++ b/apps/login/src/components/idps/pages/login-failed.tsx @@ -0,0 +1,24 @@ +import { BrandingSettings } from "@zitadel/proto/zitadel/settings/v2/branding_settings_pb"; +import { Alert, AlertType } from "../../alert"; +import { DynamicTheme } from "../../dynamic-theme"; +import { Translated } from "../../translated"; + +export async function loginFailed(branding?: BrandingSettings, error?: string) { + return ( + +
+

+ +

+

+ +

+ {error && ( +
+ {{error}} +
+ )} +
+
+ ); +} diff --git a/apps/login/src/components/idps/pages/login-success.tsx b/apps/login/src/components/idps/pages/login-success.tsx new file mode 100644 index 0000000000..6beec160a9 --- /dev/null +++ b/apps/login/src/components/idps/pages/login-success.tsx @@ -0,0 +1,30 @@ +import { BrandingSettings } from "@zitadel/proto/zitadel/settings/v2/branding_settings_pb"; +import { DynamicTheme } from "../../dynamic-theme"; +import { IdpSignin } from "../../idp-signin"; +import { Translated } from "../../translated"; + +export async function loginSuccess( + userId: string, + idpIntent: { idpIntentId: string; idpIntentToken: string }, + requestId?: string, + branding?: BrandingSettings, +) { + return ( + +
+

+ +

+

+ +

+ + +
+
+ ); +} diff --git a/apps/login/src/components/idps/sign-in-with-apple.tsx b/apps/login/src/components/idps/sign-in-with-apple.tsx new file mode 100644 index 0000000000..8047cd6a7c --- /dev/null +++ b/apps/login/src/components/idps/sign-in-with-apple.tsx @@ -0,0 +1,36 @@ +"use client"; + +import { forwardRef } from "react"; +import { Translated } from "../translated"; +import { BaseButton, SignInWithIdentityProviderProps } from "./base-button"; + +export const SignInWithApple = forwardRef< + HTMLButtonElement, + SignInWithIdentityProviderProps +>(function SignInWithApple(props, ref) { + const { children, name, ...restProps } = props; + + return ( + +
+
+ + Apple Logo + + +
+
+ {children ? ( + children + ) : ( + + {name ? ( + name + ) : ( + + )} + + )} +
+ ); +}); diff --git a/apps/login/src/components/idps/sign-in-with-azure-ad.tsx b/apps/login/src/components/idps/sign-in-with-azure-ad.tsx new file mode 100644 index 0000000000..f16b8cab21 --- /dev/null +++ b/apps/login/src/components/idps/sign-in-with-azure-ad.tsx @@ -0,0 +1,42 @@ +"use client"; + +import { forwardRef } from "react"; +import { Translated } from "../translated"; +import { BaseButton, SignInWithIdentityProviderProps } from "./base-button"; + +export const SignInWithAzureAd = forwardRef< + HTMLButtonElement, + SignInWithIdentityProviderProps +>(function SignInWithAzureAd(props, ref) { + const { children, name, ...restProps } = props; + + return ( + +
+ + + + + + +
+ {children ? ( + children + ) : ( + + {name ? ( + name + ) : ( + + )} + + )} +
+ ); +}); diff --git a/apps/login/src/components/idps/sign-in-with-generic.tsx b/apps/login/src/components/idps/sign-in-with-generic.tsx new file mode 100644 index 0000000000..ab8f2f99be --- /dev/null +++ b/apps/login/src/components/idps/sign-in-with-generic.tsx @@ -0,0 +1,21 @@ +"use client"; + +import { forwardRef } from "react"; +import { BaseButton, SignInWithIdentityProviderProps } from "./base-button"; + +export const SignInWithGeneric = forwardRef< + HTMLButtonElement, + SignInWithIdentityProviderProps +>(function SignInWithGeneric(props, ref) { + const { + children, + name = "", + className = "h-[50px] pl-20", + ...restProps + } = props; + return ( + + {children ? children : {name}} + + ); +}); diff --git a/apps/login/src/components/idps/sign-in-with-github.tsx b/apps/login/src/components/idps/sign-in-with-github.tsx new file mode 100644 index 0000000000..55eb987112 --- /dev/null +++ b/apps/login/src/components/idps/sign-in-with-github.tsx @@ -0,0 +1,64 @@ +"use client"; + +import { forwardRef } from "react"; +import { Translated } from "../translated"; +import { BaseButton, SignInWithIdentityProviderProps } from "./base-button"; + +function GitHubLogo() { + return ( + <> + + + + + + + + ); +} + +export const SignInWithGithub = forwardRef< + HTMLButtonElement, + SignInWithIdentityProviderProps +>(function SignInWithGithub(props, ref) { + const { children, name, ...restProps } = props; + + return ( + +
+ +
+ {children ? ( + children + ) : ( + + {name ? ( + name + ) : ( + + )} + + )} +
+ ); +}); diff --git a/apps/login/src/components/idps/sign-in-with-gitlab.test.tsx b/apps/login/src/components/idps/sign-in-with-gitlab.test.tsx new file mode 100644 index 0000000000..ab5bfda54d --- /dev/null +++ b/apps/login/src/components/idps/sign-in-with-gitlab.test.tsx @@ -0,0 +1,45 @@ +import { afterEach, describe, expect, test } from "vitest"; + +import { cleanup, render, screen } from "@testing-library/react"; +import { NextIntlClientProvider } from "next-intl"; + +import { SignInWithGitlab } from "./sign-in-with-gitlab"; + +afterEach(cleanup); + +describe("", async () => { + const messages = { + idp: { + signInWithGitlab: "Sign in with GitLab", + }, + }; + + test("renders without crashing", () => { + const { container } = render( + + + , + ); + expect(container.firstChild).toBeDefined(); + }); + + test("displays the default text", () => { + render( + + + , + ); + const signInText = screen.getByText(/Sign in with Gitlab/i); + expect(signInText).toBeInTheDocument(); + }); + + test("displays the given text", () => { + render( + + + , + ); + const signInText = screen.getByText(/Gitlab/i); + expect(signInText).toBeInTheDocument(); + }); +}); diff --git a/apps/login/src/components/idps/sign-in-with-gitlab.tsx b/apps/login/src/components/idps/sign-in-with-gitlab.tsx new file mode 100644 index 0000000000..40a1ff115c --- /dev/null +++ b/apps/login/src/components/idps/sign-in-with-gitlab.tsx @@ -0,0 +1,53 @@ +"use client"; + +import { forwardRef } from "react"; +import { Translated } from "../translated"; +import { BaseButton, SignInWithIdentityProviderProps } from "./base-button"; + +export const SignInWithGitlab = forwardRef< + HTMLButtonElement, + SignInWithIdentityProviderProps +>(function SignInWithGitlab(props, ref) { + const { children, name, ...restProps } = props; + + return ( + +
+ + + + + + +
+ {children ? ( + children + ) : ( + + {name ? ( + name + ) : ( + + )} + + )} +
+ ); +}); diff --git a/apps/login/src/components/idps/sign-in-with-google.test.tsx b/apps/login/src/components/idps/sign-in-with-google.test.tsx new file mode 100644 index 0000000000..953da21d94 --- /dev/null +++ b/apps/login/src/components/idps/sign-in-with-google.test.tsx @@ -0,0 +1,44 @@ +import { afterEach, describe, expect, test } from "vitest"; + +import { cleanup, render, screen } from "@testing-library/react"; +import { NextIntlClientProvider } from "next-intl"; +import { SignInWithGoogle } from "./sign-in-with-google"; + +afterEach(cleanup); + +describe("", async () => { + const messages = { + idp: { + signInWithGoogle: "Sign in with Google", + }, + }; + + test("renders without crashing", () => { + const { container } = render( + + + , + ); + expect(container.firstChild).toBeDefined(); + }); + + test("displays the default text", () => { + render( + + + , + ); + const signInText = screen.getByText(/Sign in with Google/i); + expect(signInText).toBeInTheDocument(); + }); + + test("displays the given text", () => { + render( + + + , + ); + const signInText = screen.getByText(/Google/i); + expect(signInText).toBeInTheDocument(); + }); +}); diff --git a/apps/login/src/components/idps/sign-in-with-google.tsx b/apps/login/src/components/idps/sign-in-with-google.tsx new file mode 100644 index 0000000000..f1ba044075 --- /dev/null +++ b/apps/login/src/components/idps/sign-in-with-google.tsx @@ -0,0 +1,66 @@ +"use client"; + +import { forwardRef } from "react"; +import { Translated } from "../translated"; +import { BaseButton, SignInWithIdentityProviderProps } from "./base-button"; + +export const SignInWithGoogle = forwardRef< + HTMLButtonElement, + SignInWithIdentityProviderProps +>(function SignInWithGoogle(props, ref) { + const { children, name, ...restProps } = props; + + return ( + +
+ + + + + + + +
+ {children ? ( + children + ) : ( + + {name ? ( + name + ) : ( + + )} + + )} +
+ ); +}); diff --git a/apps/login/src/components/input.tsx b/apps/login/src/components/input.tsx new file mode 100644 index 0000000000..7d29fce691 --- /dev/null +++ b/apps/login/src/components/input.tsx @@ -0,0 +1,99 @@ +"use client"; + +import { CheckCircleIcon } from "@heroicons/react/24/solid"; +import { clsx } from "clsx"; +import { + ChangeEvent, + DetailedHTMLProps, + forwardRef, + InputHTMLAttributes, + ReactNode, +} from "react"; + +export type TextInputProps = DetailedHTMLProps< + InputHTMLAttributes, + HTMLInputElement +> & { + label: string; + suffix?: string; + placeholder?: string; + defaultValue?: string; + error?: string | ReactNode; + success?: string | ReactNode; + disabled?: boolean; + onChange?: (value: ChangeEvent) => void; + onBlur?: (value: ChangeEvent) => void; +}; + +const styles = (error: boolean, disabled: boolean) => + clsx({ + "h-[40px] mb-[2px] rounded p-[7px] bg-input-light-background dark:bg-input-dark-background transition-colors duration-300 grow": true, + "border border-input-light-border dark:border-input-dark-border hover:border-black hover:dark:border-white focus:border-primary-light-500 focus:dark:border-primary-dark-500": true, + "focus:outline-none focus:ring-0 text-base text-black dark:text-white placeholder:italic placeholder-gray-700 dark:placeholder-gray-700": true, + "border border-warn-light-500 dark:border-warn-dark-500 hover:border-warn-light-500 hover:dark:border-warn-dark-500 focus:border-warn-light-500 focus:dark:border-warn-dark-500": + error, + "pointer-events-none text-gray-500 dark:text-gray-800 border border-input-light-border dark:border-input-dark-border hover:border-light-hoverborder hover:dark:border-hoverborder cursor-default": + disabled, + }); + +// eslint-disable-next-line react/display-name +export const TextInput = forwardRef( + ( + { + label, + placeholder, + defaultValue, + suffix, + required = false, + error, + disabled, + success, + onChange, + onBlur, + ...props + }, + ref, + ) => { + return ( + + ); + }, +); diff --git a/apps/login/src/components/language-provider.tsx b/apps/login/src/components/language-provider.tsx new file mode 100644 index 0000000000..21a53093bb --- /dev/null +++ b/apps/login/src/components/language-provider.tsx @@ -0,0 +1,13 @@ +import { NextIntlClientProvider } from "next-intl"; +import { getMessages } from "next-intl/server"; +import { ReactNode } from "react"; + +export async function LanguageProvider({ children }: { children: ReactNode }) { + const messages = await getMessages(); + + return ( + + {children} + + ); +} diff --git a/apps/login/src/components/language-switcher.tsx b/apps/login/src/components/language-switcher.tsx new file mode 100644 index 0000000000..db5df5e14c --- /dev/null +++ b/apps/login/src/components/language-switcher.tsx @@ -0,0 +1,74 @@ +"use client"; + +import { setLanguageCookie } from "@/lib/cookies"; +import { Lang, LANGS } from "@/lib/i18n"; +import { + Listbox, + ListboxButton, + ListboxOption, + ListboxOptions, +} from "@headlessui/react"; +import { CheckIcon, ChevronDownIcon } from "@heroicons/react/24/outline"; +import clsx from "clsx"; +import { useLocale } from "next-intl"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; + +export function LanguageSwitcher() { + const currentLocale = useLocale(); + + const [selected, setSelected] = useState( + LANGS.find((l) => l.code === currentLocale) || LANGS[0], + ); + + const router = useRouter(); + + const handleChange = async (language: Lang) => { + setSelected(language); + const newLocale = language.code; + + await setLanguageCookie(newLocale); + + router.refresh(); + }; + + return ( +
+ + + {selected.name} + + + {LANGS.map((lang) => ( + + +
+ {lang.name} +
+
+ ))} +
+
+
+ ); +} diff --git a/apps/login/src/components/layout-providers.tsx b/apps/login/src/components/layout-providers.tsx new file mode 100644 index 0000000000..fee93d015e --- /dev/null +++ b/apps/login/src/components/layout-providers.tsx @@ -0,0 +1,17 @@ +"use client"; + +import { useTheme } from "next-themes"; +import { ReactNode } from "react"; + +type Props = { + children: ReactNode; +}; + +export function LayoutProviders({ children }: Props) { + const { resolvedTheme } = useTheme(); + const isDark = resolvedTheme === "dark"; + + return ( +
{children}
+ ); +} diff --git a/apps/login/src/components/ldap-username-password-form.tsx b/apps/login/src/components/ldap-username-password-form.tsx new file mode 100644 index 0000000000..cc9071875f --- /dev/null +++ b/apps/login/src/components/ldap-username-password-form.tsx @@ -0,0 +1,109 @@ +"use client"; + +import { createNewSessionForLDAP } from "@/lib/server/idp"; +import { useTranslations } from "next-intl"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { Alert } from "./alert"; +import { BackButton } from "./back-button"; +import { Button, ButtonVariants } from "./button"; +import { TextInput } from "./input"; +import { Spinner } from "./spinner"; +import { Translated } from "./translated"; + +type Inputs = { + loginName: string; + password: string; +}; + +type Props = { + idpId: string; + link: boolean; +}; + +export function LDAPUsernamePasswordForm({ idpId, link }: Props) { + const { register, handleSubmit, formState } = useForm({ + mode: "onBlur", + }); + + const t = useTranslations("ldap"); + + const [error, setError] = useState(""); + + const [loading, setLoading] = useState(false); + + const router = useRouter(); + + async function submitUsernamePassword(values: Inputs) { + setError(""); + setLoading(true); + + const response = await createNewSessionForLDAP({ + idpId: idpId, + username: values.loginName, + password: values.password, + link: link, + }) + .catch(() => { + setError("Could not start LDAP flow"); + return; + }) + .finally(() => { + setLoading(false); + }); + + if (response && "error" in response && response.error) { + setError(response.error); + return; + } + + if (response && "redirect" in response && response.redirect) { + return router.push(response.redirect); + } + } + + return ( +
+ + +
+ +
+ + {error && ( +
+ {error} +
+ )} + +
+ + + +
+ + ); +} diff --git a/apps/login/src/components/login-otp.tsx b/apps/login/src/components/login-otp.tsx new file mode 100644 index 0000000000..ffaa5320b0 --- /dev/null +++ b/apps/login/src/components/login-otp.tsx @@ -0,0 +1,287 @@ +"use client"; + +import { getNextUrl } from "@/lib/client"; +import { updateSession } from "@/lib/server/session"; +import { create } from "@zitadel/client"; +import { RequestChallengesSchema } from "@zitadel/proto/zitadel/session/v2/challenge_pb"; +import { ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; +import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; +import { useRouter } from "next/navigation"; +import { useEffect, useRef, useState } from "react"; +import { useForm } from "react-hook-form"; +import { Alert, AlertType } from "./alert"; +import { BackButton } from "./back-button"; +import { useTranslations } from "next-intl"; +import { Button, ButtonVariants } from "./button"; +import { TextInput } from "./input"; +import { Spinner } from "./spinner"; +import { Translated } from "./translated"; + +// either loginName or sessionId must be provided +type Props = { + host: string | null; + loginName?: string; + sessionId?: string; + requestId?: string; + organization?: string; + method: string; + code?: string; + loginSettings?: LoginSettings; +}; + +type Inputs = { + code: string; +}; + +export function LoginOTP({ + host, + loginName, + sessionId, + requestId, + organization, + method, + code, + loginSettings, +}: Props) { + const t = useTranslations("otp"); + + const [error, setError] = useState(""); + const [loading, setLoading] = useState(false); + + const router = useRouter(); + + const initialized = useRef(false); + + const { register, handleSubmit, formState } = useForm({ + mode: "onBlur", + defaultValues: { + code: code ? code : "", + }, + }); + + useEffect(() => { + if (!initialized.current && ["email", "sms"].includes(method) && !code) { + initialized.current = true; + setLoading(true); + updateSessionForOTPChallenge() + .catch((error) => { + setError(error); + return; + }) + .finally(() => { + setLoading(false); + }); + } + }, []); + + async function updateSessionForOTPChallenge() { + let challenges; + + const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? ""; + + if (method === "email") { + challenges = create(RequestChallengesSchema, { + otpEmail: { + deliveryType: { + case: "sendCode", + value: host + ? { + urlTemplate: + `${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/otp/${method}?code={{.Code}}&userId={{.UserID}}&sessionId={{.SessionID}}` + + (requestId ? `&requestId=${requestId}` : ""), + } + : {}, + }, + }, + }); + } + + if (method === "sms") { + challenges = create(RequestChallengesSchema, { + otpSms: {}, + }); + } + + setLoading(true); + const response = await updateSession({ + loginName, + sessionId, + organization, + challenges, + requestId, + }) + .catch(() => { + setError("Could not request OTP challenge"); + return; + }) + .finally(() => { + setLoading(false); + }); + + if (response && "error" in response && response.error) { + setError(response.error); + return; + } + + return response; + } + + async function submitCode(values: Inputs, organization?: string) { + setLoading(true); + + let body: any = { + code: values.code, + method, + }; + + if (organization) { + body.organization = organization; + } + + if (requestId) { + body.requestId = requestId; + } + + let checks; + + if (method === "sms") { + checks = create(ChecksSchema, { + otpSms: { code: values.code }, + }); + } + if (method === "email") { + checks = create(ChecksSchema, { + otpEmail: { code: values.code }, + }); + } + if (method === "time-based") { + checks = create(ChecksSchema, { + totp: { code: values.code }, + }); + } + + const response = await updateSession({ + loginName, + sessionId, + organization, + checks, + requestId, + }) + .catch(() => { + setError("Could not verify OTP code"); + return; + }) + .finally(() => { + setLoading(false); + }); + + if (response && "error" in response && response.error) { + setError(response.error); + return; + } + + return response; + } + + function setCodeAndContinue(values: Inputs, organization?: string) { + return submitCode(values, organization).then(async (response) => { + if (response && "sessionId" in response) { + setLoading(true); + // Wait for 2 seconds to avoid eventual consistency issues with an OTP code being verified in the /login endpoint + await new Promise((resolve) => setTimeout(resolve, 2000)); + + const url = + requestId && response.sessionId + ? await getNextUrl( + { + sessionId: response.sessionId, + requestId: requestId, + organization: response.factors?.user?.organizationId, + }, + loginSettings?.defaultRedirectUri, + ) + : response.factors?.user + ? await getNextUrl( + { + loginName: response.factors.user.loginName, + organization: response.factors?.user?.organizationId, + }, + loginSettings?.defaultRedirectUri, + ) + : null; + + setLoading(false); + if (url) { + router.push(url); + } + } + }); + } + + return ( +
+ {["email", "sms"].includes(method) && ( + +
+ + + + +
+
+ )} +
+ +
+ + {error && ( +
+ {error} +
+ )} + +
+ + + +
+
+ ); +} diff --git a/apps/login/src/components/login-passkey.tsx b/apps/login/src/components/login-passkey.tsx new file mode 100644 index 0000000000..968f861ff1 --- /dev/null +++ b/apps/login/src/components/login-passkey.tsx @@ -0,0 +1,280 @@ +"use client"; + +import { coerceToArrayBuffer, coerceToBase64Url } from "@/helpers/base64"; +import { sendPasskey } from "@/lib/server/passkeys"; +import { updateSession } from "@/lib/server/session"; +import { create, JsonObject } from "@zitadel/client"; +import { + RequestChallengesSchema, + UserVerificationRequirement, +} from "@zitadel/proto/zitadel/session/v2/challenge_pb"; +import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; +import { useRouter } from "next/navigation"; +import { useEffect, useRef, useState } from "react"; +import { Alert } from "./alert"; +import { BackButton } from "./back-button"; +import { Button, ButtonVariants } from "./button"; +import { Spinner } from "./spinner"; +import { Translated } from "./translated"; + +// either loginName or sessionId must be provided +type Props = { + loginName?: string; + sessionId?: string; + requestId?: string; + altPassword: boolean; + login?: boolean; + organization?: string; +}; + +export function LoginPasskey({ + loginName, + sessionId, + requestId, + altPassword, + organization, + login = true, +}: Props) { + const [error, setError] = useState(""); + const [loading, setLoading] = useState(false); + + const router = useRouter(); + + const initialized = useRef(false); + + useEffect(() => { + if (!initialized.current) { + initialized.current = true; + setLoading(true); + updateSessionForChallenge() + .then((response) => { + const pK = + response?.challenges?.webAuthN?.publicKeyCredentialRequestOptions + ?.publicKey; + + if (!pK) { + setError("Could not request passkey challenge"); + setLoading(false); + return; + } + + return submitLoginAndContinue(pK) + .catch((error) => { + setError(error); + return; + }) + .finally(() => { + setLoading(false); + }); + }) + .catch((error) => { + setError(error); + return; + }) + .finally(() => { + setLoading(false); + }); + } + }, []); + + async function updateSessionForChallenge( + userVerificationRequirement: number = login + ? UserVerificationRequirement.REQUIRED + : UserVerificationRequirement.DISCOURAGED, + ) { + setError(""); + setLoading(true); + const session = await updateSession({ + loginName, + sessionId, + organization, + challenges: create(RequestChallengesSchema, { + webAuthN: { + domain: "", + userVerificationRequirement, + }, + }), + requestId, + }) + .catch(() => { + setError("Could not request passkey challenge"); + return; + }) + .finally(() => { + setLoading(false); + }); + + if (session && "error" in session && session.error) { + setError(session.error); + return; + } + + return session; + } + + async function submitLogin(data: JsonObject) { + setLoading(true); + const response = await sendPasskey({ + loginName, + sessionId, + organization, + checks: { + webAuthN: { credentialAssertionData: data }, + } as Checks, + requestId, + }) + .catch(() => { + setError("Could not verify passkey"); + return; + }) + .finally(() => { + setLoading(false); + }); + + if (response && "error" in response && response.error) { + setError(response.error); + return; + } + + if (response && "redirect" in response && response.redirect) { + return router.push(response.redirect); + } + } + + async function submitLoginAndContinue( + publicKey: any, + ): Promise { + publicKey.challenge = coerceToArrayBuffer( + publicKey.challenge, + "publicKey.challenge", + ); + publicKey.allowCredentials.map((listItem: any) => { + listItem.id = coerceToArrayBuffer( + listItem.id, + "publicKey.allowCredentials.id", + ); + }); + + navigator.credentials + .get({ + publicKey, + }) + .then((assertedCredential: any) => { + if (!assertedCredential) { + setError("An error on retrieving passkey"); + return; + } + + const authData = new Uint8Array( + assertedCredential.response.authenticatorData, + ); + const clientDataJSON = new Uint8Array( + assertedCredential.response.clientDataJSON, + ); + const rawId = new Uint8Array(assertedCredential.rawId); + const sig = new Uint8Array(assertedCredential.response.signature); + const userHandle = new Uint8Array( + assertedCredential.response.userHandle, + ); + const data = { + id: assertedCredential.id, + rawId: coerceToBase64Url(rawId, "rawId"), + type: assertedCredential.type, + response: { + authenticatorData: coerceToBase64Url(authData, "authData"), + clientDataJSON: coerceToBase64Url(clientDataJSON, "clientDataJSON"), + signature: coerceToBase64Url(sig, "sig"), + userHandle: coerceToBase64Url(userHandle, "userHandle"), + }, + }; + + return submitLogin(data); + }) + .finally(() => { + setLoading(false); + }); + } + + return ( +
+ {error && ( +
+ {error} +
+ )} +
+ {altPassword ? ( + + ) : ( + + )} + + + +
+
+ ); +} diff --git a/apps/login/src/components/logo.tsx b/apps/login/src/components/logo.tsx new file mode 100644 index 0000000000..09819f2ac3 --- /dev/null +++ b/apps/login/src/components/logo.tsx @@ -0,0 +1,37 @@ +import Image from "next/image"; + +type Props = { + darkSrc?: string; + lightSrc?: string; + height?: number; + width?: number; +}; + +export function Logo({ lightSrc, darkSrc, height = 40, width = 147.5 }: Props) { + return ( + <> + {darkSrc && ( +
+ logo +
+ )} + {lightSrc && ( +
+ logo +
+ )} + + ); +} diff --git a/apps/login/src/components/password-complexity.test.tsx b/apps/login/src/components/password-complexity.test.tsx new file mode 100644 index 0000000000..090c95d397 --- /dev/null +++ b/apps/login/src/components/password-complexity.test.tsx @@ -0,0 +1,64 @@ +import { + cleanup, + render, + screen, + waitFor, + within, +} from "@testing-library/react"; +import { afterEach, beforeEach, describe, expect, test } from "vitest"; +import { PasswordComplexity } from "./password-complexity"; + +const matchesTitle = `Matches`; +const doesntMatchTitle = `Doesn't match`; + +describe("", () => { + describe.each` + settingsMinLength | password | expectSVGTitle + ${5} | ${"Password1!"} | ${matchesTitle} + ${30} | ${"Password1!"} | ${doesntMatchTitle} + ${0} | ${"Password1!"} | ${matchesTitle} + ${undefined} | ${"Password1!"} | ${false} + `( + `With settingsMinLength=$settingsMinLength, password=$password, expectSVGTitle=$expectSVGTitle`, + ({ settingsMinLength, password, expectSVGTitle }) => { + const feedbackElementLabel = /password length/i; + beforeEach(() => { + render( + , + ); + }); + afterEach(cleanup); + + if (expectSVGTitle === false) { + test(`should not render the feedback element`, async () => { + await waitFor(() => { + expect( + screen.queryByText(feedbackElementLabel), + ).not.toBeInTheDocument(); + }); + }); + } else { + test(`Should show one SVG with title ${expectSVGTitle}`, async () => { + await waitFor(async () => { + const svg = within( + screen.getByText(feedbackElementLabel) + .parentElement as HTMLElement, + ).findByRole("img"); + expect(await svg).toHaveTextContent(expectSVGTitle); + }); + }); + } + }, + ); +}); diff --git a/apps/login/src/components/password-complexity.tsx b/apps/login/src/components/password-complexity.tsx new file mode 100644 index 0000000000..4f0b460c71 --- /dev/null +++ b/apps/login/src/components/password-complexity.tsx @@ -0,0 +1,99 @@ +import { + lowerCaseValidator, + numberValidator, + symbolValidator, + upperCaseValidator, +} from "@/helpers/validators"; +import { PasswordComplexitySettings } from "@zitadel/proto/zitadel/settings/v2/password_settings_pb"; + +type Props = { + passwordComplexitySettings: PasswordComplexitySettings; + password: string; + equals: boolean; +}; + +const check = ( + + Matches + + +); +const cross = ( + + Doesn't match + + +); +const desc = + "text-14px leading-4 text-input-light-label dark:text-input-dark-label"; + +export function PasswordComplexity({ + passwordComplexitySettings, + password, + equals, +}: Props) { + const hasMinLength = password?.length >= passwordComplexitySettings.minLength; + const hasSymbol = symbolValidator(password); + const hasNumber = numberValidator(password); + const hasUppercase = upperCaseValidator(password); + const hasLowercase = lowerCaseValidator(password); + + return ( +
+ {passwordComplexitySettings.minLength != undefined ? ( +
+ {hasMinLength ? check : cross} + + Password length {passwordComplexitySettings.minLength.toString()} + +
+ ) : ( + + )} +
+ {hasSymbol ? check : cross} + has Symbol +
+
+ {hasNumber ? check : cross} + has Number +
+
+ {hasUppercase ? check : cross} + has uppercase +
+
+ {hasLowercase ? check : cross} + has lowercase +
+
+ {equals ? check : cross} + equals +
+
+ ); +} diff --git a/apps/login/src/components/password-form.tsx b/apps/login/src/components/password-form.tsx new file mode 100644 index 0000000000..c8a10a9907 --- /dev/null +++ b/apps/login/src/components/password-form.tsx @@ -0,0 +1,179 @@ +"use client"; + +import { resetPassword, sendPassword } from "@/lib/server/password"; +import { create } from "@zitadel/client"; +import { ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; +import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; +import { useTranslations } from "next-intl"; +import { useForm } from "react-hook-form"; +import { Alert, AlertType } from "./alert"; +import { BackButton } from "./back-button"; +import { Button, ButtonVariants } from "./button"; +import { TextInput } from "./input"; +import { Spinner } from "./spinner"; +import { Translated } from "./translated"; + +type Inputs = { + password: string; +}; + +type Props = { + loginSettings: LoginSettings | undefined; + loginName: string; + organization?: string; + requestId?: string; +}; + +export function PasswordForm({ + loginSettings, + loginName, + organization, + requestId, +}: Props) { + const { register, handleSubmit, formState } = useForm({ + mode: "onBlur", + }); + + const t = useTranslations("password"); + + const [info, setInfo] = useState(""); + const [error, setError] = useState(""); + + const [loading, setLoading] = useState(false); + + const router = useRouter(); + + async function submitPassword(values: Inputs) { + setError(""); + setLoading(true); + + const response = await sendPassword({ + loginName, + organization, + checks: create(ChecksSchema, { + password: { password: values.password }, + }), + requestId, + }) + .catch(() => { + setError("Could not verify password"); + return; + }) + .finally(() => { + setLoading(false); + }); + + if (response && "error" in response && response.error) { + setError(response.error); + return; + } + + if (response && "redirect" in response && response.redirect) { + return router.push(response.redirect); + } + } + + async function resetPasswordAndContinue() { + setError(""); + setInfo(""); + setLoading(true); + + const response = await resetPassword({ + loginName, + organization, + requestId, + }) + .catch(() => { + setError("Could not reset password"); + return; + }) + .finally(() => { + setLoading(false); + }); + + if (response && "error" in response) { + setError(response.error); + return; + } + + setInfo("Password was reset. Please check your email."); + + const params = new URLSearchParams({ + loginName: loginName, + }); + + if (organization) { + params.append("organization", organization); + } + + if (requestId) { + params.append("requestId", requestId); + } + + return router.push("/password/set?" + params); + } + + return ( +
+
+ + {!loginSettings?.hidePasswordReset && ( + + )} + + {loginName && ( + + )} +
+ + {info && ( +
+ {info} +
+ )} + + {error && ( +
+ {error} +
+ )} + +
+ + + +
+
+ ); +} diff --git a/apps/login/src/components/privacy-policy-checkboxes.tsx b/apps/login/src/components/privacy-policy-checkboxes.tsx new file mode 100644 index 0000000000..86ccb08721 --- /dev/null +++ b/apps/login/src/components/privacy-policy-checkboxes.tsx @@ -0,0 +1,105 @@ +"use client"; +import { LegalAndSupportSettings } from "@zitadel/proto/zitadel/settings/v2/legal_settings_pb"; +import Link from "next/link"; +import { useState } from "react"; +import { Checkbox } from "./checkbox"; +import { Translated } from "./translated"; + +type Props = { + legal: LegalAndSupportSettings; + onChange: (allAccepted: boolean) => void; +}; + +type AcceptanceState = { + tosAccepted: boolean; + privacyPolicyAccepted: boolean; +}; + +export function PrivacyPolicyCheckboxes({ legal, onChange }: Props) { + const [acceptanceState, setAcceptanceState] = useState({ + tosAccepted: false, + privacyPolicyAccepted: false, + }); + + return ( + <> +

+ + {legal?.helpLink && ( + + + + + + + + )} +

+ {legal?.tosLink && ( +
+ { + setAcceptanceState({ + ...acceptanceState, + tosAccepted: checked, + }); + onChange(checked && acceptanceState.privacyPolicyAccepted); + }} + data-testid="privacy-policy-checkbox" + /> + +
+

+ + + +

+
+
+ )} + {legal?.privacyPolicyLink && ( +
+ { + setAcceptanceState({ + ...acceptanceState, + privacyPolicyAccepted: checked, + }); + onChange(checked && acceptanceState.tosAccepted); + }} + data-testid="tos-checkbox" + /> + +
+

+ + + +

+
+
+ )} + + ); +} diff --git a/apps/login/src/components/register-form-idp-incomplete.tsx b/apps/login/src/components/register-form-idp-incomplete.tsx new file mode 100644 index 0000000000..10e766be76 --- /dev/null +++ b/apps/login/src/components/register-form-idp-incomplete.tsx @@ -0,0 +1,159 @@ +"use client"; + +import { registerUserAndLinkToIDP } from "@/lib/server/register"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; +import { useTranslations } from "next-intl"; +import { FieldValues, useForm } from "react-hook-form"; +import { Alert } from "./alert"; +import { BackButton } from "./back-button"; +import { Button, ButtonVariants } from "./button"; +import { TextInput } from "./input"; +import { Spinner } from "./spinner"; +import { Translated } from "./translated"; + +type Inputs = + | { + firstname: string; + lastname: string; + email: string; + } + | FieldValues; + +type Props = { + organization: string; + requestId?: string; + idpIntent: { + idpIntentId: string; + idpIntentToken: string; + }; + defaultValues?: { + firstname?: string; + lastname?: string; + email?: string; + }; + idpUserId: string; + idpId: string; + idpUserName: string; +}; + +export function RegisterFormIDPIncomplete({ + organization, + requestId, + idpIntent, + defaultValues, + idpUserId, + idpId, + idpUserName, +}: Props) { + const { register, handleSubmit, formState } = useForm({ + mode: "onBlur", + defaultValues: { + email: defaultValues?.email ?? "", + firstname: defaultValues?.firstname ?? "", + lastname: defaultValues?.lastname ?? "", + }, + }); + + const t = useTranslations("register"); + + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + + const router = useRouter(); + + async function submitAndRegister(values: Inputs) { + setLoading(true); + const response = await registerUserAndLinkToIDP({ + idpId: idpId, + idpUserName: idpUserName, + idpUserId: idpUserId, + email: values.email, + firstName: values.firstname, + lastName: values.lastname, + organization: organization, + requestId: requestId, + idpIntent: idpIntent, + }) + .catch(() => { + setError("Could not register user"); + return; + }) + .finally(() => { + setLoading(false); + }); + + if (response && "error" in response && response.error) { + setError(response.error); + return; + } + + if (response && "redirect" in response && response.redirect) { + return router.push(response.redirect); + } + + return response; + } + + const { errors } = formState; + + return ( +
+
+
+ +
+
+ +
+
+ +
+
+ + {error && ( +
+ {error} +
+ )} + +
+ + +
+
+ ); +} diff --git a/apps/login/src/components/register-form.tsx b/apps/login/src/components/register-form.tsx new file mode 100644 index 0000000000..7b4254b0b3 --- /dev/null +++ b/apps/login/src/components/register-form.tsx @@ -0,0 +1,230 @@ +"use client"; + +import { registerUser } from "@/lib/server/register"; +import { LegalAndSupportSettings } from "@zitadel/proto/zitadel/settings/v2/legal_settings_pb"; +import { + LoginSettings, + PasskeysType, +} from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; +import { useTranslations } from "next-intl"; +import { FieldValues, useForm } from "react-hook-form"; +import { Alert, AlertType } from "./alert"; +import { + AuthenticationMethod, + AuthenticationMethodRadio, + methods, +} from "./authentication-method-radio"; +import { BackButton } from "./back-button"; +import { Button, ButtonVariants } from "./button"; +import { TextInput } from "./input"; +import { PrivacyPolicyCheckboxes } from "./privacy-policy-checkboxes"; +import { Spinner } from "./spinner"; +import { Translated } from "./translated"; + +type Inputs = + | { + firstname: string; + lastname: string; + email: string; + } + | FieldValues; + +type Props = { + legal: LegalAndSupportSettings; + firstname?: string; + lastname?: string; + email?: string; + organization: string; + requestId?: string; + loginSettings?: LoginSettings; + idpCount: number; +}; + +export function RegisterForm({ + legal, + email, + firstname, + lastname, + organization, + requestId, + loginSettings, + idpCount = 0, +}: Props) { + const { register, handleSubmit, formState } = useForm({ + mode: "onBlur", + defaultValues: { + email: email ?? "", + firstName: firstname ?? "", + lastname: lastname ?? "", + }, + }); + + const t = useTranslations("register"); + + const [loading, setLoading] = useState(false); + const [selected, setSelected] = useState(methods[0]); + const [error, setError] = useState(""); + + const router = useRouter(); + + async function submitAndRegister(values: Inputs) { + setLoading(true); + const response = await registerUser({ + email: values.email, + firstName: values.firstname, + lastName: values.lastname, + organization: organization, + requestId: requestId, + }) + .catch(() => { + setError("Could not register user"); + return; + }) + .finally(() => { + setLoading(false); + }); + + if (response && "error" in response && response.error) { + setError(response.error); + return; + } + + if (response && "redirect" in response && response.redirect) { + return router.push(response.redirect); + } + + return response; + } + + async function submitAndContinue( + value: Inputs, + withPassword: boolean = false, + ) { + const registerParams: any = value; + + if (organization) { + registerParams.organization = organization; + } + + if (requestId) { + registerParams.requestId = requestId; + } + + // redirect user to /register/password if password is chosen + if (withPassword) { + return router.push( + `/register/password?` + new URLSearchParams(registerParams), + ); + } else { + return submitAndRegister(value); + } + } + + const { errors } = formState; + + const [tosAndPolicyAccepted, setTosAndPolicyAccepted] = useState(false); + return ( +
+
+
+ +
+
+ +
+
+ +
+
+ {legal && ( + + )} + {/* show chooser if both methods are allowed */} + {loginSettings && + loginSettings.allowUsernamePassword && + loginSettings.passkeysType == PasskeysType.ALLOWED && ( + <> +

+ +

+ +
+ +
+ + )} + {!loginSettings?.allowUsernamePassword && + loginSettings?.passkeysType !== PasskeysType.ALLOWED && + (!loginSettings?.allowExternalIdp || !idpCount) && ( +
+ + + +
+ )} + + {error && ( +
+ {error} +
+ )} + +
+ + +
+ + ); +} diff --git a/apps/login/src/components/register-passkey.tsx b/apps/login/src/components/register-passkey.tsx new file mode 100644 index 0000000000..5855c906fd --- /dev/null +++ b/apps/login/src/components/register-passkey.tsx @@ -0,0 +1,220 @@ +"use client"; + +import { coerceToArrayBuffer, coerceToBase64Url } from "@/helpers/base64"; +import { + registerPasskeyLink, + verifyPasskeyRegistration, +} from "@/lib/server/passkeys"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { Alert } from "./alert"; +import { BackButton } from "./back-button"; +import { Button, ButtonVariants } from "./button"; +import { Spinner } from "./spinner"; +import { Translated } from "./translated"; + +type Inputs = {}; + +type Props = { + sessionId: string; + isPrompt: boolean; + requestId?: string; + organization?: string; +}; + +export function RegisterPasskey({ + sessionId, + isPrompt, + organization, + requestId, +}: Props) { + const { handleSubmit, formState } = useForm({ + mode: "onBlur", + }); + + const [error, setError] = useState(""); + + const [loading, setLoading] = useState(false); + + const router = useRouter(); + + async function submitVerify( + passkeyId: string, + passkeyName: string, + publicKeyCredential: any, + sessionId: string, + ) { + setLoading(true); + const response = await verifyPasskeyRegistration({ + passkeyId, + passkeyName, + publicKeyCredential, + sessionId, + }) + .catch(() => { + setError("Could not verify Passkey"); + return; + }) + .finally(() => { + setLoading(false); + }); + + return response; + } + + async function submitRegisterAndContinue(): Promise { + setLoading(true); + const resp = await registerPasskeyLink({ + sessionId, + }) + .catch(() => { + setError("Could not register passkey"); + return; + }) + .finally(() => { + setLoading(false); + }); + + if (!resp) { + setError("An error on registering passkey"); + return; + } + + if ("error" in resp && resp.error) { + setError(resp.error); + return; + } + + if (!("passkeyId" in resp)) { + setError("An error on registering passkey"); + return; + } + + const passkeyId = resp.passkeyId; + const options: CredentialCreationOptions = + (resp.publicKeyCredentialCreationOptions as CredentialCreationOptions) ?? + {}; + + if (!options.publicKey) { + setError("An error on registering passkey"); + return; + } + + options.publicKey.challenge = coerceToArrayBuffer( + options.publicKey.challenge, + "challenge", + ); + options.publicKey.user.id = coerceToArrayBuffer( + options.publicKey.user.id, + "userid", + ); + if (options.publicKey.excludeCredentials) { + options.publicKey.excludeCredentials.map((cred: any) => { + cred.id = coerceToArrayBuffer( + cred.id as string, + "excludeCredentials.id", + ); + return cred; + }); + } + + const credentials = await navigator.credentials.create(options); + + if ( + !credentials || + !(credentials as any).response?.attestationObject || + !(credentials as any).response?.clientDataJSON || + !(credentials as any).rawId + ) { + setError("An error on registering passkey"); + return; + } + + const attestationObject = (credentials as any).response.attestationObject; + const clientDataJSON = (credentials as any).response.clientDataJSON; + const rawId = (credentials as any).rawId; + + const data = { + id: credentials.id, + rawId: coerceToBase64Url(rawId, "rawId"), + type: credentials.type, + response: { + attestationObject: coerceToBase64Url( + attestationObject, + "attestationObject", + ), + clientDataJSON: coerceToBase64Url(clientDataJSON, "clientDataJSON"), + }, + }; + + const verificationResponse = await submitVerify( + passkeyId, + "", + data, + sessionId, + ); + + if (!verificationResponse) { + setError("Could not verify Passkey!"); + return; + } + + continueAndLogin(); + } + + function continueAndLogin() { + const params = new URLSearchParams(); + + if (organization) { + params.set("organization", organization); + } + + if (requestId) { + params.set("requestId", requestId); + } + + params.set("sessionId", sessionId); + + router.push("/passkey?" + params); + } + + return ( +
+ {error && ( +
+ {error} +
+ )} + +
+ {isPrompt ? ( + + ) : ( + + )} + + + +
+
+ ); +} diff --git a/apps/login/src/components/register-u2f.tsx b/apps/login/src/components/register-u2f.tsx new file mode 100644 index 0000000000..ebaabb8390 --- /dev/null +++ b/apps/login/src/components/register-u2f.tsx @@ -0,0 +1,225 @@ +"use client"; + +import { coerceToArrayBuffer, coerceToBase64Url } from "@/helpers/base64"; +import { getNextUrl } from "@/lib/client"; +import { addU2F, verifyU2F } from "@/lib/server/u2f"; +import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; +import { RegisterU2FResponse } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; +import { Alert } from "./alert"; +import { BackButton } from "./back-button"; +import { Button, ButtonVariants } from "./button"; +import { Spinner } from "./spinner"; +import { Translated } from "./translated"; + +type Props = { + loginName?: string; + sessionId: string; + requestId?: string; + organization?: string; + checkAfter: boolean; + loginSettings?: LoginSettings; +}; + +export function RegisterU2f({ + loginName, + sessionId, + organization, + requestId, + checkAfter, + loginSettings, +}: Props) { + const [error, setError] = useState(""); + + const [loading, setLoading] = useState(false); + + const router = useRouter(); + + async function submitVerify( + u2fId: string, + passkeyName: string, + publicKeyCredential: any, + sessionId: string, + ) { + setError(""); + setLoading(true); + const response = await verifyU2F({ + u2fId, + passkeyName, + publicKeyCredential, + sessionId, + }) + .catch(() => { + setError("An error on verifying passkey occurred"); + return; + }) + .finally(() => { + setLoading(false); + }); + + if (response && "error" in response && response?.error) { + setError(response?.error); + return; + } + + return response; + } + + async function submitRegisterAndContinue(): Promise { + setError(""); + setLoading(true); + const response = await addU2F({ + sessionId, + }) + .catch(() => { + setError("An error on registering passkey"); + return; + }) + .finally(() => { + setLoading(false); + }); + + if (response && "error" in response && response?.error) { + setError(response?.error); + return; + } + + if (!response || !("u2fId" in response)) { + setError("An error on registering passkey"); + return; + } + + const u2fResponse = response as unknown as RegisterU2FResponse; + + const u2fId = u2fResponse.u2fId; + const options: CredentialCreationOptions = + (u2fResponse?.publicKeyCredentialCreationOptions as CredentialCreationOptions) ?? + {}; + + if (options.publicKey) { + options.publicKey.challenge = coerceToArrayBuffer( + options.publicKey.challenge, + "challenge", + ); + options.publicKey.user.id = coerceToArrayBuffer( + options.publicKey.user.id, + "userid", + ); + if (options.publicKey.excludeCredentials) { + options.publicKey.excludeCredentials.map((cred: any) => { + cred.id = coerceToArrayBuffer( + cred.id as string, + "excludeCredentials.id", + ); + return cred; + }); + } + + const resp = await navigator.credentials.create(options); + + if ( + !resp || + !(resp as any).response.attestationObject || + !(resp as any).response.clientDataJSON || + !(resp as any).rawId + ) { + setError("An error on registering passkey"); + return; + } + + const attestationObject = (resp as any).response.attestationObject; + const clientDataJSON = (resp as any).response.clientDataJSON; + const rawId = (resp as any).rawId; + + const data = { + id: resp.id, + rawId: coerceToBase64Url(rawId, "rawId"), + type: resp.type, + response: { + attestationObject: coerceToBase64Url( + attestationObject, + "attestationObject", + ), + clientDataJSON: coerceToBase64Url(clientDataJSON, "clientDataJSON"), + }, + }; + + const submitResponse = await submitVerify(u2fId, "", data, sessionId); + + if (!submitResponse) { + setError("An error on verifying passkey"); + return; + } + + if (checkAfter) { + const paramsToContinue = new URLSearchParams({}); + + if (sessionId) { + paramsToContinue.append("sessionId", sessionId); + } + if (loginName) { + paramsToContinue.append("loginName", loginName); + } + if (organization) { + paramsToContinue.append("organization", organization); + } + if (requestId) { + paramsToContinue.append("requestId", requestId); + } + + return router.push(`/u2f?` + paramsToContinue); + } else { + const url = + requestId && sessionId + ? await getNextUrl( + { + sessionId: sessionId, + requestId: requestId, + organization: organization, + }, + loginSettings?.defaultRedirectUri, + ) + : loginName + ? await getNextUrl( + { + loginName: loginName, + organization: organization, + }, + loginSettings?.defaultRedirectUri, + ) + : null; + if (url) { + return router.push(url); + } + } + } + } + + return ( +
+ {error && ( +
+ {error} +
+ )} + +
+ + + + +
+
+ ); +} diff --git a/apps/login/src/components/self-service-menu.tsx b/apps/login/src/components/self-service-menu.tsx new file mode 100644 index 0000000000..511c14c0ab --- /dev/null +++ b/apps/login/src/components/self-service-menu.tsx @@ -0,0 +1,42 @@ +import Link from "next/link"; + +export function SelfServiceMenu() { + const list: any[] = []; + + // if (!!config.selfservice.change_password.enabled) { + // list.push({ + // link: + // `/me/change-password?` + + // new URLSearchParams({ + // sessionId: sessionId, + // }), + // name: "Change password", + // }); + // } + + return ( +
+ {list.map((menuitem, index) => { + return ( + + ); + })} +
+ ); +} + +const SelfServiceItem = ({ name, link }: { name: string; link: string }) => { + return ( + + {name} + + ); +}; diff --git a/apps/login/src/components/session-clear-item.tsx b/apps/login/src/components/session-clear-item.tsx new file mode 100644 index 0000000000..4f305b1bd7 --- /dev/null +++ b/apps/login/src/components/session-clear-item.tsx @@ -0,0 +1,106 @@ +"use client"; + +import { clearSession } from "@/lib/server/session"; +import { timestampDate } from "@zitadel/client"; +import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; +import moment from "moment"; +import { useLocale } from "next-intl"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; +import { Avatar } from "./avatar"; +import { isSessionValid } from "./session-item"; +import { Translated } from "./translated"; + +export function SessionClearItem({ + session, + reload, +}: { + session: Session; + reload: () => void; +}) { + const currentLocale = useLocale(); + moment.locale(currentLocale === "zh" ? "zh-cn" : currentLocale); + + const [_loading, setLoading] = useState(false); + + async function clearSessionId(id: string) { + setLoading(true); + const response = await clearSession({ + sessionId: id, + }) + .catch((error) => { + setError(error.message); + return; + }) + .finally(() => { + setLoading(false); + }); + + return response; + } + + const { valid, verifiedAt } = isSessionValid(session); + + const [_error, setError] = useState(null); + + // TODO: To we have to call this? + useRouter(); + + return ( + + ); +} diff --git a/apps/login/src/components/session-item.tsx b/apps/login/src/components/session-item.tsx new file mode 100644 index 0000000000..41948282a1 --- /dev/null +++ b/apps/login/src/components/session-item.tsx @@ -0,0 +1,174 @@ +"use client"; + +import { sendLoginname } from "@/lib/server/loginname"; +import { clearSession, continueWithSession } from "@/lib/server/session"; +import { XCircleIcon } from "@heroicons/react/24/outline"; +import * as Tooltip from "@radix-ui/react-tooltip"; +import { Timestamp, timestampDate } from "@zitadel/client"; +import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; +import moment from "moment"; +import { useLocale } from "next-intl"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; +import { Avatar } from "./avatar"; +import { Translated } from "./translated"; + +export function isSessionValid(session: Partial): { + valid: boolean; + verifiedAt?: Timestamp; +} { + const validPassword = session?.factors?.password?.verifiedAt; + const validPasskey = session?.factors?.webAuthN?.verifiedAt; + const validIDP = session?.factors?.intent?.verifiedAt; + + const stillValid = session.expirationDate + ? timestampDate(session.expirationDate) > new Date() + : true; + + const verifiedAt = validPassword || validPasskey || validIDP; + const valid = !!((validPassword || validPasskey || validIDP) && stillValid); + + return { valid, verifiedAt }; +} + +export function SessionItem({ + session, + reload, + requestId, +}: { + session: Session; + reload: () => void; + requestId?: string; +}) { + const currentLocale = useLocale(); + moment.locale(currentLocale === "zh" ? "zh-cn" : currentLocale); + + const [_loading, setLoading] = useState(false); + + async function clearSessionId(id: string) { + setLoading(true); + const response = await clearSession({ + sessionId: id, + }) + .catch((error) => { + setError(error.message); + return; + }) + .finally(() => { + setLoading(false); + }); + + return response; + } + + const { valid, verifiedAt } = isSessionValid(session); + + const [_error, setError] = useState(null); + + const router = useRouter(); + + return ( + + + + + {valid && session.expirationDate && ( + + + Expires {moment(timestampDate(session.expirationDate)).fromNow()} + + + + )} + + ); +} diff --git a/apps/login/src/components/sessions-clear-list.tsx b/apps/login/src/components/sessions-clear-list.tsx new file mode 100644 index 0000000000..5989948725 --- /dev/null +++ b/apps/login/src/components/sessions-clear-list.tsx @@ -0,0 +1,109 @@ +"use client"; + +import { clearSession } from "@/lib/server/session"; +import { timestampDate } from "@zitadel/client"; +import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; +import { redirect, useRouter } from "next/navigation"; +import { useEffect, useState } from "react"; +import { Alert, AlertType } from "./alert"; +import { SessionClearItem } from "./session-clear-item"; +import { Translated } from "./translated"; + +type Props = { + sessions: Session[]; + postLogoutRedirectUri?: string; + logoutHint?: string; + organization?: string; +}; + +export function SessionsClearList({ + sessions, + logoutHint, + postLogoutRedirectUri, + organization, +}: Props) { + const [list, setList] = useState(sessions); + const router = useRouter(); + + async function clearHintedSession() { + console.log("Clearing session for login hint:", logoutHint); + // If a login hint is provided, we logout that specific session + const sessionIdToBeCleared = sessions.find((session) => { + return session.factors?.user?.loginName === logoutHint; + })?.id; + + if (sessionIdToBeCleared) { + const clearSessionResponse = await clearSession({ + sessionId: sessionIdToBeCleared, + }).catch((error) => { + console.error("Error clearing session:", error); + return; + }); + + if (!clearSessionResponse) { + console.error("Failed to clear session for login hint:", logoutHint); + } + + if (postLogoutRedirectUri) { + return redirect(postLogoutRedirectUri); + } + + const params = new URLSearchParams(); + + if (organization) { + params.set("organization", organization); + } + + return router.push("/logout/success?" + params); + } else { + console.warn(`No session found for login hint: ${logoutHint}`); + } + } + + useEffect(() => { + if (logoutHint) { + clearHintedSession(); + } + }, []); + + return sessions ? ( +
+ {list + .filter((session) => session?.factors?.user?.loginName) + // sort by change date descending + .sort((a, b) => { + const dateA = a.changeDate + ? timestampDate(a.changeDate).getTime() + : 0; + const dateB = b.changeDate + ? timestampDate(b.changeDate).getTime() + : 0; + return dateB - dateA; + }) + // TODO: add sorting to move invalid sessions to the bottom + .map((session, index) => { + return ( + { + setList(list.filter((s) => s.id !== session.id)); + if (postLogoutRedirectUri) { + router.push(postLogoutRedirectUri); + } + }} + key={"session-" + index} + /> + ); + })} + {list.length === 0 && ( + + + + )} +
+ ) : ( + + + + ); +} diff --git a/apps/login/src/components/sessions-list.tsx b/apps/login/src/components/sessions-list.tsx new file mode 100644 index 0000000000..a3a1f8ed94 --- /dev/null +++ b/apps/login/src/components/sessions-list.tsx @@ -0,0 +1,50 @@ +"use client"; + +import { timestampDate } from "@zitadel/client"; +import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; +import { useState } from "react"; +import { Alert } from "./alert"; +import { SessionItem } from "./session-item"; +import { Translated } from "./translated"; + +type Props = { + sessions: Session[]; + requestId?: string; +}; + +export function SessionsList({ sessions, requestId }: Props) { + const [list, setList] = useState(sessions); + return sessions ? ( +
+ {list + .filter((session) => session?.factors?.user?.loginName) + // sort by change date descending + .sort((a, b) => { + const dateA = a.changeDate + ? timestampDate(a.changeDate).getTime() + : 0; + const dateB = b.changeDate + ? timestampDate(b.changeDate).getTime() + : 0; + return dateB - dateA; + }) + // TODO: add sorting to move invalid sessions to the bottom + .map((session, index) => { + return ( + { + setList(list.filter((s) => s.id !== session.id)); + }} + key={"session-" + index} + /> + ); + })} +
+ ) : ( + + + + ); +} diff --git a/apps/login/src/components/set-password-form.tsx b/apps/login/src/components/set-password-form.tsx new file mode 100644 index 0000000000..83e8849124 --- /dev/null +++ b/apps/login/src/components/set-password-form.tsx @@ -0,0 +1,289 @@ +"use client"; + +import { + lowerCaseValidator, + numberValidator, + symbolValidator, + upperCaseValidator, +} from "@/helpers/validators"; +import { + changePassword, + resetPassword, + sendPassword, +} from "@/lib/server/password"; +import { create } from "@zitadel/client"; +import { ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; +import { PasswordComplexitySettings } from "@zitadel/proto/zitadel/settings/v2/password_settings_pb"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; +import { useTranslations } from "next-intl"; +import { FieldValues, useForm } from "react-hook-form"; +import { Alert, AlertType } from "./alert"; +import { BackButton } from "./back-button"; +import { Button, ButtonVariants } from "./button"; +import { TextInput } from "./input"; +import { PasswordComplexity } from "./password-complexity"; +import { Spinner } from "./spinner"; +import { Translated } from "./translated"; + +type Inputs = + | { + code: string; + password: string; + confirmPassword: string; + } + | FieldValues; + +type Props = { + code?: string; + passwordComplexitySettings: PasswordComplexitySettings; + loginName: string; + userId: string; + organization?: string; + requestId?: string; + codeRequired: boolean; +}; + +export function SetPasswordForm({ + passwordComplexitySettings, + organization, + requestId, + loginName, + userId, + code, + codeRequired, +}: Props) { + const { register, handleSubmit, watch, formState } = useForm({ + mode: "onBlur", + defaultValues: { + code: code ?? "", + }, + }); + + const t = useTranslations("password"); + + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + + const router = useRouter(); + + async function resendCode() { + setError(""); + setLoading(true); + + const response = await resetPassword({ + loginName, + organization, + requestId, + }) + .catch(() => { + setError("Could not reset password"); + return; + }) + .finally(() => { + setLoading(false); + }); + + if (response && "error" in response) { + setError(response.error); + return; + } + } + + async function submitPassword(values: Inputs) { + setLoading(true); + let payload: { userId: string; password: string; code?: string } = { + userId: userId, + password: values.password, + }; + + // this is not required for initial password setup + if (codeRequired) { + payload = { ...payload, code: values.code }; + } + + const changeResponse = await changePassword(payload) + .catch(() => { + setError("Could not set password"); + return; + }) + .finally(() => { + setLoading(false); + }); + + if (changeResponse && "error" in changeResponse) { + setError(changeResponse.error); + return; + } + + if (!changeResponse) { + setError("Could not set password"); + return; + } + + const params = new URLSearchParams({}); + + if (loginName) { + params.append("loginName", loginName); + } + if (organization) { + params.append("organization", organization); + } + + await new Promise((resolve) => setTimeout(resolve, 2000)); // Wait for a second to avoid eventual consistency issues with an initial password being set + + const passwordResponse = await sendPassword({ + loginName, + organization, + checks: create(ChecksSchema, { + password: { password: values.password }, + }), + requestId, + }) + .catch(() => { + setError("Could not verify password"); + return; + }) + .finally(() => { + setLoading(false); + }); + + if ( + passwordResponse && + "error" in passwordResponse && + passwordResponse.error + ) { + setError(passwordResponse.error); + return; + } + + if ( + passwordResponse && + "redirect" in passwordResponse && + passwordResponse.redirect + ) { + return router.push(passwordResponse.redirect); + } + + return; + } + + const { errors } = formState; + + const watchPassword = watch("password", ""); + const watchConfirmPassword = watch("confirmPassword", ""); + + const hasMinLength = + passwordComplexitySettings && + watchPassword?.length >= passwordComplexitySettings.minLength; + const hasSymbol = symbolValidator(watchPassword); + const hasNumber = numberValidator(watchPassword); + const hasUppercase = upperCaseValidator(watchPassword); + const hasLowercase = lowerCaseValidator(watchPassword); + + const policyIsValid = + passwordComplexitySettings && + (passwordComplexitySettings.requiresLowercase ? hasLowercase : true) && + (passwordComplexitySettings.requiresNumber ? hasNumber : true) && + (passwordComplexitySettings.requiresUppercase ? hasUppercase : true) && + (passwordComplexitySettings.requiresSymbol ? hasSymbol : true) && + hasMinLength; + + return ( +
+
+ {codeRequired && ( + +
+ + + + +
+
+ )} + {codeRequired && ( +
+ +
+ )} +
+ +
+
+ +
+
+ + {passwordComplexitySettings && ( + + )} + + {error && {error}} + +
+ + +
+ + ); +} diff --git a/apps/login/src/components/set-register-password-form.tsx b/apps/login/src/components/set-register-password-form.tsx new file mode 100644 index 0000000000..dc61c67311 --- /dev/null +++ b/apps/login/src/components/set-register-password-form.tsx @@ -0,0 +1,173 @@ +"use client"; + +import { + lowerCaseValidator, + numberValidator, + symbolValidator, + upperCaseValidator, +} from "@/helpers/validators"; +import { registerUser } from "@/lib/server/register"; +import { PasswordComplexitySettings } from "@zitadel/proto/zitadel/settings/v2/password_settings_pb"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; +import { useTranslations } from "next-intl"; +import { FieldValues, useForm } from "react-hook-form"; +import { Alert } from "./alert"; +import { BackButton } from "./back-button"; +import { Button, ButtonVariants } from "./button"; +import { TextInput } from "./input"; +import { PasswordComplexity } from "./password-complexity"; +import { Spinner } from "./spinner"; +import { Translated } from "./translated"; + +type Inputs = + | { + password: string; + confirmPassword: string; + } + | FieldValues; + +type Props = { + passwordComplexitySettings: PasswordComplexitySettings; + email: string; + firstname: string; + lastname: string; + organization: string; + requestId?: string; +}; + +export function SetRegisterPasswordForm({ + passwordComplexitySettings, + email, + firstname, + lastname, + organization, + requestId, +}: Props) { + const { register, handleSubmit, watch, formState } = useForm({ + mode: "onBlur", + defaultValues: { + email: email ?? "", + firstname: firstname ?? "", + lastname: lastname ?? "", + }, + }); + + const t = useTranslations("register"); + + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + + const router = useRouter(); + + async function submitRegister(values: Inputs) { + setLoading(true); + const response = await registerUser({ + email: email, + firstName: firstname, + lastName: lastname, + organization: organization, + requestId: requestId, + password: values.password, + }) + .catch(() => { + setError("Could not register user"); + return; + }) + .finally(() => { + setLoading(false); + }); + + if (response && "error" in response && response.error) { + setError(response.error); + return; + } + + if (response && "redirect" in response && response.redirect) { + return router.push(response.redirect); + } + } + + const { errors } = formState; + + const watchPassword = watch("password", ""); + const watchConfirmPassword = watch("confirmPassword", ""); + + const hasMinLength = + passwordComplexitySettings && + watchPassword?.length >= passwordComplexitySettings.minLength; + const hasSymbol = symbolValidator(watchPassword); + const hasNumber = numberValidator(watchPassword); + const hasUppercase = upperCaseValidator(watchPassword); + const hasLowercase = lowerCaseValidator(watchPassword); + + const policyIsValid = + passwordComplexitySettings && + (passwordComplexitySettings.requiresLowercase ? hasLowercase : true) && + (passwordComplexitySettings.requiresNumber ? hasNumber : true) && + (passwordComplexitySettings.requiresUppercase ? hasUppercase : true) && + (passwordComplexitySettings.requiresSymbol ? hasSymbol : true) && + hasMinLength; + + return ( +
+
+
+ +
+
+ +
+
+ + {passwordComplexitySettings && ( + + )} + + {error && {error}} + +
+ + +
+ + ); +} diff --git a/apps/login/src/components/sign-in-with-idp.tsx b/apps/login/src/components/sign-in-with-idp.tsx new file mode 100644 index 0000000000..755416f6c9 --- /dev/null +++ b/apps/login/src/components/sign-in-with-idp.tsx @@ -0,0 +1,93 @@ +"use client"; + +import { idpTypeToSlug } from "@/lib/idp"; +import { redirectToIdp } from "@/lib/server/idp"; +import { + IdentityProvider, + IdentityProviderType, +} from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; +import { ReactNode, useActionState } from "react"; +import { Alert } from "./alert"; +import { SignInWithIdentityProviderProps } from "./idps/base-button"; +import { SignInWithApple } from "./idps/sign-in-with-apple"; +import { SignInWithAzureAd } from "./idps/sign-in-with-azure-ad"; +import { SignInWithGeneric } from "./idps/sign-in-with-generic"; +import { SignInWithGithub } from "./idps/sign-in-with-github"; +import { SignInWithGitlab } from "./idps/sign-in-with-gitlab"; +import { SignInWithGoogle } from "./idps/sign-in-with-google"; +import { Translated } from "./translated"; + +export interface SignInWithIDPProps { + children?: ReactNode; + identityProviders: IdentityProvider[]; + requestId?: string; + organization?: string; + linkOnly?: boolean; +} + +export function SignInWithIdp({ + identityProviders, + requestId, + organization, + linkOnly, +}: Readonly) { + const [state, action, _isPending] = useActionState(redirectToIdp, {}); + + const renderIDPButton = (idp: IdentityProvider, index: number) => { + const { id, name, type } = idp; + + const components: Partial< + Record< + IdentityProviderType, + (props: SignInWithIdentityProviderProps) => ReactNode + > + > = { + [IdentityProviderType.APPLE]: SignInWithApple, + [IdentityProviderType.OAUTH]: SignInWithGeneric, + [IdentityProviderType.OIDC]: SignInWithGeneric, + [IdentityProviderType.GITHUB]: SignInWithGithub, + [IdentityProviderType.GITHUB_ES]: SignInWithGithub, + [IdentityProviderType.AZURE_AD]: SignInWithAzureAd, + [IdentityProviderType.GOOGLE]: (props) => ( + + ), + [IdentityProviderType.GITLAB]: SignInWithGitlab, + [IdentityProviderType.GITLAB_SELF_HOSTED]: SignInWithGitlab, + [IdentityProviderType.SAML]: SignInWithGeneric, + [IdentityProviderType.LDAP]: SignInWithGeneric, + [IdentityProviderType.JWT]: SignInWithGeneric, + }; + + const Component = components[type]; + return Component ? ( +
+ + + + + + + + ) : null; + }; + + return ( +
+

+ +

+ {!!identityProviders.length && identityProviders?.map(renderIDPButton)} + {state?.error && ( +
+ {state?.error} +
+ )} +
+ ); +} + +SignInWithIdp.displayName = "SignInWithIDP"; diff --git a/apps/login/src/components/skeleton-card.tsx b/apps/login/src/components/skeleton-card.tsx new file mode 100644 index 0000000000..80b3793e8f --- /dev/null +++ b/apps/login/src/components/skeleton-card.tsx @@ -0,0 +1,16 @@ +import { clsx } from "clsx"; + +export const SkeletonCard = ({ isLoading }: { isLoading?: boolean }) => ( +
+
+
+
+
+
+
+); diff --git a/apps/login/src/components/skeleton.tsx b/apps/login/src/components/skeleton.tsx new file mode 100644 index 0000000000..c54351f410 --- /dev/null +++ b/apps/login/src/components/skeleton.tsx @@ -0,0 +1,9 @@ +import { ReactNode } from "react"; + +export function Skeleton({ children }: { children?: ReactNode }) { + return ( +
+ {children} +
+ ); +} diff --git a/apps/login/src/components/spinner.tsx b/apps/login/src/components/spinner.tsx new file mode 100644 index 0000000000..d4f7731323 --- /dev/null +++ b/apps/login/src/components/spinner.tsx @@ -0,0 +1,22 @@ +import { FC } from "react"; + +export const Spinner: FC<{ className?: string }> = ({ className = "" }) => { + return ( + + + + + ); +}; diff --git a/apps/login/src/components/state-badge.tsx b/apps/login/src/components/state-badge.tsx new file mode 100644 index 0000000000..cbc6f32ea6 --- /dev/null +++ b/apps/login/src/components/state-badge.tsx @@ -0,0 +1,39 @@ +import { clsx } from "clsx"; +import { ReactNode } from "react"; + +export enum BadgeState { + Info = "info", + Error = "error", + Success = "success", + Alert = "alert", +} + +export type StateBadgeProps = { + state: BadgeState; + children: ReactNode; + evenPadding?: boolean; +}; + +const getBadgeClasses = (state: BadgeState, evenPadding: boolean) => + clsx({ + "w-fit border-box h-18.5px flex flex-row items-center whitespace-nowrap tracking-wider leading-4 items-center justify-center px-2 py-2px text-12px rounded-full shadow-sm": true, + "bg-state-success-light-background text-state-success-light-color dark:bg-state-success-dark-background dark:text-state-success-dark-color ": + state === BadgeState.Success, + "bg-state-neutral-light-background text-state-neutral-light-color dark:bg-state-neutral-dark-background dark:text-state-neutral-dark-color": + state === BadgeState.Info, + "bg-state-error-light-background text-state-error-light-color dark:bg-state-error-dark-background dark:text-state-error-dark-color": + state === BadgeState.Error, + "bg-state-alert-light-background text-state-alert-light-color dark:bg-state-alert-dark-background dark:text-state-alert-dark-color": + state === BadgeState.Alert, + "p-[2px]": evenPadding, + }); + +export function StateBadge({ + state = BadgeState.Success, + evenPadding = false, + children, +}: StateBadgeProps) { + return ( + {children} + ); +} diff --git a/apps/login/src/components/tab-group.tsx b/apps/login/src/components/tab-group.tsx new file mode 100644 index 0000000000..afa625d345 --- /dev/null +++ b/apps/login/src/components/tab-group.tsx @@ -0,0 +1,16 @@ +import { Tab } from "@/components/tab"; + +export type Item = { + text: string; + slug?: string; +}; + +export const TabGroup = ({ path, items }: { path: string; items: Item[] }) => { + return ( +
+ {items.map((item) => ( + + ))} +
+ ); +}; diff --git a/apps/login/src/components/tab.tsx b/apps/login/src/components/tab.tsx new file mode 100644 index 0000000000..f26652de96 --- /dev/null +++ b/apps/login/src/components/tab.tsx @@ -0,0 +1,35 @@ +"use client"; + +import type { Item } from "@/components/tab-group"; +import { clsx } from "clsx"; +import Link from "next/link"; +import { useSelectedLayoutSegment } from "next/navigation"; + +export const Tab = ({ + path, + item: { slug, text }, +}: { + path: string; + item: Item; +}) => { + const segment = useSelectedLayoutSegment(); + const href = slug ? path + "/" + slug : path; + const isActive = + // Example home pages e.g. `/layouts` + (!slug && segment === null) || + // Nested pages e.g. `/layouts/electronics` + segment === slug; + + return ( + + {text} + + ); +}; diff --git a/apps/login/src/components/theme-provider.tsx b/apps/login/src/components/theme-provider.tsx new file mode 100644 index 0000000000..a8a72f86a6 --- /dev/null +++ b/apps/login/src/components/theme-provider.tsx @@ -0,0 +1,16 @@ +"use client"; +import { ThemeProvider as ThemeP } from "next-themes"; +import { ReactNode } from "react"; + +export function ThemeProvider({ children }: { children: ReactNode }) { + return ( + + {children} + + ); +} diff --git a/apps/login/src/components/theme-wrapper.tsx b/apps/login/src/components/theme-wrapper.tsx new file mode 100644 index 0000000000..314c3a2ef0 --- /dev/null +++ b/apps/login/src/components/theme-wrapper.tsx @@ -0,0 +1,18 @@ +"use client"; + +import { setTheme } from "@/helpers/colors"; +import { BrandingSettings } from "@zitadel/proto/zitadel/settings/v2/branding_settings_pb"; +import { ReactNode, useEffect } from "react"; + +type Props = { + branding: BrandingSettings | undefined; + children: ReactNode; +}; + +export const ThemeWrapper = ({ children, branding }: Props) => { + useEffect(() => { + setTheme(document, branding); + }, [branding]); + + return
{children}
; +}; diff --git a/apps/login/src/components/theme.tsx b/apps/login/src/components/theme.tsx new file mode 100644 index 0000000000..fecaabebee --- /dev/null +++ b/apps/login/src/components/theme.tsx @@ -0,0 +1,44 @@ +"use client"; + +import { MoonIcon, SunIcon } from "@heroicons/react/24/outline"; +import { useTheme } from "next-themes"; +import { useEffect, useState } from "react"; + +export function Theme() { + const { resolvedTheme, setTheme } = useTheme(); + const [mounted, setMounted] = useState(false); + + const isDark = resolvedTheme === "dark"; + + // useEffect only runs on the client, so now we can safely show the UI + useEffect(() => { + setMounted(true); + }, []); + + if (!mounted) { + return null; + } + + return ( +
+ + +
+ ); +} diff --git a/apps/login/src/components/totp-register.tsx b/apps/login/src/components/totp-register.tsx new file mode 100644 index 0000000000..ec9f630f91 --- /dev/null +++ b/apps/login/src/components/totp-register.tsx @@ -0,0 +1,159 @@ +"use client"; + +import { getNextUrl } from "@/lib/client"; +import { verifyTOTP } from "@/lib/server/verify"; +import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { QRCodeSVG } from "qrcode.react"; +import { useState } from "react"; +import { useTranslations } from "next-intl"; +import { useForm } from "react-hook-form"; +import { Alert } from "./alert"; +import { Button, ButtonVariants } from "./button"; +import { CopyToClipboard } from "./copy-to-clipboard"; +import { TextInput } from "./input"; +import { Spinner } from "./spinner"; +import { Translated } from "./translated"; + +type Inputs = { + code: string; +}; + +type Props = { + uri: string; + secret: string; + loginName?: string; + sessionId?: string; + requestId?: string; + organization?: string; + checkAfter?: boolean; + loginSettings?: LoginSettings; +}; +export function TotpRegister({ + uri, + loginName, + sessionId, + requestId, + organization, + checkAfter, + loginSettings, +}: Props) { + const [error, setError] = useState(""); + const [loading, setLoading] = useState(false); + const router = useRouter(); + + const { register, handleSubmit, formState } = useForm({ + mode: "onBlur", + defaultValues: { + code: "", + }, + }); + + const t = useTranslations("otp"); + + async function continueWithCode(values: Inputs) { + setLoading(true); + return verifyTOTP(values.code, loginName, organization) + .then(async () => { + // if attribute is set, validate MFA after it is setup, otherwise proceed as usual (when mfa is enforced to login) + if (checkAfter) { + const params = new URLSearchParams({}); + + if (loginName) { + params.append("loginName", loginName); + } + if (requestId) { + params.append("requestId", requestId); + } + if (organization) { + params.append("organization", organization); + } + + return router.push(`/otp/time-based?` + params); + } else { + const url = + requestId && sessionId + ? await getNextUrl( + { + sessionId: sessionId, + requestId: requestId, + organization: organization, + }, + loginSettings?.defaultRedirectUri, + ) + : loginName + ? await getNextUrl( + { + loginName: loginName, + organization: organization, + }, + loginSettings?.defaultRedirectUri, + ) + : null; + + if (url) { + return router.push(url); + } + } + }) + .catch((e) => { + setError(e.message); + return; + }) + .finally(() => { + setLoading(false); + }); + } + + return ( +
+ {uri && ( + <> + +
+ + {uri} + + + +
+
+
+ +
+ + {error && ( +
+ {error} +
+ )} + +
+ + +
+
+ + )} +
+ ); +} diff --git a/apps/login/src/components/translated.tsx b/apps/login/src/components/translated.tsx new file mode 100644 index 0000000000..2a9f5c1329 --- /dev/null +++ b/apps/login/src/components/translated.tsx @@ -0,0 +1,22 @@ +import { useTranslations } from "next-intl"; + +export function Translated({ + i18nKey, + namespace, + data, + ...props +}: { + i18nKey: string; + children?: React.ReactNode; + namespace?: string; + data?: any; +} & React.HTMLAttributes) { + const t = useTranslations(namespace); + const helperKey = `${namespace ? `${namespace}.` : ""}${i18nKey}`; + + return ( + + {t(i18nKey, data)} + + ); +} diff --git a/apps/login/src/components/user-avatar.tsx b/apps/login/src/components/user-avatar.tsx new file mode 100644 index 0000000000..8a26ba187a --- /dev/null +++ b/apps/login/src/components/user-avatar.tsx @@ -0,0 +1,59 @@ +import { Avatar } from "@/components/avatar"; +import { ChevronDownIcon } from "@heroicons/react/24/outline"; +import Link from "next/link"; + +type Props = { + loginName?: string; + displayName?: string; + showDropdown: boolean; + searchParams?: Record; +}; + +export function UserAvatar({ + loginName, + displayName, + showDropdown, + searchParams, +}: Props) { + const params = new URLSearchParams({}); + + if (searchParams?.sessionId) { + params.set("sessionId", searchParams.sessionId); + } + + if (searchParams?.organization) { + params.set("organization", searchParams.organization); + } + + if (searchParams?.requestId) { + params.set("requestId", searchParams.requestId); + } + + if (searchParams?.loginName) { + params.set("loginName", searchParams.loginName); + } + + return ( +
+
+ +
+ + {loginName} + + + {showDropdown && ( + + + + )} +
+ ); +} diff --git a/apps/login/src/components/username-form.tsx b/apps/login/src/components/username-form.tsx new file mode 100644 index 0000000000..38eeffff98 --- /dev/null +++ b/apps/login/src/components/username-form.tsx @@ -0,0 +1,157 @@ +"use client"; + +import { sendLoginname } from "@/lib/server/loginname"; +import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; +import { useRouter } from "next/navigation"; +import { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { Alert } from "./alert"; +import { BackButton } from "./back-button"; +import { Button, ButtonVariants } from "./button"; +import { TextInput } from "./input"; +import { Spinner } from "./spinner"; +import { Translated } from "./translated"; +import { useTranslations } from "next-intl"; + +type Inputs = { + loginName: string; +}; + +type Props = { + loginName: string | undefined; + requestId: string | undefined; + loginSettings: LoginSettings | undefined; + organization?: string; + suffix?: string; + submit: boolean; + allowRegister: boolean; +}; + +export function UsernameForm({ + loginName, + requestId, + organization, + suffix, + loginSettings, + submit, + allowRegister, +}: Props) { + const { register, handleSubmit, formState } = useForm({ + mode: "onBlur", + defaultValues: { + loginName: loginName ? loginName : "", + }, + }); + + const t = useTranslations("loginname"); + + const router = useRouter(); + + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + + async function submitLoginName(values: Inputs, organization?: string) { + setLoading(true); + + const res = await sendLoginname({ + loginName: values.loginName, + organization, + requestId, + suffix, + }) + .catch(() => { + setError("An internal error occurred"); + return; + }) + .finally(() => { + setLoading(false); + }); + + if (res && "redirect" in res && res.redirect) { + return router.push(res.redirect); + } + + if (res && "error" in res && res.error) { + setError(res.error); + return; + } + + return res; + } + + useEffect(() => { + if (submit && loginName) { + // When we navigate to this page, we always want to be redirected if submit is true and the parameters are valid. + submitLoginName({ loginName }, organization); + } + }, []); + + let inputLabel = "Loginname"; + if ( + loginSettings?.disableLoginWithEmail && + loginSettings?.disableLoginWithPhone + ) { + inputLabel = "Username"; + } else if (loginSettings?.disableLoginWithEmail) { + inputLabel = "Username or phone number"; + } else if (loginSettings?.disableLoginWithPhone) { + inputLabel = "Username or email"; + } + + return ( +
+
+ + {allowRegister && ( + + )} +
+ + {error && ( +
+ {error} +
+ )} +
+ + + +
+
+ ); +} diff --git a/apps/login/src/components/verify-form.tsx b/apps/login/src/components/verify-form.tsx new file mode 100644 index 0000000000..ada7501196 --- /dev/null +++ b/apps/login/src/components/verify-form.tsx @@ -0,0 +1,171 @@ +"use client"; + +import { Alert, AlertType } from "@/components/alert"; +import { resendVerification, sendVerification } from "@/lib/server/verify"; +import { useRouter } from "next/navigation"; +import { useCallback, useEffect, useState } from "react"; +import { useTranslations } from "next-intl"; +import { useForm } from "react-hook-form"; +import { BackButton } from "./back-button"; +import { Button, ButtonVariants } from "./button"; +import { TextInput } from "./input"; +import { Spinner } from "./spinner"; +import { Translated } from "./translated"; + +type Inputs = { + code: string; +}; + +type Props = { + userId: string; + loginName?: string; + organization?: string; + code?: string; + isInvite: boolean; + requestId?: string; +}; + +export function VerifyForm({ + userId, + loginName, + organization, + requestId, + code, + isInvite, +}: Props) { + const router = useRouter(); + + const { register, handleSubmit, formState } = useForm({ + mode: "onBlur", + defaultValues: { + code: code ?? "", + }, + }); + + const t = useTranslations("verify"); + + const [error, setError] = useState(""); + + const [loading, setLoading] = useState(false); + + async function resendCode() { + setError(""); + setLoading(true); + + const response = await resendVerification({ + userId, + isInvite: isInvite, + }) + .catch(() => { + setError("Could not resend email"); + return; + }) + .finally(() => { + setLoading(false); + }); + + if (response && "error" in response && response?.error) { + setError(response.error); + return; + } + + return response; + } + + const fcn = useCallback( + async function submitCodeAndContinue( + value: Inputs, + ): Promise { + setLoading(true); + + const response = await sendVerification({ + code: value.code, + userId, + isInvite: isInvite, + loginName: loginName, + organization: organization, + requestId: requestId, + }) + .catch(() => { + setError("Could not verify user"); + return; + }) + .finally(() => { + setLoading(false); + }); + + if (response && "error" in response && response?.error) { + setError(response.error); + return; + } + + if (response && "redirect" in response && response?.redirect) { + return router.push(response?.redirect); + } + }, + [isInvite, userId], + ); + + useEffect(() => { + if (code) { + fcn({ code }); + } + }, [code, fcn]); + + return ( + <> +
+ +
+ + + + +
+
+
+ +
+ + {error && ( +
+ {error} +
+ )} + +
+ + + +
+
+ + ); +} diff --git a/apps/login/src/components/zitadel-logo-dark.tsx b/apps/login/src/components/zitadel-logo-dark.tsx new file mode 100644 index 0000000000..c87d190839 --- /dev/null +++ b/apps/login/src/components/zitadel-logo-dark.tsx @@ -0,0 +1,210 @@ +import { FC } from "react"; + +export const ZitadelLogoDark: FC = () => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +); diff --git a/apps/login/src/components/zitadel-logo-light.tsx b/apps/login/src/components/zitadel-logo-light.tsx new file mode 100644 index 0000000000..f389dd5eb1 --- /dev/null +++ b/apps/login/src/components/zitadel-logo-light.tsx @@ -0,0 +1,210 @@ +import { FC } from "react"; + +export const ZitadelLogoLight: FC = () => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +); diff --git a/apps/login/src/components/zitadel-logo.tsx b/apps/login/src/components/zitadel-logo.tsx new file mode 100644 index 0000000000..105665fbba --- /dev/null +++ b/apps/login/src/components/zitadel-logo.tsx @@ -0,0 +1,32 @@ +import Image from "next/image"; +type Props = { + height?: number; + width?: number; +}; + +export function ZitadelLogo({ height = 40, width = 147.5 }: Props) { + return ( + <> +
+ {/* */} + + zitadel logo +
+
+ zitadel logo +
+ + ); +} diff --git a/apps/login/src/helpers/base64.ts b/apps/login/src/helpers/base64.ts new file mode 100644 index 0000000000..967cdc8d17 --- /dev/null +++ b/apps/login/src/helpers/base64.ts @@ -0,0 +1,63 @@ +export function coerceToBase64Url(thing: any, name: string) { + // Array or ArrayBuffer to Uint8Array + if (Array.isArray(thing)) { + thing = Uint8Array.from(thing); + } + + if (thing instanceof ArrayBuffer) { + thing = new Uint8Array(thing); + } + + // Uint8Array to base64 + if (thing instanceof Uint8Array) { + var str = ""; + var len = thing.byteLength; + + for (var i = 0; i < len; i++) { + str += String.fromCharCode(thing[i]); + } + thing = window.btoa(str); + } + + if (typeof thing !== "string") { + throw new Error("could not coerce '" + name + "' to string"); + } + + // base64 to base64url + // NOTE: "=" at the end of challenge is optional, strip it off here + thing = thing.replace(/\+/g, "-").replace(/\//g, "_").replace(/=*$/g, ""); + + return thing; +} + +export function coerceToArrayBuffer(thing: any, name: string) { + if (typeof thing === "string") { + // base64url to base64 + thing = thing.replace(/-/g, "+").replace(/_/g, "/"); + + // base64 to Uint8Array + var str = window.atob(thing); + var bytes = new Uint8Array(str.length); + for (var i = 0; i < str.length; i++) { + bytes[i] = str.charCodeAt(i); + } + thing = bytes; + } + + // Array to Uint8Array + if (Array.isArray(thing)) { + thing = new Uint8Array(thing); + } + + // Uint8Array to ArrayBuffer + if (thing instanceof Uint8Array) { + thing = thing.buffer; + } + + // error if none of the above worked + if (!(thing instanceof ArrayBuffer)) { + throw new TypeError("could not coerce '" + name + "' to ArrayBuffer"); + } + + return thing; +} diff --git a/apps/login/src/helpers/colors.ts b/apps/login/src/helpers/colors.ts new file mode 100644 index 0000000000..3c833cf407 --- /dev/null +++ b/apps/login/src/helpers/colors.ts @@ -0,0 +1,439 @@ +import { BrandingSettings } from "@zitadel/proto/zitadel/settings/v2/branding_settings_pb"; +import tinycolor from "tinycolor2"; + +export interface Color { + name: string; + hex: string; + rgb: string; + contrastColor: string; +} + +export type MapName = "background" | "primary" | "warn" | "text" | "link"; + +export type ColorName = + | "50" + | "100" + | "200" + | "300" + | "400" + | "500" + | "600" + | "700" + | "800" + | "C900" + | "A100" + | "A200" + | "A400" + | "A700"; + +export type ColorMap = { + [_key in MapName]: Color[]; +}; + +export const DARK_PRIMARY = "#2073c4"; +export const PRIMARY = "#5469d4"; + +export const DARK_WARN = "#ff3b5b"; +export const WARN = "#cd3d56"; + +export const DARK_BACKGROUND = "#111827"; +export const BACKGROUND = "#fafafa"; + +export const DARK_TEXT = "#ffffff"; +export const TEXT = "#000000"; + +export type LabelPolicyColors = { + backgroundColor: string; + backgroundColorDark: string; + fontColor: string; + fontColorDark: string; + warnColor: string; + warnColorDark: string; + primaryColor: string; + primaryColorDark: string; +}; + +type BrandingColors = { + lightTheme: { + backgroundColor: string; + fontColor: string; + primaryColor: string; + warnColor: string; + }; + darkTheme: { + backgroundColor: string; + fontColor: string; + primaryColor: string; + warnColor: string; + }; +}; + +export function setTheme(document: any, policy?: BrandingSettings) { + const lP: BrandingColors = { + lightTheme: { + backgroundColor: policy?.lightTheme?.backgroundColor || BACKGROUND, + fontColor: policy?.lightTheme?.fontColor || TEXT, + primaryColor: policy?.lightTheme?.primaryColor || PRIMARY, + warnColor: policy?.lightTheme?.warnColor || WARN, + }, + darkTheme: { + backgroundColor: policy?.darkTheme?.backgroundColor || DARK_BACKGROUND, + fontColor: policy?.darkTheme?.fontColor || DARK_TEXT, + primaryColor: policy?.darkTheme?.primaryColor || DARK_PRIMARY, + warnColor: policy?.darkTheme?.warnColor || DARK_WARN, + }, + }; + + const dark = computeMap(lP, true); + const light = computeMap(lP, false); + + setColorShades(dark.background, "background", "dark", document); + setColorShades(light.background, "background", "light", document); + + setColorShades(dark.primary, "primary", "dark", document); + setColorShades(light.primary, "primary", "light", document); + + setColorShades(dark.warn, "warn", "dark", document); + setColorShades(light.warn, "warn", "light", document); + + setColorAlpha(dark.text, "text", "dark", document); + setColorAlpha(light.text, "text", "light", document); + + setColorAlpha(dark.link, "link", "dark", document); + setColorAlpha(light.link, "link", "light", document); +} + +function setColorShades( + map: Color[], + type: string, + theme: string, + document: any, +) { + map.forEach((color) => { + document.documentElement.style.setProperty( + `--theme-${theme}-${type}-${color.name}`, + color.hex, + ); + document.documentElement.style.setProperty( + `--theme-${theme}-${type}-contrast-${color.name}`, + color.contrastColor, + ); + }); +} + +function setColorAlpha( + map: Color[], + type: string, + theme: string, + document: any, +) { + map.forEach((color) => { + document.documentElement.style.setProperty( + `--theme-${theme}-${type}-${color.name}`, + color.hex, + ); + document.documentElement.style.setProperty( + `--theme-${theme}-${type}-contrast-${color.name}`, + color.contrastColor, + ); + document.documentElement.style.setProperty( + `--theme-${theme}-${type}-secondary-${color.name}`, + `${color.hex}c7`, + ); + }); +} + +function computeColors(hex: string): Color[] { + return [ + getColorObject(tinycolor(hex).lighten(52), "50"), + getColorObject(tinycolor(hex).lighten(37), "100"), + getColorObject(tinycolor(hex).lighten(26), "200"), + getColorObject(tinycolor(hex).lighten(12), "300"), + getColorObject(tinycolor(hex).lighten(6), "400"), + getColorObject(tinycolor(hex), "500"), + getColorObject(tinycolor(hex).darken(6), "600"), + getColorObject(tinycolor(hex).darken(12), "700"), + getColorObject(tinycolor(hex).darken(18), "800"), + getColorObject(tinycolor(hex).darken(24), "900"), + getColorObject(tinycolor(hex).lighten(50).saturate(30), "A100"), + getColorObject(tinycolor(hex).lighten(30).saturate(30), "A200"), + getColorObject(tinycolor(hex).lighten(10).saturate(15), "A400"), + getColorObject(tinycolor(hex).lighten(5).saturate(5), "A700"), + ]; +} + +function getColorObject(value: any, name: string): Color { + const c = tinycolor(value); + return { + name: name, + hex: c.toHexString(), + rgb: c.toRgbString(), + contrastColor: getContrast(c.toHexString()), + } as Color; +} + +function getContrast(color: string): string { + const onBlack = tinycolor.readability("#000", color); + const onWhite = tinycolor.readability("#fff", color); + if (onBlack > onWhite) { + return "hsla(0, 0%, 0%, 0.87)"; + } else { + return "#ffffff"; + } +} + +export function computeMap(branding: BrandingColors, dark: boolean): ColorMap { + return { + background: computeColors( + dark + ? branding.darkTheme.backgroundColor + : branding.lightTheme.backgroundColor, + ), + primary: computeColors( + dark ? branding.darkTheme.primaryColor : branding.lightTheme.primaryColor, + ), + warn: computeColors( + dark ? branding.darkTheme.warnColor : branding.lightTheme.warnColor, + ), + text: computeColors( + dark ? branding.darkTheme.fontColor : branding.lightTheme.fontColor, + ), + link: computeColors( + dark ? branding.darkTheme.fontColor : branding.lightTheme.fontColor, + ), + }; +} + +export interface ColorShade { + 200: string; + 300: string; + 500: string; + 600: string; + 700: string; + 900: string; +} + +export const COLORS = [ + { + 500: "#ef4444", + 200: "#fecaca", + 300: "#fca5a5", + 600: "#dc2626", + 700: "#b91c1c", + 900: "#7f1d1d", + }, + { + 500: "#f97316", + 200: "#fed7aa", + 300: "#fdba74", + 600: "#ea580c", + 700: "#c2410c", + 900: "#7c2d12", + }, + { + 500: "#f59e0b", + 200: "#fde68a", + 300: "#fcd34d", + 600: "#d97706", + 700: "#b45309", + 900: "#78350f", + }, + { + 500: "#eab308", + 200: "#fef08a", + 300: "#fde047", + 600: "#ca8a04", + 700: "#a16207", + 900: "#713f12", + }, + { + 500: "#84cc16", + 200: "#d9f99d", + 300: "#bef264", + 600: "#65a30d", + 700: "#4d7c0f", + 900: "#365314", + }, + { + 500: "#22c55e", + 200: "#bbf7d0", + 300: "#86efac", + 600: "#16a34a", + 700: "#15803d", + 900: "#14532d", + }, + { + 500: "#10b981", + 200: "#a7f3d0", + 300: "#6ee7b7", + 600: "#059669", + 700: "#047857", + 900: "#064e3b", + }, + { + 500: "#14b8a6", + 200: "#99f6e4", + 300: "#5eead4", + 600: "#0d9488", + 700: "#0f766e", + 900: "#134e4a", + }, + { + 500: "#06b6d4", + 200: "#a5f3fc", + 300: "#67e8f9", + 600: "#0891b2", + 700: "#0e7490", + 900: "#164e63", + }, + { + 500: "#0ea5e9", + 200: "#bae6fd", + 300: "#7dd3fc", + 600: "#0284c7", + 700: "#0369a1", + 900: "#0c4a6e", + }, + { + 500: "#3b82f6", + 200: "#bfdbfe", + 300: "#93c5fd", + 600: "#2563eb", + 700: "#1d4ed8", + 900: "#1e3a8a", + }, + { + 500: "#6366f1", + 200: "#c7d2fe", + 300: "#a5b4fc", + 600: "#4f46e5", + 700: "#4338ca", + 900: "#312e81", + }, + { + 500: "#8b5cf6", + 200: "#ddd6fe", + 300: "#c4b5fd", + 600: "#7c3aed", + 700: "#6d28d9", + 900: "#4c1d95", + }, + { + 500: "#a855f7", + 200: "#e9d5ff", + 300: "#d8b4fe", + 600: "#9333ea", + 700: "#7e22ce", + 900: "#581c87", + }, + { + 500: "#d946ef", + 200: "#f5d0fe", + 300: "#f0abfc", + 600: "#c026d3", + 700: "#a21caf", + 900: "#701a75", + }, + { + 500: "#ec4899", + 200: "#fbcfe8", + 300: "#f9a8d4", + 600: "#db2777", + 700: "#be185d", + 900: "#831843", + }, + { + 500: "#f43f5e", + 200: "#fecdd3", + 300: "#fda4af", + 600: "#e11d48", + 700: "#be123c", + 900: "#881337", + }, +]; + +export function getColorHash(value: string): ColorShade { + let hash = 0; + + if (value.length === 0) { + return COLORS[hash]; + } + + hash = hashCode(value); + return COLORS[hash % COLORS.length]; +} + +export function hashCode(str: string, seed = 0): number { + let h1 = 0xdeadbeef ^ seed, + h2 = 0x41c6ce57 ^ seed; + for (let i = 0, ch; i < str.length; i++) { + ch = str.charCodeAt(i); + h1 = Math.imul(h1 ^ ch, 2654435761); + h2 = Math.imul(h2 ^ ch, 1597334677); + } + h1 = + Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ + Math.imul(h2 ^ (h2 >>> 13), 3266489909); + h2 = + Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ + Math.imul(h1 ^ (h1 >>> 13), 3266489909); + return 4294967296 * (2097151 & h2) + (h1 >>> 0); +} + +export function getMembershipColor(role: string): ColorShade { + const hash = hashCode(role); + let color = COLORS[hash % COLORS.length]; + + switch (role) { + case "IAM_OWNER": + color = COLORS[0]; + break; + case "IAM_OWNER_VIEWER": + color = COLORS[14]; + break; + case "IAM_ORG_MANAGER": + color = COLORS[11]; + break; + case "IAM_USER_MANAGER": + color = COLORS[8]; + break; + + case "ORG_OWNER": + color = COLORS[16]; + break; + case "ORG_USER_MANAGER": + color = COLORS[8]; + break; + case "ORG_OWNER_VIEWER": + color = COLORS[14]; + break; + case "ORG_USER_PERMISSION_EDITOR": + color = COLORS[7]; + break; + case "ORG_PROJECT_PERMISSION_EDITOR": + color = COLORS[11]; + break; + case "ORG_PROJECT_CREATOR": + color = COLORS[12]; + break; + + case "PROJECT_OWNER": + color = COLORS[9]; + break; + case "PROJECT_OWNER_VIEWER": + color = COLORS[10]; + break; + case "PROJECT_OWNER_GLOBAL": + color = COLORS[11]; + break; + case "PROJECT_OWNER_VIEWER_GLOBAL": + color = COLORS[12]; + break; + + default: + color = COLORS[hash % COLORS.length]; + break; + } + + return color; +} diff --git a/apps/login/src/helpers/validators.ts b/apps/login/src/helpers/validators.ts new file mode 100644 index 0000000000..6a61d13ece --- /dev/null +++ b/apps/login/src/helpers/validators.ts @@ -0,0 +1,19 @@ +export function symbolValidator(value: string): boolean { + const REGEXP = /[^a-zA-Z0-9]/gi; + return REGEXP.test(value); +} + +export function numberValidator(value: string): boolean { + const REGEXP = /[0-9]/g; + return REGEXP.test(value); +} + +export function upperCaseValidator(value: string): boolean { + const REGEXP = /[A-Z]/g; + return REGEXP.test(value); +} + +export function lowerCaseValidator(value: string): boolean { + const REGEXP = /[a-z]/g; + return REGEXP.test(value); +} diff --git a/apps/login/src/i18n/request.ts b/apps/login/src/i18n/request.ts new file mode 100644 index 0000000000..5ddf7da97c --- /dev/null +++ b/apps/login/src/i18n/request.ts @@ -0,0 +1,63 @@ +import { LANGS, LANGUAGE_COOKIE_NAME, LANGUAGE_HEADER_NAME } from "@/lib/i18n"; +import { getServiceUrlFromHeaders } from "@/lib/service-url"; +import { getHostedLoginTranslation } from "@/lib/zitadel"; +import { JsonObject } from "@zitadel/client"; +import deepmerge from "deepmerge"; +import { getRequestConfig } from "next-intl/server"; +import { cookies, headers } from "next/headers"; + +export default getRequestConfig(async () => { + const fallback = "en"; + const cookiesList = await cookies(); + + let locale: string = fallback; + + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + + const languageHeader = await (await headers()).get(LANGUAGE_HEADER_NAME); + if (languageHeader) { + const headerLocale = languageHeader.split(",")[0].split("-")[0]; // Extract the language code + if (LANGS.map((l) => l.code).includes(headerLocale)) { + locale = headerLocale; + } + } + + const languageCookie = cookiesList?.get(LANGUAGE_COOKIE_NAME); + if (languageCookie && languageCookie.value) { + if (LANGS.map((l) => l.code).includes(languageCookie.value)) { + locale = languageCookie.value; + } + } + + const i18nOrganization = _headers.get("x-zitadel-i18n-organization") || ""; // You may need to set this header in middleware + + let translations: JsonObject | {} = {}; + try { + const i18nJSON = await getHostedLoginTranslation({ + serviceUrl, + locale, + organization: i18nOrganization, + }); + + if (i18nJSON) { + translations = i18nJSON; + } + } catch (error) { + console.warn("Error fetching custom translations:", error); + } + + const customMessages = translations; + const localeMessages = (await import(`../../locales/${locale}.json`)).default; + const fallbackMessages = (await import(`../../locales/${fallback}.json`)) + .default; + + return { + locale, + messages: deepmerge.all([ + fallbackMessages, + localeMessages, + customMessages, + ]) as Record, + }; +}); diff --git a/apps/login/src/lib/api.ts b/apps/login/src/lib/api.ts new file mode 100644 index 0000000000..7324007307 --- /dev/null +++ b/apps/login/src/lib/api.ts @@ -0,0 +1,17 @@ +import { newSystemToken } from "@zitadel/client/node"; + +export async function systemAPIToken() { + const token = { + audience: process.env.AUDIENCE, + userID: process.env.SYSTEM_USER_ID, + token: Buffer.from(process.env.SYSTEM_USER_PRIVATE_KEY, "base64").toString( + "utf-8", + ), + }; + + return newSystemToken({ + audience: token.audience, + subject: token.userID, + key: token.token, + }); +} diff --git a/apps/login/src/lib/client.ts b/apps/login/src/lib/client.ts new file mode 100644 index 0000000000..a59af90b77 --- /dev/null +++ b/apps/login/src/lib/client.ts @@ -0,0 +1,80 @@ +type FinishFlowCommand = + | { + sessionId: string; + requestId: string; + } + | { loginName: string }; + +function goToSignedInPage( + props: + | { sessionId: string; organization?: string; requestId?: string } + | { organization?: string; loginName: string; requestId?: string }, +) { + const params = new URLSearchParams({}); + + if ("loginName" in props && props.loginName) { + params.append("loginName", props.loginName); + } + + if ("sessionId" in props && props.sessionId) { + params.append("sessionId", props.sessionId); + } + + if (props.organization) { + params.append("organization", props.organization); + } + + // required to show conditional UI for device flow + if (props.requestId) { + params.append("requestId", props.requestId); + } + + return `/signedin?` + params; +} + +/** + * for client: redirects user back to an OIDC or SAML application or to a success page when using requestId, check if a default redirect and redirect to it, or just redirect to a success page with the loginName + * @param command + * @returns + */ +export async function getNextUrl( + command: FinishFlowCommand & { organization?: string }, + defaultRedirectUri?: string, +): Promise { + // finish Device Authorization Flow + if ( + "requestId" in command && + command.requestId.startsWith("device_") && + ("loginName" in command || "sessionId" in command) + ) { + return goToSignedInPage({ + ...command, + organization: command.organization, + }); + } + + // finish SAML or OIDC flow + if ( + "sessionId" in command && + "requestId" in command && + (command.requestId.startsWith("saml_") || + command.requestId.startsWith("oidc_")) + ) { + const params = new URLSearchParams({ + sessionId: command.sessionId, + requestId: command.requestId, + }); + + if (command.organization) { + params.append("organization", command.organization); + } + + return `/login?` + params; + } + + if (defaultRedirectUri) { + return defaultRedirectUri; + } + + return goToSignedInPage(command); +} diff --git a/apps/login/src/lib/cookies.ts b/apps/login/src/lib/cookies.ts new file mode 100644 index 0000000000..71008de24e --- /dev/null +++ b/apps/login/src/lib/cookies.ts @@ -0,0 +1,341 @@ +"use server"; + +import { timestampDate, timestampFromMs } from "@zitadel/client"; +import { cookies } from "next/headers"; +import { LANGUAGE_COOKIE_NAME } from "./i18n"; + +// TODO: improve this to handle overflow +const MAX_COOKIE_SIZE = 2048; + +export type Cookie = { + id: string; + token: string; + loginName: string; + organization?: string; + creationTs: string; + expirationTs: string; + changeTs: string; + requestId?: string; // if its linked to an OIDC flow +}; + +type SessionCookie = Cookie & T; + +async function setSessionHttpOnlyCookie( + sessions: SessionCookie[], + sameSite: boolean | "lax" | "strict" | "none" = true, +) { + const cookiesList = await cookies(); + + return cookiesList.set({ + name: "sessions", + value: JSON.stringify(sessions), + httpOnly: true, + path: "/", + sameSite: process.env.NODE_ENV === "production" ? sameSite : "lax", + secure: process.env.NODE_ENV === "production", + }); +} + +export async function setLanguageCookie(language: string) { + const cookiesList = await cookies(); + + await cookiesList.set({ + name: LANGUAGE_COOKIE_NAME, + value: language, + httpOnly: true, + path: "/", + }); +} + +export async function addSessionToCookie({ + session, + cleanup, + sameSite, +}: { + session: SessionCookie; + cleanup?: boolean; + sameSite?: boolean | "lax" | "strict" | "none" | undefined; +}): Promise { + const cookiesList = await cookies(); + const stringifiedCookie = cookiesList.get("sessions"); + + let currentSessions: SessionCookie[] = stringifiedCookie?.value + ? JSON.parse(stringifiedCookie?.value) + : []; + + const index = currentSessions.findIndex( + (s) => s.loginName === session.loginName, + ); + + if (index > -1) { + currentSessions[index] = session; + } else { + const temp = [...currentSessions, session]; + + if (JSON.stringify(temp).length >= MAX_COOKIE_SIZE) { + console.log("WARNING COOKIE OVERFLOW"); + // TODO: improve cookie handling + // this replaces the first session (oldest) with the new one + currentSessions = [session].concat(currentSessions.slice(1)); + } else { + currentSessions = [session].concat(currentSessions); + } + } + + if (cleanup) { + const now = new Date(); + const filteredSessions = currentSessions.filter((session) => + session.expirationTs + ? timestampDate(timestampFromMs(Number(session.expirationTs))) > now + : true, + ); + return setSessionHttpOnlyCookie(filteredSessions, sameSite); + } else { + return setSessionHttpOnlyCookie(currentSessions, sameSite); + } +} + +export async function updateSessionCookie({ + id, + session, + cleanup, + sameSite, +}: { + id: string; + session: SessionCookie; + cleanup?: boolean; + sameSite?: boolean | "lax" | "strict" | "none" | undefined; +}): Promise { + const cookiesList = await cookies(); + const stringifiedCookie = cookiesList.get("sessions"); + + const sessions: SessionCookie[] = stringifiedCookie?.value + ? JSON.parse(stringifiedCookie?.value) + : [session]; + + const foundIndex = sessions.findIndex((session) => session.id === id); + + if (foundIndex > -1) { + sessions[foundIndex] = session; + if (cleanup) { + const now = new Date(); + const filteredSessions = sessions.filter((session) => + session.expirationTs + ? timestampDate(timestampFromMs(Number(session.expirationTs))) > now + : true, + ); + return setSessionHttpOnlyCookie(filteredSessions, sameSite); + } else { + return setSessionHttpOnlyCookie(sessions, sameSite); + } + } else { + throw "updateSessionCookie: session id now found"; + } +} + +export async function removeSessionFromCookie({ + session, + cleanup, + sameSite, +}: { + session: SessionCookie; + cleanup?: boolean; + sameSite?: boolean | "lax" | "strict" | "none" | undefined; +}) { + const cookiesList = await cookies(); + const stringifiedCookie = cookiesList.get("sessions"); + + const sessions: SessionCookie[] = stringifiedCookie?.value + ? JSON.parse(stringifiedCookie?.value) + : [session]; + + const reducedSessions = sessions.filter((s) => s.id !== session.id); + if (cleanup) { + const now = new Date(); + const filteredSessions = reducedSessions.filter((session) => + session.expirationTs + ? timestampDate(timestampFromMs(Number(session.expirationTs))) > now + : true, + ); + return setSessionHttpOnlyCookie(filteredSessions, sameSite); + } else { + return setSessionHttpOnlyCookie(reducedSessions, sameSite); + } +} + +export async function getMostRecentSessionCookie(): Promise { + const cookiesList = await cookies(); + const stringifiedCookie = cookiesList.get("sessions"); + + if (stringifiedCookie?.value) { + const sessions: SessionCookie[] = JSON.parse(stringifiedCookie?.value); + + const latest = sessions.reduce((prev, current) => { + return prev.changeTs > current.changeTs ? prev : current; + }); + + return latest; + } else { + return Promise.reject("no session cookie found"); + } +} + +export async function getSessionCookieById({ + sessionId, + organization, +}: { + sessionId: string; + organization?: string; +}): Promise> { + const cookiesList = await cookies(); + const stringifiedCookie = cookiesList.get("sessions"); + + if (stringifiedCookie?.value) { + const sessions: SessionCookie[] = JSON.parse(stringifiedCookie?.value); + + const found = sessions.find((s) => + organization + ? s.organization === organization && s.id === sessionId + : s.id === sessionId, + ); + if (found) { + return found; + } else { + return Promise.reject(); + } + } else { + return Promise.reject(); + } +} + +export async function getSessionCookieByLoginName({ + loginName, + organization, +}: { + loginName?: string; + organization?: string; +}): Promise> { + const cookiesList = await cookies(); + const stringifiedCookie = cookiesList.get("sessions"); + + if (stringifiedCookie?.value) { + const sessions: SessionCookie[] = JSON.parse(stringifiedCookie?.value); + const found = sessions.find((s) => + organization + ? s.organization === organization && s.loginName === loginName + : s.loginName === loginName, + ); + if (found) { + return found; + } else { + return Promise.reject("no cookie found with loginName: " + loginName); + } + } else { + return Promise.reject("no session cookie found"); + } +} + +/** + * + * @param cleanup when true, removes all expired sessions, default true + * @returns Session Cookies + */ +export async function getAllSessionCookieIds( + cleanup: boolean = false, +): Promise { + const cookiesList = await cookies(); + const stringifiedCookie = cookiesList.get("sessions"); + + if (stringifiedCookie?.value) { + const sessions: SessionCookie[] = JSON.parse(stringifiedCookie?.value); + + if (cleanup) { + const now = new Date(); + return sessions + .filter((session) => + session.expirationTs + ? timestampDate(timestampFromMs(Number(session.expirationTs))) > now + : true, + ) + .map((session) => session.id); + } else { + return sessions.map((session) => session.id); + } + } else { + return []; + } +} + +/** + * + * @param cleanup when true, removes all expired sessions, default true + * @returns Session Cookies + */ +export async function getAllSessions( + cleanup: boolean = false, +): Promise[]> { + const cookiesList = await cookies(); + const stringifiedCookie = cookiesList.get("sessions"); + + if (stringifiedCookie?.value) { + const sessions: SessionCookie[] = JSON.parse(stringifiedCookie?.value); + + if (cleanup) { + const now = new Date(); + return sessions.filter((session) => + session.expirationTs + ? timestampDate(timestampFromMs(Number(session.expirationTs))) > now + : true, + ); + } else { + return sessions; + } + } else { + return []; + } +} + +/** + * Returns most recent session filtered by optinal loginName + * @param loginName optional loginName to filter cookies, if non provided, returns most recent session + * @param organization optional organization to filter cookies + * @returns most recent session + */ +export async function getMostRecentCookieWithLoginname({ + loginName, + organization, +}: { + loginName?: string; + organization?: string; +}): Promise { + const cookiesList = await cookies(); + const stringifiedCookie = cookiesList.get("sessions"); + + if (stringifiedCookie?.value) { + const sessions: SessionCookie[] = JSON.parse(stringifiedCookie?.value); + let filtered = sessions.filter((cookie) => { + return loginName ? cookie.loginName === loginName : true; + }); + + if (organization) { + filtered = filtered.filter((cookie) => { + return cookie.organization === organization; + }); + } + + const latest = + filtered && filtered.length + ? filtered.reduce((prev, current) => { + return prev.changeTs > current.changeTs ? prev : current; + }) + : undefined; + + if (latest) { + return latest; + } else { + return Promise.reject("Could not get the context or retrieve a session"); + } + } else { + return Promise.reject("Could not read session cookie"); + } +} diff --git a/apps/login/src/lib/demos.ts b/apps/login/src/lib/demos.ts new file mode 100644 index 0000000000..38912e50e5 --- /dev/null +++ b/apps/login/src/lib/demos.ts @@ -0,0 +1,38 @@ +export type Item = { + name: string; + slug: string; + description?: string; +}; + +export const demos: { name: string; items: Item[] }[] = [ + { + name: "Login", + items: [ + { + name: "Loginname", + slug: "loginname", + description: "Start the loginflow with loginname", + }, + { + name: "Accounts", + slug: "accounts", + description: "List active and inactive sessions", + }, + ], + }, + { + name: "Register", + items: [ + { + name: "Register", + slug: "register", + description: "Add a user with password or passkey", + }, + { + name: "IDP Register", + slug: "idp", + description: "Add a user from an external identity provider", + }, + ], + }, +]; diff --git a/apps/login/src/lib/fingerprint.ts b/apps/login/src/lib/fingerprint.ts new file mode 100644 index 0000000000..55b59dadc8 --- /dev/null +++ b/apps/login/src/lib/fingerprint.ts @@ -0,0 +1,66 @@ +import { create } from "@zitadel/client"; +import { + UserAgent, + UserAgentSchema, +} from "@zitadel/proto/zitadel/session/v2/session_pb"; +import { cookies, headers } from "next/headers"; +import { userAgent } from "next/server"; +import { v4 as uuidv4 } from "uuid"; + +export async function getFingerprintId() { + return uuidv4(); +} + +export async function setFingerprintIdCookie(fingerprintId: string) { + const cookiesList = await cookies(); + + return cookiesList.set({ + name: "fingerprintId", + value: fingerprintId, + httpOnly: true, + path: "/", + maxAge: 31536000, // 1 year + }); +} + +export async function getFingerprintIdCookie() { + const cookiesList = await cookies(); + return cookiesList.get("fingerprintId"); +} + +export async function getOrSetFingerprintId(): Promise { + const cookie = await getFingerprintIdCookie(); + if (cookie) { + return cookie.value; + } + + const fingerprintId = await getFingerprintId(); + await setFingerprintIdCookie(fingerprintId); + return fingerprintId; +} + +export async function getUserAgent(): Promise { + const _headers = await headers(); + + const fingerprintId = await getOrSetFingerprintId(); + + const { device, engine, os, browser } = userAgent({ headers: _headers }); + + const userAgentHeader = _headers.get("user-agent"); + + const userAgentHeaderValues = userAgentHeader?.split(","); + + const deviceDescription = `${device?.type ? `${device.type},` : ""} ${device?.vendor ? `${device.vendor},` : ""} ${device.model ? `${device.model},` : ""} `; + const osDescription = `${os?.name ? `${os.name},` : ""} ${os?.version ? `${os.version},` : ""} `; + const engineDescription = `${engine?.name ? `${engine.name},` : ""} ${engine?.version ? `${engine.version},` : ""} `; + const browserDescription = `${browser?.name ? `${browser.name},` : ""} ${browser.version ? `${browser.version},` : ""} `; + + const userAgentData: UserAgent = create(UserAgentSchema, { + ip: _headers.get("x-forwarded-for") ?? _headers.get("remoteAddress") ?? "", + header: { "user-agent": { values: userAgentHeaderValues } }, + description: `${browserDescription}, ${deviceDescription}, ${engineDescription}, ${osDescription}`, + fingerprintId: fingerprintId, + }); + + return userAgentData; +} diff --git a/apps/login/src/lib/i18n.ts b/apps/login/src/lib/i18n.ts new file mode 100644 index 0000000000..780d9a98f2 --- /dev/null +++ b/apps/login/src/lib/i18n.ts @@ -0,0 +1,42 @@ +export interface Lang { + name: string; + code: string; +} + +export const LANGS: Lang[] = [ + { + name: "English", + code: "en", + }, + { + name: "Deutsch", + code: "de", + }, + { + name: "Italiano", + code: "it", + }, + { + name: "Español", + code: "es", + }, + { + name: "Polski", + code: "pl", + }, + { + name: "简体中文", + code: "zh", + }, + { + name: "Русский", + code: "ru", + }, + { + name: "Türkçe", + code: "tr", + }, +]; + +export const LANGUAGE_COOKIE_NAME = "NEXT_LOCALE"; +export const LANGUAGE_HEADER_NAME = "accept-language"; diff --git a/apps/login/src/lib/idp.ts b/apps/login/src/lib/idp.ts new file mode 100644 index 0000000000..d355f9ab56 --- /dev/null +++ b/apps/login/src/lib/idp.ts @@ -0,0 +1,77 @@ +import { IDPType } from "@zitadel/proto/zitadel/idp/v2/idp_pb"; +import { IdentityProviderType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; + +// This maps the IdentityProviderType to a slug which is used in the /success and /failure routes +export function idpTypeToSlug(idpType: IdentityProviderType) { + switch (idpType) { + case IdentityProviderType.GITHUB: + return "github"; + case IdentityProviderType.GITHUB_ES: + return "github_es"; + case IdentityProviderType.GITLAB: + return "gitlab"; + case IdentityProviderType.GITLAB_SELF_HOSTED: + return "gitlab_es"; + case IdentityProviderType.APPLE: + return "apple"; + case IdentityProviderType.GOOGLE: + return "google"; + case IdentityProviderType.AZURE_AD: + return "azure"; + case IdentityProviderType.SAML: + return "saml"; + case IdentityProviderType.OAUTH: + return "oauth"; + case IdentityProviderType.OIDC: + return "oidc"; + case IdentityProviderType.LDAP: + return "ldap"; + case IdentityProviderType.JWT: + return "jwt"; + default: + throw new Error("Unknown identity provider type"); + } +} + +// TODO: this is ugly but needed atm as the getIDPByID returns a IDPType and not a IdentityProviderType +export function idpTypeToIdentityProviderType( + idpType: IDPType, +): IdentityProviderType { + switch (idpType) { + case IDPType.IDP_TYPE_GITHUB: + return IdentityProviderType.GITHUB; + + case IDPType.IDP_TYPE_GITHUB_ES: + return IdentityProviderType.GITHUB_ES; + + case IDPType.IDP_TYPE_GITLAB: + return IdentityProviderType.GITLAB; + + case IDPType.IDP_TYPE_GITLAB_SELF_HOSTED: + return IdentityProviderType.GITLAB_SELF_HOSTED; + + case IDPType.IDP_TYPE_APPLE: + return IdentityProviderType.APPLE; + + case IDPType.IDP_TYPE_GOOGLE: + return IdentityProviderType.GOOGLE; + + case IDPType.IDP_TYPE_AZURE_AD: + return IdentityProviderType.AZURE_AD; + + case IDPType.IDP_TYPE_SAML: + return IdentityProviderType.SAML; + + case IDPType.IDP_TYPE_OAUTH: + return IdentityProviderType.OAUTH; + + case IDPType.IDP_TYPE_OIDC: + return IdentityProviderType.OIDC; + + case IDPType.IDP_TYPE_JWT: + return IdentityProviderType.JWT; + + default: + throw new Error("Unknown identity provider type"); + } +} diff --git a/apps/login/src/lib/oidc.ts b/apps/login/src/lib/oidc.ts new file mode 100644 index 0000000000..b692300dea --- /dev/null +++ b/apps/login/src/lib/oidc.ts @@ -0,0 +1,132 @@ +import { Cookie } from "@/lib/cookies"; +import { sendLoginname, SendLoginnameCommand } from "@/lib/server/loginname"; +import { createCallback, getLoginSettings } from "@/lib/zitadel"; +import { create } from "@zitadel/client"; +import { + CreateCallbackRequestSchema, + SessionSchema, +} from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb"; +import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; +import { NextRequest, NextResponse } from "next/server"; +import { constructUrl } from "./service-url"; +import { isSessionValid } from "./session"; + +type LoginWithOIDCAndSession = { + serviceUrl: string; + authRequest: string; + sessionId: string; + sessions: Session[]; + sessionCookies: Cookie[]; + request: NextRequest; +}; +export async function loginWithOIDCAndSession({ + serviceUrl, + authRequest, + sessionId, + sessions, + sessionCookies, + request, +}: LoginWithOIDCAndSession) { + console.log( + `Login with session: ${sessionId} and authRequest: ${authRequest}`, + ); + + const selectedSession = sessions.find((s) => s.id === sessionId); + + if (selectedSession && selectedSession.id) { + console.log(`Found session ${selectedSession.id}`); + + const isValid = await isSessionValid({ + serviceUrl, + session: selectedSession, + }); + + console.log("Session is valid:", isValid); + + if (!isValid && selectedSession.factors?.user) { + // if the session is not valid anymore, we need to redirect the user to re-authenticate / + // TODO: handle IDP intent direcly if available + const command: SendLoginnameCommand = { + loginName: selectedSession.factors.user?.loginName, + organization: selectedSession.factors?.user?.organizationId, + requestId: `oidc_${authRequest}`, + }; + + const res = await sendLoginname(command); + + if (res && "redirect" in res && res?.redirect) { + const absoluteUrl = constructUrl(request, res.redirect); + return NextResponse.redirect(absoluteUrl.toString()); + } + } + + const cookie = sessionCookies.find( + (cookie) => cookie.id === selectedSession?.id, + ); + + if (cookie && cookie.id && cookie.token) { + const session = { + sessionId: cookie?.id, + sessionToken: cookie?.token, + }; + + // works not with _rsc request + try { + const { callbackUrl } = await createCallback({ + serviceUrl, + req: create(CreateCallbackRequestSchema, { + authRequestId: authRequest, + callbackKind: { + case: "session", + value: create(SessionSchema, session), + }, + }), + }); + if (callbackUrl) { + return NextResponse.redirect(callbackUrl); + } else { + return NextResponse.json( + { error: "An error occurred!" }, + { status: 500 }, + ); + } + } catch (error: unknown) { + // handle already handled gracefully as these could come up if old emails with requestId are used (reset password, register emails etc.) + console.error(error); + if ( + error && + typeof error === "object" && + "code" in error && + error?.code === 9 + ) { + const loginSettings = await getLoginSettings({ + serviceUrl, + organization: selectedSession.factors?.user?.organizationId, + }); + + if (loginSettings?.defaultRedirectUri) { + return NextResponse.redirect(loginSettings.defaultRedirectUri); + } + + const signedinUrl = constructUrl(request, "/signedin"); + + if (selectedSession.factors?.user?.loginName) { + signedinUrl.searchParams.set( + "loginName", + selectedSession.factors?.user?.loginName, + ); + } + if (selectedSession.factors?.user?.organizationId) { + signedinUrl.searchParams.set( + "organization", + selectedSession.factors?.user?.organizationId, + ); + } + return NextResponse.redirect(signedinUrl); + } else { + return NextResponse.json({ error }, { status: 500 }); + } + } + } + } +} diff --git a/apps/login/src/lib/saml.ts b/apps/login/src/lib/saml.ts new file mode 100644 index 0000000000..c89eefd2ae --- /dev/null +++ b/apps/login/src/lib/saml.ts @@ -0,0 +1,206 @@ +import { Cookie } from "@/lib/cookies"; +import { sendLoginname, SendLoginnameCommand } from "@/lib/server/loginname"; +import { createResponse, getLoginSettings } from "@/lib/zitadel"; +import { create } from "@zitadel/client"; +import { CreateResponseRequestSchema } from "@zitadel/proto/zitadel/saml/v2/saml_service_pb"; +import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; +import { cookies } from "next/headers"; +import { NextRequest, NextResponse } from "next/server"; +import { v4 as uuidv4 } from "uuid"; +import { constructUrl } from "./service-url"; +import { isSessionValid } from "./session"; + +type LoginWithSAMLAndSession = { + serviceUrl: string; + samlRequest: string; + sessionId: string; + sessions: Session[]; + sessionCookies: Cookie[]; + request: NextRequest; +}; + +export async function getSAMLFormUID() { + return uuidv4(); +} + +export async function setSAMLFormCookie(value: string): Promise { + const cookiesList = await cookies(); + const uid = await getSAMLFormUID(); + + try { + // Check cookie size limits (typical limit is 4KB) + if (value.length > 4000) { + console.warn( + `SAML form cookie value is large (${value.length} characters), may exceed browser limits`, + ); + } + + // Log the attempt + console.log( + `Setting SAML form cookie with uid: ${uid}, value length: ${value.length}`, + ); + + await cookiesList.set({ + name: uid, + value: value, + httpOnly: true, + secure: process.env.NODE_ENV === "production", // Required for HTTPS in production + sameSite: "lax", // Allows cookies with top-level navigation (needed for SAML redirects) + path: "/", + maxAge: 5 * 60, // 5 minutes + }); + + // Note: We can't reliably verify immediately due to Next.js cookies API behavior + // Instead, we'll rely on the getSAMLFormCookie function to detect failures + console.log(`Successfully set SAML form cookie with uid: ${uid}`); + + return uid; + } catch (error) { + console.error(`Failed to set SAML form cookie with uid: ${uid}`, { + error, + valueLength: value.length, + uid, + }); + throw new Error( + `Failed to set SAML form cookie: ${error instanceof Error ? error.message : String(error)}`, + ); + } +} + +export async function getSAMLFormCookie(uid: string): Promise { + const cookiesList = await cookies(); + + try { + const cookie = cookiesList.get(uid); + + if (!cookie) { + console.warn(`SAML form cookie not found for uid: ${uid}`); + return null; + } + + if (!cookie.value) { + console.warn(`SAML form cookie found but empty value for uid: ${uid}`); + return null; + } + + console.log( + `Successfully retrieved SAML form cookie for uid: ${uid}, value length: ${cookie.value.length}`, + ); + return cookie.value; + } catch (error) { + console.error(`Error retrieving SAML form cookie for uid: ${uid}`, error); + return null; + } +} + +export async function loginWithSAMLAndSession({ + serviceUrl, + samlRequest, + sessionId, + sessions, + sessionCookies, + request, +}: LoginWithSAMLAndSession) { + console.log( + `Login with session: ${sessionId} and samlRequest: ${samlRequest}`, + ); + + const selectedSession = sessions.find((s) => s.id === sessionId); + + if (selectedSession && selectedSession.id) { + console.log(`Found session ${selectedSession.id}`); + + const isValid = await isSessionValid({ + serviceUrl, + session: selectedSession, + }); + + console.log("Session is valid:", isValid); + + if (!isValid && selectedSession.factors?.user) { + // if the session is not valid anymore, we need to redirect the user to re-authenticate / + // TODO: handle IDP intent direcly if available + const command: SendLoginnameCommand = { + loginName: selectedSession.factors.user?.loginName, + organization: selectedSession.factors?.user?.organizationId, + requestId: `saml_${samlRequest}`, + }; + + const res = await sendLoginname(command); + + if (res && "redirect" in res && res?.redirect) { + const absoluteUrl = constructUrl(request, res.redirect); + return NextResponse.redirect(absoluteUrl.toString()); + } + } + + const cookie = sessionCookies.find( + (cookie) => cookie.id === selectedSession?.id, + ); + + if (cookie && cookie.id && cookie.token) { + const session = { + sessionId: cookie?.id, + sessionToken: cookie?.token, + }; + + // works not with _rsc request + try { + const { url } = await createResponse({ + serviceUrl, + req: create(CreateResponseRequestSchema, { + samlRequestId: samlRequest, + responseKind: { + case: "session", + value: session, + }, + }), + }); + if (url) { + return NextResponse.redirect(url); + } else { + return NextResponse.json( + { error: "An error occurred!" }, + { status: 500 }, + ); + } + } catch (error: unknown) { + // handle already handled gracefully as these could come up if old emails with requestId are used (reset password, register emails etc.) + console.error(error); + if ( + error && + typeof error === "object" && + "code" in error && + error?.code === 9 + ) { + const loginSettings = await getLoginSettings({ + serviceUrl, + organization: selectedSession.factors?.user?.organizationId, + }); + + if (loginSettings?.defaultRedirectUri) { + return NextResponse.redirect(loginSettings.defaultRedirectUri); + } + + const signedinUrl = constructUrl(request, "/signedin"); + + if (selectedSession.factors?.user?.loginName) { + signedinUrl.searchParams.set( + "loginName", + selectedSession.factors?.user?.loginName, + ); + } + if (selectedSession.factors?.user?.organizationId) { + signedinUrl.searchParams.set( + "organization", + selectedSession.factors?.user?.organizationId, + ); + } + return NextResponse.redirect(signedinUrl); + } else { + return NextResponse.json({ error }, { status: 500 }); + } + } + } + } +} diff --git a/apps/login/src/lib/self.ts b/apps/login/src/lib/self.ts new file mode 100644 index 0000000000..df8508c29e --- /dev/null +++ b/apps/login/src/lib/self.ts @@ -0,0 +1,60 @@ +"use server"; + +import { createUserServiceClient } from "@zitadel/client/v2"; +import { headers } from "next/headers"; +import { getSessionCookieById } from "./cookies"; +import { getServiceUrlFromHeaders } from "./service-url"; +import { createServerTransport, getSession } from "./zitadel"; + +const myUserService = async (serviceUrl: string, sessionToken: string) => { + const transportPromise = await createServerTransport( + sessionToken, + serviceUrl, + ); + return createUserServiceClient(transportPromise); +}; + +export async function setMyPassword({ + sessionId, + password, +}: { + sessionId: string; + password: string; +}) { + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + + const sessionCookie = await getSessionCookieById({ sessionId }); + + const { session } = await getSession({ + serviceUrl, + sessionId: sessionCookie.id, + sessionToken: sessionCookie.token, + }); + + if (!session) { + return { error: "Could not load session" }; + } + + const service = await myUserService(serviceUrl, `${sessionCookie.token}`); + + if (!session?.factors?.user?.id) { + return { error: "No user id found in session" }; + } + + return service + .setPassword( + { + userId: session.factors.user.id, + newPassword: { password, changeRequired: false }, + }, + {}, + ) + .catch((error) => { + console.log(error); + if (error.code === 7) { + return { error: "Session is not valid." }; + } + throw error; + }); +} diff --git a/apps/login/src/lib/server/cookie.ts b/apps/login/src/lib/server/cookie.ts new file mode 100644 index 0000000000..7f87f49731 --- /dev/null +++ b/apps/login/src/lib/server/cookie.ts @@ -0,0 +1,302 @@ +"use server"; + +import { addSessionToCookie, updateSessionCookie } from "@/lib/cookies"; +import { + createSessionForUserIdAndIdpIntent, + createSessionFromChecks, + getSecuritySettings, + getSession, + setSession, +} from "@/lib/zitadel"; +import { ConnectError, Duration, timestampMs } from "@zitadel/client"; +import { + CredentialsCheckError, + CredentialsCheckErrorSchema, + ErrorDetail, +} from "@zitadel/proto/zitadel/message_pb"; +import { + Challenges, + RequestChallenges, +} from "@zitadel/proto/zitadel/session/v2/challenge_pb"; +import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; +import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; +import { headers } from "next/headers"; +import { getServiceUrlFromHeaders } from "../service-url"; + +type CustomCookieData = { + id: string; + token: string; + loginName: string; + organization?: string; + creationTs: string; + expirationTs: string; + changeTs: string; + requestId?: string; // if its linked to an OIDC flow +}; + +const passwordAttemptsHandler = (error: ConnectError) => { + const details = error.findDetails(CredentialsCheckErrorSchema); + + if (details[0] && "failedAttempts" in details[0]) { + const failedAttempts = details[0].failedAttempts; + throw { + error: `Failed to authenticate: You had ${failedAttempts} password attempts.`, + failedAttempts: failedAttempts, + }; + } + throw error; +}; + +export async function createSessionAndUpdateCookie(command: { + checks: Checks; + requestId: string | undefined; + lifetime?: Duration; +}): Promise { + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + + let sessionLifetime = command.lifetime; + + if (!sessionLifetime) { + console.warn("No session lifetime provided, using default of 24 hours."); + + sessionLifetime = { + seconds: BigInt(24 * 60 * 60), // 24 hours + nanos: 0, + } as Duration; // for usecases where the lifetime is not specified (user discovery) + } + + const createdSession = await createSessionFromChecks({ + serviceUrl, + checks: command.checks, + lifetime: sessionLifetime, + }); + + if (createdSession) { + return getSession({ + serviceUrl, + sessionId: createdSession.sessionId, + sessionToken: createdSession.sessionToken, + }).then(async (response) => { + if (response?.session && response.session?.factors?.user?.loginName) { + const sessionCookie: CustomCookieData = { + id: createdSession.sessionId, + token: createdSession.sessionToken, + creationTs: response.session.creationDate + ? `${timestampMs(response.session.creationDate)}` + : "", + expirationTs: response.session.expirationDate + ? `${timestampMs(response.session.expirationDate)}` + : "", + changeTs: response.session.changeDate + ? `${timestampMs(response.session.changeDate)}` + : "", + loginName: response.session.factors.user.loginName ?? "", + }; + + if (command.requestId) { + sessionCookie.requestId = command.requestId; + } + + if (response.session.factors.user.organizationId) { + sessionCookie.organization = + response.session.factors.user.organizationId; + } + + const securitySettings = await getSecuritySettings({ serviceUrl }); + const sameSite = securitySettings?.embeddedIframe?.enabled + ? "none" + : true; + + await addSessionToCookie({ session: sessionCookie, sameSite }); + + return response.session as Session; + } else { + throw "could not get session or session does not have loginName"; + } + }); + } else { + throw "Could not create session"; + } +} + +export async function createSessionForIdpAndUpdateCookie({ + userId, + idpIntent, + requestId, + lifetime, +}: { + userId: string; + idpIntent: { + idpIntentId?: string | undefined; + idpIntentToken?: string | undefined; + }; + requestId: string | undefined; + lifetime?: Duration; +}): Promise { + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + + let sessionLifetime = lifetime; + + if (!sessionLifetime) { + console.warn( + "No IDP session lifetime provided, using default of 24 hours.", + ); + + sessionLifetime = { + seconds: BigInt(24 * 60 * 60), // 24 hours + nanos: 0, + } as Duration; + } + + const createdSession = await createSessionForUserIdAndIdpIntent({ + serviceUrl, + userId, + idpIntent, + lifetime: sessionLifetime, + }).catch((error: ErrorDetail | CredentialsCheckError) => { + console.error("Could not set session", error); + if ("failedAttempts" in error && error.failedAttempts) { + throw { + error: `Failed to authenticate: You had ${error.failedAttempts} password attempts.`, + failedAttempts: error.failedAttempts, + }; + } + throw error; + }); + + if (!createdSession) { + throw "Could not create session"; + } + + const { session } = await getSession({ + serviceUrl, + sessionId: createdSession.sessionId, + sessionToken: createdSession.sessionToken, + }); + + if (!session || !session.factors?.user?.loginName) { + throw "Could not retrieve session"; + } + + const sessionCookie: CustomCookieData = { + id: createdSession.sessionId, + token: createdSession.sessionToken, + creationTs: session.creationDate + ? `${timestampMs(session.creationDate)}` + : "", + expirationTs: session.expirationDate + ? `${timestampMs(session.expirationDate)}` + : "", + changeTs: session.changeDate ? `${timestampMs(session.changeDate)}` : "", + loginName: session.factors.user.loginName ?? "", + organization: session.factors.user.organizationId ?? "", + }; + + if (requestId) { + sessionCookie.requestId = requestId; + } + + if (session.factors.user.organizationId) { + sessionCookie.organization = session.factors.user.organizationId; + } + + const securitySettings = await getSecuritySettings({ serviceUrl }); + const sameSite = securitySettings?.embeddedIframe?.enabled ? "none" : true; + + return addSessionToCookie({ session: sessionCookie, sameSite }).then(() => { + return session as Session; + }); +} + +export type SessionWithChallenges = Session & { + challenges: Challenges | undefined; +}; + +export async function setSessionAndUpdateCookie(command: { + recentCookie: CustomCookieData; + checks?: Checks; + challenges?: RequestChallenges; + requestId?: string; + lifetime: Duration; +}) { + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + + return setSession({ + serviceUrl, + sessionId: command.recentCookie.id, + sessionToken: command.recentCookie.token, + challenges: command.challenges, + checks: command.checks, + lifetime: command.lifetime, + }) + .then((updatedSession) => { + if (updatedSession) { + const sessionCookie: CustomCookieData = { + id: command.recentCookie.id, + token: updatedSession.sessionToken, + creationTs: command.recentCookie.creationTs, + expirationTs: command.recentCookie.expirationTs, + // just overwrite the changeDate with the new one + changeTs: updatedSession.details?.changeDate + ? `${timestampMs(updatedSession.details.changeDate)}` + : "", + loginName: command.recentCookie.loginName, + organization: command.recentCookie.organization, + }; + + if (command.requestId) { + sessionCookie.requestId = command.requestId; + } + + return getSession({ + serviceUrl, + sessionId: sessionCookie.id, + sessionToken: sessionCookie.token, + }).then(async (response) => { + if ( + !response?.session || + !response.session.factors?.user?.loginName + ) { + throw "could not get session or session does not have loginName"; + } + + const { session } = response; + const newCookie: CustomCookieData = { + id: sessionCookie.id, + token: updatedSession.sessionToken, + creationTs: sessionCookie.creationTs, + expirationTs: sessionCookie.expirationTs, + // just overwrite the changeDate with the new one + changeTs: updatedSession.details?.changeDate + ? `${timestampMs(updatedSession.details.changeDate)}` + : "", + loginName: session.factors?.user?.loginName ?? "", + organization: session.factors?.user?.organizationId ?? "", + }; + + if (sessionCookie.requestId) { + newCookie.requestId = sessionCookie.requestId; + } + + const securitySettings = await getSecuritySettings({ serviceUrl }); + const sameSite = securitySettings?.embeddedIframe?.enabled + ? "none" + : true; + + return updateSessionCookie({ + id: sessionCookie.id, + session: newCookie, + sameSite, + }).then(() => { + return { challenges: updatedSession.challenges, ...session }; + }); + }); + } else { + throw "Session not be set"; + } + }) + .catch(passwordAttemptsHandler); +} diff --git a/apps/login/src/lib/server/device.ts b/apps/login/src/lib/server/device.ts new file mode 100644 index 0000000000..5e36facfc8 --- /dev/null +++ b/apps/login/src/lib/server/device.ts @@ -0,0 +1,20 @@ +"use server"; + +import { authorizeOrDenyDeviceAuthorization } from "@/lib/zitadel"; +import { headers } from "next/headers"; +import { getServiceUrlFromHeaders } from "../service-url"; + +export async function completeDeviceAuthorization( + deviceAuthorizationId: string, + session?: { sessionId: string; sessionToken: string }, +) { + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + + // without the session, device auth request is denied + return authorizeOrDenyDeviceAuthorization({ + serviceUrl, + deviceAuthorizationId, + session, + }); +} diff --git a/apps/login/src/lib/server/idp.ts b/apps/login/src/lib/server/idp.ts new file mode 100644 index 0000000000..87f88a7c32 --- /dev/null +++ b/apps/login/src/lib/server/idp.ts @@ -0,0 +1,241 @@ +"use server"; + +import { + getLoginSettings, + getUserByID, + startIdentityProviderFlow, + startLDAPIdentityProviderFlow, +} from "@/lib/zitadel"; +import { headers } from "next/headers"; +import { redirect } from "next/navigation"; +import { getNextUrl } from "../client"; +import { getServiceUrlFromHeaders } from "../service-url"; +import { checkEmailVerification } from "../verify-helper"; +import { createSessionForIdpAndUpdateCookie } from "./cookie"; + +export type RedirectToIdpState = { error?: string | null } | undefined; + +export async function redirectToIdp( + prevState: RedirectToIdpState, + formData: FormData, +): Promise { + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + const host = _headers.get("host"); + if (!host) { + return { error: "Could not get host" }; + } + + const params = new URLSearchParams(); + + const linkOnly = formData.get("linkOnly") === "true"; + const requestId = formData.get("requestId") as string; + const organization = formData.get("organization") as string; + const idpId = formData.get("id") as string; + const provider = formData.get("provider") as string; + + if (linkOnly) params.set("link", "true"); + if (requestId) params.set("requestId", requestId); + if (organization) params.set("organization", organization); + + // redirect to LDAP page where username and password is requested + if (provider === "ldap") { + params.set("idpId", idpId); + redirect(`/idp/ldap?` + params.toString()); + } + + const response = await startIDPFlow({ + serviceUrl, + host, + idpId, + successUrl: `/idp/${provider}/success?` + params.toString(), + failureUrl: `/idp/${provider}/failure?` + params.toString(), + }); + + if (!response) { + return { error: "Could not start IDP flow" }; + } + + if (response && "redirect" in response && response?.redirect) { + redirect(response.redirect); + } + + return { error: "Unexpected response from IDP flow" }; +} + +export type StartIDPFlowCommand = { + serviceUrl: string; + host: string; + idpId: string; + successUrl: string; + failureUrl: string; +}; + +async function startIDPFlow(command: StartIDPFlowCommand) { + const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? ""; + + const url = await startIdentityProviderFlow({ + serviceUrl: command.serviceUrl, + idpId: command.idpId, + urls: { + successUrl: `${command.host.includes("localhost") ? "http://" : "https://"}${command.host}${basePath}${command.successUrl}`, + failureUrl: `${command.host.includes("localhost") ? "http://" : "https://"}${command.host}${basePath}${command.failureUrl}`, + }, + }); + + if (!url) { + return { error: "Could not start IDP flow" }; + } + + return { redirect: url }; +} + +type CreateNewSessionCommand = { + userId: string; + idpIntent: { + idpIntentId: string; + idpIntentToken: string; + }; + loginName?: string; + password?: string; + organization?: string; + requestId?: string; +}; + +export async function createNewSessionFromIdpIntent( + command: CreateNewSessionCommand, +) { + const _headers = await headers(); + + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + const host = _headers.get("host"); + + if (!host) { + return { error: "Could not get domain" }; + } + + if (!command.userId || !command.idpIntent) { + throw new Error("No userId or loginName provided"); + } + + const userResponse = await getUserByID({ + serviceUrl, + userId: command.userId, + }); + + if (!userResponse || !userResponse.user) { + return { error: "User not found in the system" }; + } + + const loginSettings = await getLoginSettings({ + serviceUrl, + organization: userResponse.user.details?.resourceOwner, + }); + + const session = await createSessionForIdpAndUpdateCookie({ + userId: command.userId, + idpIntent: command.idpIntent, + requestId: command.requestId, + lifetime: loginSettings?.externalLoginCheckLifetime, + }); + + if (!session || !session.factors?.user) { + return { error: "Could not create session" }; + } + + const humanUser = + userResponse.user.type.case === "human" + ? userResponse.user.type.value + : undefined; + + // check to see if user was verified + const emailVerificationCheck = checkEmailVerification( + session, + humanUser, + command.organization, + command.requestId, + ); + + if (emailVerificationCheck?.redirect) { + return emailVerificationCheck; + } + + // TODO: check if user has MFA methods + // const mfaFactorCheck = checkMFAFactors(session, loginSettings, authMethods, organization, requestId); + // if (mfaFactorCheck?.redirect) { + // return mfaFactorCheck; + // } + + const url = await getNextUrl( + command.requestId && session.id + ? { + sessionId: session.id, + requestId: command.requestId, + organization: session.factors.user.organizationId, + } + : { + loginName: session.factors.user.loginName, + organization: session.factors.user.organizationId, + }, + loginSettings?.defaultRedirectUri, + ); + + if (url) { + return { redirect: url }; + } +} + +type createNewSessionForLDAPCommand = { + username: string; + password: string; + idpId: string; + link: boolean; +}; + +export async function createNewSessionForLDAP( + command: createNewSessionForLDAPCommand, +) { + const _headers = await headers(); + + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + const host = _headers.get("host"); + + if (!host) { + return { error: "Could not get domain" }; + } + + if (!command.username || !command.password) { + return { error: "No username or password provided" }; + } + + const response = await startLDAPIdentityProviderFlow({ + serviceUrl, + idpId: command.idpId, + username: command.username, + password: command.password, + }); + + if ( + !response || + response.nextStep.case !== "idpIntent" || + !response.nextStep.value + ) { + return { error: "Could not start LDAP identity provider flow" }; + } + + const { userId, idpIntentId, idpIntentToken } = response.nextStep.value; + + const params = new URLSearchParams({ + userId, + id: idpIntentId, + token: idpIntentToken, + }); + + if (command.link) { + params.set("link", "true"); + } + + return { + redirect: `/idp/ldap/success?` + params.toString(), + }; +} diff --git a/apps/login/src/lib/server/loginname.ts b/apps/login/src/lib/server/loginname.ts new file mode 100644 index 0000000000..dee740bf4f --- /dev/null +++ b/apps/login/src/lib/server/loginname.ts @@ -0,0 +1,466 @@ +"use server"; + +import { create } from "@zitadel/client"; +import { ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; +import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; +import { headers } from "next/headers"; +import { idpTypeToIdentityProviderType, idpTypeToSlug } from "../idp"; + +import { PasskeysType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; +import { UserState } from "@zitadel/proto/zitadel/user/v2/user_pb"; +import { getServiceUrlFromHeaders } from "../service-url"; +import { + getActiveIdentityProviders, + getIDPByID, + getLoginSettings, + getOrgsByDomain, + listAuthenticationMethodTypes, + listIDPLinks, + searchUsers, + SearchUsersCommand, + startIdentityProviderFlow, +} from "../zitadel"; +import { createSessionAndUpdateCookie } from "./cookie"; + +export type SendLoginnameCommand = { + loginName: string; + requestId?: string; + organization?: string; + suffix?: string; +}; + +const ORG_SUFFIX_REGEX = /(?<=@)(.+)/; + +export async function sendLoginname(command: SendLoginnameCommand) { + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + const host = _headers.get("host"); + + if (!host) { + throw new Error("Could not get domain"); + } + + const loginSettingsByContext = await getLoginSettings({ + serviceUrl, + organization: command.organization, + }); + + if (!loginSettingsByContext) { + return { error: "Could not get login settings" }; + } + + let searchUsersRequest: SearchUsersCommand = { + serviceUrl, + searchValue: command.loginName, + organizationId: command.organization, + loginSettings: loginSettingsByContext, + suffix: command.suffix, + }; + + const searchResult = await searchUsers(searchUsersRequest); + + if ("error" in searchResult && searchResult.error) { + return searchResult; + } + + if (!("result" in searchResult)) { + return { error: "Could not search users" }; + } + + const { result: potentialUsers } = searchResult; + + const redirectUserToSingleIDPIfAvailable = async () => { + const identityProviders = await getActiveIdentityProviders({ + serviceUrl, + orgId: command.organization, + }).then((resp) => { + return resp.identityProviders; + }); + + if (identityProviders.length === 1) { + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + const host = _headers.get("host"); + + if (!host) { + return { error: "Could not get host" }; + } + + const identityProviderType = identityProviders[0].type; + + const provider = idpTypeToSlug(identityProviderType); + + const params = new URLSearchParams(); + + if (command.requestId) { + params.set("requestId", command.requestId); + } + + if (command.organization) { + params.set("organization", command.organization); + } + + const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? ""; + + const url = await startIdentityProviderFlow({ + serviceUrl, + idpId: identityProviders[0].id, + urls: { + successUrl: + `${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/idp/${provider}/success?` + + new URLSearchParams(params), + failureUrl: + `${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/idp/${provider}/failure?` + + new URLSearchParams(params), + }, + }); + + if (!url) { + return { error: "Could not start IDP flow" }; + } + + return { redirect: url }; + } + }; + + const redirectUserToIDP = async (userId: string) => { + const identityProviders = await listIDPLinks({ + serviceUrl, + userId, + }).then((resp) => { + return resp.result; + }); + + if (identityProviders.length === 1) { + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + const host = _headers.get("host"); + + if (!host) { + return { error: "Could not get host" }; + } + + const identityProviderId = identityProviders[0].idpId; + + const idp = await getIDPByID({ + serviceUrl, + id: identityProviderId, + }); + + const idpType = idp?.type; + + if (!idp || !idpType) { + throw new Error("Could not find identity provider"); + } + + const identityProviderType = idpTypeToIdentityProviderType(idpType); + const provider = idpTypeToSlug(identityProviderType); + + const params = new URLSearchParams({ userId }); + + if (command.requestId) { + params.set("requestId", command.requestId); + } + + if (command.organization) { + params.set("organization", command.organization); + } + + const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? ""; + + const url = await startIdentityProviderFlow({ + serviceUrl, + idpId: idp.id, + urls: { + successUrl: + `${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/idp/${provider}/success?` + + new URLSearchParams(params), + failureUrl: + `${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/idp/${provider}/failure?` + + new URLSearchParams(params), + }, + }); + + if (!url) { + return { error: "Could not start IDP flow" }; + } + + return { redirect: url }; + } + }; + + if (potentialUsers.length > 1) { + return { error: "More than one user found. Provide a unique identifier." }; + } else if (potentialUsers.length == 1 && potentialUsers[0].userId) { + const user = potentialUsers[0]; + const userId = potentialUsers[0].userId; + + const userLoginSettings = await getLoginSettings({ + serviceUrl, + organization: user.details?.resourceOwner, + }); + + // compare with the concatenated suffix when set + const concatLoginname = command.suffix + ? `${command.loginName}@${command.suffix}` + : command.loginName; + + const humanUser = + potentialUsers[0].type.case === "human" + ? potentialUsers[0].type.value + : undefined; + + // recheck login settings after user discovery, as the search might have been done without org scope + if ( + userLoginSettings?.disableLoginWithEmail && + userLoginSettings?.disableLoginWithPhone + ) { + if (user.preferredLoginName !== concatLoginname) { + return { error: "User not found in the system!" }; + } + } else if (userLoginSettings?.disableLoginWithEmail) { + if ( + user.preferredLoginName !== concatLoginname || + humanUser?.phone?.phone !== command.loginName + ) { + return { error: "User not found in the system!" }; + } + } else if (userLoginSettings?.disableLoginWithPhone) { + if ( + user.preferredLoginName !== concatLoginname || + humanUser?.email?.email !== command.loginName + ) { + return { error: "User not found in the system!" }; + } + } + + const checks = create(ChecksSchema, { + user: { search: { case: "userId", value: userId } }, + }); + + const session = await createSessionAndUpdateCookie({ + checks, + requestId: command.requestId, + }); + + if (!session.factors?.user?.id) { + return { error: "Could not create session for user" }; + } + + // TODO: check if handling of userstate INITIAL is needed + if (user.state === UserState.INITIAL) { + return { error: "Initial User not supported" }; + } + + const methods = await listAuthenticationMethodTypes({ + serviceUrl, + userId: session.factors?.user?.id, + }); + + // always resend invite if user has no auth method set + if (!methods.authMethodTypes || !methods.authMethodTypes.length) { + const params = new URLSearchParams({ + loginName: session.factors?.user?.loginName as string, + send: "true", // set this to true to request a new code immediately + invite: "true", + }); + + if (command.requestId) { + params.append("requestId", command.requestId); + } + + if (command.organization || session.factors?.user?.organizationId) { + params.append( + "organization", + command.organization ?? + (session.factors?.user?.organizationId as string), + ); + } + + return { redirect: `/verify?` + params }; + } + + if (methods.authMethodTypes.length == 1) { + const method = methods.authMethodTypes[0]; + switch (method) { + case AuthenticationMethodType.PASSWORD: // user has only password as auth method + if (!userLoginSettings?.allowUsernamePassword) { + return { + error: + "Username Password not allowed! Contact your administrator for more information.", + }; + } + + const paramsPassword = new URLSearchParams({ + loginName: session.factors?.user?.loginName, + }); + + // TODO: does this have to be checked in loginSettings.allowDomainDiscovery + + if (command.organization || session.factors?.user?.organizationId) { + paramsPassword.append( + "organization", + command.organization ?? session.factors?.user?.organizationId, + ); + } + + if (command.requestId) { + paramsPassword.append("requestId", command.requestId); + } + + return { + redirect: "/password?" + paramsPassword, + }; + + case AuthenticationMethodType.PASSKEY: // AuthenticationMethodType.AUTHENTICATION_METHOD_TYPE_PASSKEY + if (userLoginSettings?.passkeysType === PasskeysType.NOT_ALLOWED) { + return { + error: + "Passkeys not allowed! Contact your administrator for more information.", + }; + } + + const paramsPasskey = new URLSearchParams({ + loginName: session.factors?.user?.loginName, + }); + if (command.requestId) { + paramsPasskey.append("requestId", command.requestId); + } + + if (command.organization || session.factors?.user?.organizationId) { + paramsPasskey.append( + "organization", + command.organization ?? session.factors?.user?.organizationId, + ); + } + + return { redirect: "/passkey?" + paramsPasskey }; + } + } else { + // prefer passkey in favor of other methods + if (methods.authMethodTypes.includes(AuthenticationMethodType.PASSKEY)) { + const passkeyParams = new URLSearchParams({ + loginName: session.factors?.user?.loginName, + altPassword: `${methods.authMethodTypes.includes(1)}`, // show alternative password option + }); + + if (command.requestId) { + passkeyParams.append("requestId", command.requestId); + } + + if (command.organization || session.factors?.user?.organizationId) { + passkeyParams.append( + "organization", + command.organization ?? session.factors?.user?.organizationId, + ); + } + + return { redirect: "/passkey?" + passkeyParams }; + } else if ( + methods.authMethodTypes.includes(AuthenticationMethodType.IDP) + ) { + return redirectUserToIDP(userId); + } else if ( + methods.authMethodTypes.includes(AuthenticationMethodType.PASSWORD) + ) { + // user has no passkey setup and login settings allow passkeys + const paramsPasswordDefault = new URLSearchParams({ + loginName: session.factors?.user?.loginName, + }); + + if (command.requestId) { + paramsPasswordDefault.append("requestId", command.requestId); + } + + if (command.organization || session.factors?.user?.organizationId) { + paramsPasswordDefault.append( + "organization", + command.organization ?? session.factors?.user?.organizationId, + ); + } + + return { + redirect: "/password?" + paramsPasswordDefault, + }; + } + } + } + + // user not found, check if register is enabled on instance / organization context + if ( + loginSettingsByContext?.allowRegister && + !loginSettingsByContext?.allowUsernamePassword + ) { + const resp = await redirectUserToSingleIDPIfAvailable(); + if (resp) { + return resp; + } + return { error: "User not found in the system" }; + } else if ( + loginSettingsByContext?.allowRegister && + loginSettingsByContext?.allowUsernamePassword + ) { + let orgToRegisterOn: string | undefined = command.organization; + + if ( + !loginSettingsByContext?.ignoreUnknownUsernames && + !orgToRegisterOn && + command.loginName && + ORG_SUFFIX_REGEX.test(command.loginName) + ) { + const matched = ORG_SUFFIX_REGEX.exec(command.loginName); + const suffix = matched?.[1] ?? ""; + + // this just returns orgs where the suffix is set as primary domain + const orgs = await getOrgsByDomain({ + serviceUrl, + domain: suffix, + }); + const orgToCheckForDiscovery = + orgs.result && orgs.result.length === 1 ? orgs.result[0].id : undefined; + + const orgLoginSettings = await getLoginSettings({ + serviceUrl, + organization: orgToCheckForDiscovery, + }); + if (orgLoginSettings?.allowDomainDiscovery) { + orgToRegisterOn = orgToCheckForDiscovery; + } + } + + // do not register user if ignoreUnknownUsernames is set + if (orgToRegisterOn && !loginSettingsByContext?.ignoreUnknownUsernames) { + const params = new URLSearchParams({ organization: orgToRegisterOn }); + + if (command.requestId) { + params.set("requestId", command.requestId); + } + + if (command.loginName) { + params.set("email", command.loginName); + } + + return { redirect: "/register?" + params }; + } + } + + if (loginSettingsByContext?.ignoreUnknownUsernames) { + const paramsPasswordDefault = new URLSearchParams({ + loginName: command.loginName, + }); + + if (command.requestId) { + paramsPasswordDefault.append("requestId", command.requestId); + } + + if (command.organization) { + paramsPasswordDefault.append("organization", command.organization); + } + + return { redirect: "/password?" + paramsPasswordDefault }; + } + + // fallbackToPassword + + return { error: "User not found in the system" }; +} diff --git a/apps/login/src/lib/server/oidc.ts b/apps/login/src/lib/server/oidc.ts new file mode 100644 index 0000000000..36a31fe419 --- /dev/null +++ b/apps/login/src/lib/server/oidc.ts @@ -0,0 +1,15 @@ +"use server"; + +import { getDeviceAuthorizationRequest as zitadelGetDeviceAuthorizationRequest } from "@/lib/zitadel"; +import { headers } from "next/headers"; +import { getServiceUrlFromHeaders } from "../service-url"; + +export async function getDeviceAuthorizationRequest(userCode: string) { + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + + return zitadelGetDeviceAuthorizationRequest({ + serviceUrl, + userCode, + }); +} diff --git a/apps/login/src/lib/server/passkeys.ts b/apps/login/src/lib/server/passkeys.ts new file mode 100644 index 0000000000..ca603471ab --- /dev/null +++ b/apps/login/src/lib/server/passkeys.ts @@ -0,0 +1,286 @@ +"use server"; + +import { + createPasskeyRegistrationLink, + getLoginSettings, + getSession, + getUserByID, + listAuthenticationMethodTypes, + registerPasskey, + verifyPasskeyRegistration as zitadelVerifyPasskeyRegistration, +} from "@/lib/zitadel"; +import { create, Duration, Timestamp, timestampDate } from "@zitadel/client"; +import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; +import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; +import { + RegisterPasskeyResponse, + VerifyPasskeyRegistrationRequestSchema, +} from "@zitadel/proto/zitadel/user/v2/user_service_pb"; +import { headers } from "next/headers"; +import { userAgent } from "next/server"; +import { getNextUrl } from "../client"; +import { + getMostRecentSessionCookie, + getSessionCookieById, + getSessionCookieByLoginName, +} from "../cookies"; +import { getServiceUrlFromHeaders } from "../service-url"; +import { + checkEmailVerification, + checkUserVerification, +} from "../verify-helper"; +import { setSessionAndUpdateCookie } from "./cookie"; + +type VerifyPasskeyCommand = { + passkeyId: string; + passkeyName?: string; + publicKeyCredential: any; + sessionId: string; +}; + +type RegisterPasskeyCommand = { + sessionId: string; +}; + +function isSessionValid(session: Partial): { + valid: boolean; + verifiedAt?: Timestamp; +} { + const validPassword = session?.factors?.password?.verifiedAt; + const validPasskey = session?.factors?.webAuthN?.verifiedAt; + const stillValid = session.expirationDate + ? timestampDate(session.expirationDate) > new Date() + : true; + + const verifiedAt = validPassword || validPasskey; + const valid = !!((validPassword || validPasskey) && stillValid); + + return { valid, verifiedAt }; +} + +export async function registerPasskeyLink( + command: RegisterPasskeyCommand, +): Promise { + const { sessionId } = command; + + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + const host = _headers.get("host"); + + if (!host) { + throw new Error("Could not get domain"); + } + + const sessionCookie = await getSessionCookieById({ sessionId }); + const session = await getSession({ + serviceUrl, + sessionId: sessionCookie.id, + sessionToken: sessionCookie.token, + }); + + if (!session?.session?.factors?.user?.id) { + return { error: "Could not determine user from session" }; + } + + const sessionValid = isSessionValid(session.session); + + if (!sessionValid) { + const authmethods = await listAuthenticationMethodTypes({ + serviceUrl, + userId: session.session.factors.user.id, + }); + + // if the user has no authmethods set, we need to check if the user was verified + if (authmethods.authMethodTypes.length !== 0) { + return { + error: + "You have to authenticate or have a valid User Verification Check", + }; + } + + // check if a verification was done earlier + const hasValidUserVerificationCheck = await checkUserVerification( + session.session.factors.user.id, + ); + + if (!hasValidUserVerificationCheck) { + return { error: "User Verification Check has to be done" }; + } + } + + const [hostname] = host.split(":"); + + if (!hostname) { + throw new Error("Could not get hostname"); + } + + const userId = session?.session?.factors?.user?.id; + + if (!userId) { + throw new Error("Could not get session"); + } + // TODO: add org context + + // use session token to add the passkey + const registerLink = await createPasskeyRegistrationLink({ + serviceUrl, + userId, + }); + + if (!registerLink.code) { + throw new Error("Missing code in response"); + } + + return registerPasskey({ + serviceUrl, + userId, + code: registerLink.code, + domain: hostname, + }); +} + +export async function verifyPasskeyRegistration(command: VerifyPasskeyCommand) { + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + + // if no name is provided, try to generate one from the user agent + let passkeyName = command.passkeyName; + if (!passkeyName) { + const headersList = await headers(); + const userAgentStructure = { headers: headersList }; + const { browser, device, os } = userAgent(userAgentStructure); + + passkeyName = `${device.vendor ?? ""} ${device.model ?? ""}${ + device.vendor || device.model ? ", " : "" + }${os.name}${os.name ? ", " : ""}${browser.name}`; + } + + const sessionCookie = await getSessionCookieById({ + sessionId: command.sessionId, + }); + const session = await getSession({ + serviceUrl, + sessionId: sessionCookie.id, + sessionToken: sessionCookie.token, + }); + const userId = session?.session?.factors?.user?.id; + + if (!userId) { + throw new Error("Could not get session"); + } + + return zitadelVerifyPasskeyRegistration({ + serviceUrl, + request: create(VerifyPasskeyRegistrationRequestSchema, { + passkeyId: command.passkeyId, + publicKeyCredential: command.publicKeyCredential, + passkeyName, + userId, + }), + }); +} + +type SendPasskeyCommand = { + loginName?: string; + sessionId?: string; + organization?: string; + checks?: Checks; + requestId?: string; + lifetime?: Duration; +}; + +export async function sendPasskey(command: SendPasskeyCommand) { + let { loginName, sessionId, organization, checks, requestId } = command; + const recentSession = sessionId + ? await getSessionCookieById({ sessionId }) + : loginName + ? await getSessionCookieByLoginName({ loginName, organization }) + : await getMostRecentSessionCookie(); + + if (!recentSession) { + return { + error: "Could not find session", + }; + } + + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + + const loginSettings = await getLoginSettings({ + serviceUrl, + organization, + }); + + let lifetime = checks?.webAuthN + ? loginSettings?.multiFactorCheckLifetime // TODO different lifetime for webauthn u2f/passkey + : checks?.otpEmail || checks?.otpSms + ? loginSettings?.secondFactorCheckLifetime + : undefined; + + if (!lifetime) { + console.warn("No passkey lifetime provided, defaulting to 24 hours"); + + lifetime = { + seconds: BigInt(60 * 60 * 24), // default to 24 hours + nanos: 0, + } as Duration; + } + + const session = await setSessionAndUpdateCookie({ + recentCookie: recentSession, + checks, + requestId, + lifetime, + }); + + if (!session || !session?.factors?.user?.id) { + return { error: "Could not update session" }; + } + + const userResponse = await getUserByID({ + serviceUrl, + userId: session?.factors?.user?.id, + }); + + if (!userResponse.user) { + return { error: "User not found in the system" }; + } + + const humanUser = + userResponse.user.type.case === "human" + ? userResponse.user.type.value + : undefined; + + const emailVerificationCheck = checkEmailVerification( + session, + humanUser, + organization, + requestId, + ); + + if (emailVerificationCheck?.redirect) { + return emailVerificationCheck; + } + + const url = + requestId && session.id + ? await getNextUrl( + { + sessionId: session.id, + requestId: requestId, + organization: organization, + }, + loginSettings?.defaultRedirectUri, + ) + : session?.factors?.user?.loginName + ? await getNextUrl( + { + loginName: session.factors.user.loginName, + organization: organization, + }, + loginSettings?.defaultRedirectUri, + ) + : null; + + return { redirect: url }; +} diff --git a/apps/login/src/lib/server/password.ts b/apps/login/src/lib/server/password.ts new file mode 100644 index 0000000000..013255d06f --- /dev/null +++ b/apps/login/src/lib/server/password.ts @@ -0,0 +1,478 @@ +"use server"; + +import { + createSessionAndUpdateCookie, + setSessionAndUpdateCookie, +} from "@/lib/server/cookie"; +import { + getLockoutSettings, + getLoginSettings, + getPasswordExpirySettings, + getSession, + getUserByID, + listAuthenticationMethodTypes, + listUsers, + passwordReset, + setPassword, + setUserPassword, +} from "@/lib/zitadel"; +import { ConnectError, create, Duration } from "@zitadel/client"; +import { createUserServiceClient } from "@zitadel/client/v2"; +import { + Checks, + ChecksSchema, +} from "@zitadel/proto/zitadel/session/v2/session_service_pb"; +import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; +import { User, UserState } from "@zitadel/proto/zitadel/user/v2/user_pb"; +import { + AuthenticationMethodType, + SetPasswordRequestSchema, +} from "@zitadel/proto/zitadel/user/v2/user_service_pb"; +import { headers } from "next/headers"; +import { getNextUrl } from "../client"; +import { getSessionCookieById, getSessionCookieByLoginName } from "../cookies"; +import { getServiceUrlFromHeaders } from "../service-url"; +import { + checkEmailVerification, + checkMFAFactors, + checkPasswordChangeRequired, + checkUserVerification, +} from "../verify-helper"; +import { createServerTransport } from "../zitadel"; + +type ResetPasswordCommand = { + loginName: string; + organization?: string; + requestId?: string; +}; + +export async function resetPassword(command: ResetPasswordCommand) { + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + const host = _headers.get("host"); + + if (!host || typeof host !== "string") { + throw new Error("No host found"); + } + + const users = await listUsers({ + serviceUrl, + loginName: command.loginName, + organizationId: command.organization, + }); + + if ( + !users.details || + users.details.totalResult !== BigInt(1) || + !users.result[0].userId + ) { + return { error: "Could not send Password Reset Link" }; + } + const userId = users.result[0].userId; + + const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? ""; + + return passwordReset({ + serviceUrl, + userId, + urlTemplate: + `${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/password/set?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` + + (command.requestId ? `&requestId=${command.requestId}` : ""), + }); +} + +export type UpdateSessionCommand = { + loginName: string; + organization?: string; + checks: Checks; + requestId?: string; +}; + +export async function sendPassword(command: UpdateSessionCommand) { + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + + let sessionCookie = await getSessionCookieByLoginName({ + loginName: command.loginName, + organization: command.organization, + }).catch((error) => { + console.warn("Ignored error:", error); + }); + + let session; + let user: User; + let loginSettings: LoginSettings | undefined; + + if (!sessionCookie) { + const users = await listUsers({ + serviceUrl, + loginName: command.loginName, + organizationId: command.organization, + }); + + if (users.details?.totalResult == BigInt(1) && users.result[0].userId) { + user = users.result[0]; + + const checks = create(ChecksSchema, { + user: { search: { case: "userId", value: users.result[0].userId } }, + password: { password: command.checks.password?.password }, + }); + + loginSettings = await getLoginSettings({ + serviceUrl, + organization: command.organization, + }); + + try { + session = await createSessionAndUpdateCookie({ + checks, + requestId: command.requestId, + lifetime: loginSettings?.passwordCheckLifetime, + }); + } catch (error: any) { + if ("failedAttempts" in error && error.failedAttempts) { + const lockoutSettings = await getLockoutSettings({ + serviceUrl, + orgId: command.organization, + }); + + return { + error: + `Failed to authenticate. You had ${error.failedAttempts} of ${lockoutSettings?.maxPasswordAttempts} password attempts.` + + (lockoutSettings?.maxPasswordAttempts && + error.failedAttempts >= lockoutSettings?.maxPasswordAttempts + ? "Contact your administrator to unlock your account" + : ""), + }; + } + return { error: "Could not create session for user" }; + } + } + + // this is a fake error message to hide that the user does not even exist + return { error: "Could not verify password" }; + } else { + loginSettings = await getLoginSettings({ + serviceUrl, + organization: sessionCookie.organization, + }); + + if (!loginSettings) { + return { error: "Could not load login settings" }; + } + + let lifetime = loginSettings.passwordCheckLifetime; + + if (!lifetime) { + console.warn("No password lifetime provided, defaulting to 24 hours"); + lifetime = { + seconds: BigInt(60 * 60 * 24), // default to 24 hours + nanos: 0, + } as Duration; + } + + try { + session = await setSessionAndUpdateCookie({ + recentCookie: sessionCookie, + checks: command.checks, + requestId: command.requestId, + lifetime, + }); + } catch (error: any) { + if ("failedAttempts" in error && error.failedAttempts) { + const lockoutSettings = await getLockoutSettings({ + serviceUrl, + orgId: command.organization, + }); + + return { + error: + `Failed to authenticate. You had ${error.failedAttempts} of ${lockoutSettings?.maxPasswordAttempts} password attempts.` + + (lockoutSettings?.maxPasswordAttempts && + error.failedAttempts >= lockoutSettings?.maxPasswordAttempts + ? " Contact your administrator to unlock your account" + : ""), + }; + } + throw error; + } + + if (!session?.factors?.user?.id) { + return { error: "Could not create session for user" }; + } + + const userResponse = await getUserByID({ + serviceUrl, + userId: session?.factors?.user?.id, + }); + + if (!userResponse.user) { + return { error: "User not found in the system" }; + } + + user = userResponse.user; + } + + if (!loginSettings) { + loginSettings = await getLoginSettings({ + serviceUrl, + organization: + command.organization ?? session.factors?.user?.organizationId, + }); + } + + if (!session?.factors?.user?.id || !sessionCookie) { + return { error: "Could not create session for user" }; + } + + const humanUser = user.type.case === "human" ? user.type.value : undefined; + + const expirySettings = await getPasswordExpirySettings({ + serviceUrl, + orgId: command.organization ?? session.factors?.user?.organizationId, + }); + + // check if the user has to change password first + const passwordChangedCheck = checkPasswordChangeRequired( + expirySettings, + session, + humanUser, + command.organization, + command.requestId, + ); + + if (passwordChangedCheck?.redirect) { + return passwordChangedCheck; + } + + // throw error if user is in initial state here and do not continue + if (user.state === UserState.INITIAL) { + return { error: "Initial User not supported" }; + } + + // check to see if user was verified + const emailVerificationCheck = checkEmailVerification( + session, + humanUser, + command.organization, + command.requestId, + ); + + if (emailVerificationCheck?.redirect) { + return emailVerificationCheck; + } + + // if password, check if user has MFA methods + let authMethods; + if (command.checks && command.checks.password && session.factors?.user?.id) { + const response = await listAuthenticationMethodTypes({ + serviceUrl, + userId: session.factors.user.id, + }); + if (response.authMethodTypes && response.authMethodTypes.length) { + authMethods = response.authMethodTypes; + } + } + + if (!authMethods) { + return { error: "Could not verify password!" }; + } + + const mfaFactorCheck = await checkMFAFactors( + serviceUrl, + session, + loginSettings, + authMethods, + command.organization, + command.requestId, + ); + + if (mfaFactorCheck?.redirect) { + return mfaFactorCheck; + } + + if (command.requestId && session.id) { + const nextUrl = await getNextUrl( + { + sessionId: session.id, + requestId: command.requestId, + organization: + command.organization ?? session.factors?.user?.organizationId, + }, + loginSettings?.defaultRedirectUri, + ); + + return { redirect: nextUrl }; + } + + const url = await getNextUrl( + { + loginName: session.factors.user.loginName, + organization: session.factors?.user?.organizationId, + }, + loginSettings?.defaultRedirectUri, + ); + + return { redirect: url }; +} + +// this function lets users with code set a password or users with valid User Verification Check +export async function changePassword(command: { + code?: string; + userId: string; + password: string; +}) { + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + + // check for init state + const { user } = await getUserByID({ + serviceUrl, + userId: command.userId, + }); + + if (!user || user.userId !== command.userId) { + return { error: "Could not send Password Reset Link" }; + } + const userId = user.userId; + + if (user.state === UserState.INITIAL) { + return { error: "User Initial State is not supported" }; + } + + // check if the user has no password set in order to set a password + if (!command.code) { + const authmethods = await listAuthenticationMethodTypes({ + serviceUrl, + userId, + }); + + // if the user has no authmethods set, we need to check if the user was verified + if (authmethods.authMethodTypes.length !== 0) { + return { + error: + "You have to provide a code or have a valid User Verification Check", + }; + } + + // check if a verification was done earlier + const hasValidUserVerificationCheck = await checkUserVerification( + user.userId, + ); + + if (!hasValidUserVerificationCheck) { + return { error: "User Verification Check has to be done" }; + } + } + + return setUserPassword({ + serviceUrl, + userId, + password: command.password, + code: command.code, + }); +} + +type CheckSessionAndSetPasswordCommand = { + sessionId: string; + password: string; +}; + +export async function checkSessionAndSetPassword({ + sessionId, + password, +}: CheckSessionAndSetPasswordCommand) { + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + + const sessionCookie = await getSessionCookieById({ sessionId }); + + const { session } = await getSession({ + serviceUrl, + sessionId: sessionCookie.id, + sessionToken: sessionCookie.token, + }); + + if (!session || !session.factors?.user?.id) { + return { error: "Could not load session" }; + } + + const payload = create(SetPasswordRequestSchema, { + userId: session.factors.user.id, + newPassword: { + password, + }, + }); + + // check if the user has no password set in order to set a password + const authmethods = await listAuthenticationMethodTypes({ + serviceUrl, + userId: session.factors.user.id, + }); + + if (!authmethods) { + return { error: "Could not load auth methods" }; + } + + const requiredAuthMethodsForForceMFA = [ + AuthenticationMethodType.OTP_EMAIL, + AuthenticationMethodType.OTP_SMS, + AuthenticationMethodType.TOTP, + AuthenticationMethodType.U2F, + ]; + + const hasNoMFAMethods = requiredAuthMethodsForForceMFA.every( + (method) => !authmethods.authMethodTypes.includes(method), + ); + + const loginSettings = await getLoginSettings({ + serviceUrl, + organization: session.factors.user.organizationId, + }); + + const forceMfa = !!( + loginSettings?.forceMfa || loginSettings?.forceMfaLocalOnly + ); + + // if the user has no MFA but MFA is enforced, we can set a password otherwise we use the token of the user + if (forceMfa && hasNoMFAMethods) { + return setPassword({ serviceUrl, payload }).catch((error) => { + // throw error if failed precondition (ex. User is not yet initialized) + if (error.code === 9 && error.message) { + return { error: "Failed precondition" }; + } else { + throw error; + } + }); + } else { + const transport = async (serviceUrl: string, token: string) => { + return createServerTransport(token, serviceUrl); + }; + + const myUserService = async (serviceUrl: string, sessionToken: string) => { + const transportPromise = await transport(serviceUrl, sessionToken); + return createUserServiceClient(transportPromise); + }; + + const selfService = await myUserService( + serviceUrl, + `${sessionCookie.token}`, + ); + + return selfService + .setPassword( + { + userId: session.factors.user.id, + newPassword: { password, changeRequired: false }, + }, + {}, + ) + .catch((error: ConnectError) => { + console.log(error); + if (error.code === 7) { + return { error: "Session is not valid." }; + } + throw error; + }); + } +} diff --git a/apps/login/src/lib/server/register.ts b/apps/login/src/lib/server/register.ts new file mode 100644 index 0000000000..f84b4c8d51 --- /dev/null +++ b/apps/login/src/lib/server/register.ts @@ -0,0 +1,233 @@ +"use server"; + +import { + createSessionAndUpdateCookie, + createSessionForIdpAndUpdateCookie, +} from "@/lib/server/cookie"; +import { + addHumanUser, + addIDPLink, + getLoginSettings, + getUserByID, +} from "@/lib/zitadel"; +import { create } from "@zitadel/client"; +import { Factors } from "@zitadel/proto/zitadel/session/v2/session_pb"; +import { + ChecksJson, + ChecksSchema, +} from "@zitadel/proto/zitadel/session/v2/session_service_pb"; +import { headers } from "next/headers"; +import { getNextUrl } from "../client"; +import { getServiceUrlFromHeaders } from "../service-url"; +import { checkEmailVerification } from "../verify-helper"; + +type RegisterUserCommand = { + email: string; + firstName: string; + lastName: string; + password?: string; + organization: string; + requestId?: string; +}; + +export type RegisterUserResponse = { + userId: string; + sessionId: string; + factors: Factors | undefined; +}; +export async function registerUser(command: RegisterUserCommand) { + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + const host = _headers.get("host"); + + if (!host || typeof host !== "string") { + throw new Error("No host found"); + } + + const addResponse = await addHumanUser({ + serviceUrl, + email: command.email, + firstName: command.firstName, + lastName: command.lastName, + password: command.password ? command.password : undefined, + organization: command.organization, + }); + + if (!addResponse) { + return { error: "Could not create user" }; + } + + const loginSettings = await getLoginSettings({ + serviceUrl, + organization: command.organization, + }); + + let checkPayload: any = { + user: { search: { case: "userId", value: addResponse.userId } }, + }; + + if (command.password) { + checkPayload = { + ...checkPayload, + password: { password: command.password }, + } as ChecksJson; + } + + const checks = create(ChecksSchema, checkPayload); + + const session = await createSessionAndUpdateCookie({ + checks, + requestId: command.requestId, + lifetime: command.password + ? loginSettings?.passwordCheckLifetime + : undefined, + }); + + if (!session || !session.factors?.user) { + return { error: "Could not create session" }; + } + + if (!command.password) { + const params = new URLSearchParams({ + loginName: session.factors.user.loginName, + organization: session.factors.user.organizationId, + }); + + if (command.requestId) { + params.append("requestId", command.requestId); + } + + return { redirect: "/passkey/set?" + params }; + } else { + const userResponse = await getUserByID({ + serviceUrl, + userId: session?.factors?.user?.id, + }); + + if (!userResponse.user) { + return { error: "User not found in the system" }; + } + + const humanUser = + userResponse.user.type.case === "human" + ? userResponse.user.type.value + : undefined; + + const emailVerificationCheck = checkEmailVerification( + session, + humanUser, + session.factors.user.organizationId, + command.requestId, + ); + + if (emailVerificationCheck?.redirect) { + return emailVerificationCheck; + } + + const url = await getNextUrl( + command.requestId && session.id + ? { + sessionId: session.id, + requestId: command.requestId, + organization: session.factors.user.organizationId, + } + : { + loginName: session.factors.user.loginName, + organization: session.factors.user.organizationId, + }, + loginSettings?.defaultRedirectUri, + ); + + return { redirect: url }; + } +} + +type RegisterUserAndLinkToIDPommand = { + email: string; + firstName: string; + lastName: string; + organization: string; + requestId?: string; + idpIntent: { + idpIntentId: string; + idpIntentToken: string; + }; + idpUserId: string; + idpId: string; + idpUserName: string; +}; + +export type registerUserAndLinkToIDPResponse = { + userId: string; + sessionId: string; + factors: Factors | undefined; +}; +export async function registerUserAndLinkToIDP( + command: RegisterUserAndLinkToIDPommand, +) { + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + const host = _headers.get("host"); + + if (!host || typeof host !== "string") { + throw new Error("No host found"); + } + + const addResponse = await addHumanUser({ + serviceUrl, + email: command.email, + firstName: command.firstName, + lastName: command.lastName, + organization: command.organization, + }); + + if (!addResponse) { + return { error: "Could not create user" }; + } + + const loginSettings = await getLoginSettings({ + serviceUrl, + organization: command.organization, + }); + + const idpLink = await addIDPLink({ + serviceUrl, + idp: { + id: command.idpId, + userId: command.idpUserId, + userName: command.idpUserName, + }, + userId: addResponse.userId, + }); + + if (!idpLink) { + return { error: "Could not link IDP to user" }; + } + + const session = await createSessionForIdpAndUpdateCookie({ + requestId: command.requestId, + userId: addResponse.userId, // the user we just created + idpIntent: command.idpIntent, + lifetime: loginSettings?.externalLoginCheckLifetime, + }); + + if (!session || !session.factors?.user) { + return { error: "Could not create session" }; + } + + const url = await getNextUrl( + command.requestId && session.id + ? { + sessionId: session.id, + requestId: command.requestId, + organization: session.factors.user.organizationId, + } + : { + loginName: session.factors.user.loginName, + organization: session.factors.user.organizationId, + }, + loginSettings?.defaultRedirectUri, + ); + + return { redirect: url }; +} diff --git a/apps/login/src/lib/server/session.ts b/apps/login/src/lib/server/session.ts new file mode 100644 index 0000000000..957c89ad81 --- /dev/null +++ b/apps/login/src/lib/server/session.ts @@ -0,0 +1,229 @@ +"use server"; + +import { setSessionAndUpdateCookie } from "@/lib/server/cookie"; +import { + deleteSession, + getLoginSettings, + getSecuritySettings, + humanMFAInitSkipped, + listAuthenticationMethodTypes, +} from "@/lib/zitadel"; +import { Duration } from "@zitadel/client"; +import { RequestChallenges } from "@zitadel/proto/zitadel/session/v2/challenge_pb"; +import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; +import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; +import { headers } from "next/headers"; +import { getNextUrl } from "../client"; +import { + getMostRecentSessionCookie, + getSessionCookieById, + getSessionCookieByLoginName, + removeSessionFromCookie, +} from "../cookies"; +import { getServiceUrlFromHeaders } from "../service-url"; + +export async function skipMFAAndContinueWithNextUrl({ + userId, + requestId, + loginName, + sessionId, + organization, +}: { + userId: string; + loginName?: string; + sessionId?: string; + requestId?: string; + organization?: string; +}) { + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + + const loginSettings = await getLoginSettings({ + serviceUrl, + organization: organization, + }); + + await humanMFAInitSkipped({ serviceUrl, userId }); + + const url = + requestId && sessionId + ? await getNextUrl( + { + sessionId: sessionId, + requestId: requestId, + organization: organization, + }, + loginSettings?.defaultRedirectUri, + ) + : loginName + ? await getNextUrl( + { + loginName: loginName, + organization: organization, + }, + loginSettings?.defaultRedirectUri, + ) + : null; + if (url) { + return { redirect: url }; + } +} + +export async function continueWithSession({ + requestId, + ...session +}: Session & { requestId?: string }) { + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + + const loginSettings = await getLoginSettings({ + serviceUrl, + organization: session.factors?.user?.organizationId, + }); + + const url = + requestId && session.id && session.factors?.user + ? await getNextUrl( + { + sessionId: session.id, + requestId: requestId, + organization: session.factors.user.organizationId, + }, + loginSettings?.defaultRedirectUri, + ) + : session.factors?.user + ? await getNextUrl( + { + loginName: session.factors.user.loginName, + organization: session.factors.user.organizationId, + }, + loginSettings?.defaultRedirectUri, + ) + : null; + if (url) { + return { redirect: url }; + } +} + +export type UpdateSessionCommand = { + loginName?: string; + sessionId?: string; + organization?: string; + checks?: Checks; + requestId?: string; + challenges?: RequestChallenges; + lifetime?: Duration; +}; + +export async function updateSession(options: UpdateSessionCommand) { + let { loginName, sessionId, organization, checks, requestId, challenges } = + options; + const recentSession = sessionId + ? await getSessionCookieById({ sessionId }) + : loginName + ? await getSessionCookieByLoginName({ loginName, organization }) + : await getMostRecentSessionCookie(); + + if (!recentSession) { + return { + error: "Could not find session", + }; + } + + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + const host = _headers.get("host"); + + if (!host) { + return { error: "Could not get host" }; + } + + if ( + host && + challenges && + challenges.webAuthN && + !challenges.webAuthN.domain + ) { + const [hostname] = host.split(":"); + + challenges.webAuthN.domain = hostname; + } + + const loginSettings = await getLoginSettings({ + serviceUrl, + organization, + }); + + let lifetime = checks?.webAuthN + ? loginSettings?.multiFactorCheckLifetime // TODO different lifetime for webauthn u2f/passkey + : checks?.otpEmail || checks?.otpSms + ? loginSettings?.secondFactorCheckLifetime + : undefined; + + if (!lifetime) { + console.warn("No lifetime provided for session, defaulting to 24 hours"); + lifetime = { + seconds: BigInt(60 * 60 * 24), // default to 24 hours + nanos: 0, + } as Duration; + } + + const session = await setSessionAndUpdateCookie({ + recentCookie: recentSession, + checks, + challenges, + requestId, + lifetime, + }); + + if (!session) { + return { error: "Could not update session" }; + } + + // if password, check if user has MFA methods + let authMethods; + if (checks && checks.password && session.factors?.user?.id) { + const response = await listAuthenticationMethodTypes({ + serviceUrl, + userId: session.factors.user.id, + }); + if (response.authMethodTypes && response.authMethodTypes.length) { + authMethods = response.authMethodTypes; + } + } + + return { + sessionId: session.id, + factors: session.factors, + challenges: session.challenges, + authMethods, + }; +} + +type ClearSessionOptions = { + sessionId: string; +}; + +export async function clearSession(options: ClearSessionOptions) { + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + + const { sessionId } = options; + + const sessionCookie = await getSessionCookieById({ sessionId }); + + const deleteResponse = await deleteSession({ + serviceUrl, + sessionId: sessionCookie.id, + sessionToken: sessionCookie.token, + }); + + const securitySettings = await getSecuritySettings({ serviceUrl }); + const sameSite = securitySettings?.embeddedIframe?.enabled ? "none" : true; + + if (!deleteResponse) { + throw new Error("Could not delete session"); + } + + return removeSessionFromCookie({ session: sessionCookie, sameSite }); +} diff --git a/apps/login/src/lib/server/u2f.ts b/apps/login/src/lib/server/u2f.ts new file mode 100644 index 0000000000..70eac14e55 --- /dev/null +++ b/apps/login/src/lib/server/u2f.ts @@ -0,0 +1,103 @@ +"use server"; + +import { getSession, registerU2F, verifyU2FRegistration } from "@/lib/zitadel"; +import { create } from "@zitadel/client"; +import { VerifyU2FRegistrationRequestSchema } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; +import { headers } from "next/headers"; +import { userAgent } from "next/server"; +import { getSessionCookieById } from "../cookies"; +import { getServiceUrlFromHeaders } from "../service-url"; + +type RegisterU2FCommand = { + sessionId: string; +}; + +type VerifyU2FCommand = { + u2fId: string; + passkeyName?: string; + publicKeyCredential: any; + sessionId: string; +}; + +export async function addU2F(command: RegisterU2FCommand) { + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + const host = _headers.get("host"); + + if (!host || typeof host !== "string") { + throw new Error("No host found"); + } + + const sessionCookie = await getSessionCookieById({ + sessionId: command.sessionId, + }); + + if (!sessionCookie) { + return { error: "Could not get session" }; + } + + const session = await getSession({ + serviceUrl, + sessionId: sessionCookie.id, + sessionToken: sessionCookie.token, + }); + + const [hostname] = host.split(":"); + + if (!hostname) { + throw new Error("Could not get hostname"); + } + + const userId = session?.session?.factors?.user?.id; + + if (!session || !userId) { + return { error: "Could not get session" }; + } + + return registerU2F({ serviceUrl, userId, domain: hostname }); +} + +export async function verifyU2F(command: VerifyU2FCommand) { + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + const host = _headers.get("host"); + + if (!host || typeof host !== "string") { + throw new Error("No host found"); + } + + let passkeyName = command.passkeyName; + if (!passkeyName) { + const headersList = await headers(); + const userAgentStructure = { headers: headersList }; + const { browser, device, os } = userAgent(userAgentStructure); + + passkeyName = `${device.vendor ?? ""} ${device.model ?? ""}${ + device.vendor || device.model ? ", " : "" + }${os.name}${os.name ? ", " : ""}${browser.name}`; + } + const sessionCookie = await getSessionCookieById({ + sessionId: command.sessionId, + }); + + const session = await getSession({ + serviceUrl, + sessionId: sessionCookie.id, + sessionToken: sessionCookie.token, + }); + + const userId = session?.session?.factors?.user?.id; + + if (!userId) { + return { error: "Could not get session" }; + } + + const request = create(VerifyU2FRegistrationRequestSchema, { + u2fId: command.u2fId, + publicKeyCredential: command.publicKeyCredential, + tokenName: passkeyName, + userId, + }); + + return verifyU2FRegistration({ serviceUrl, request }); +} diff --git a/apps/login/src/lib/server/verify.ts b/apps/login/src/lib/server/verify.ts new file mode 100644 index 0000000000..cf60f739b3 --- /dev/null +++ b/apps/login/src/lib/server/verify.ts @@ -0,0 +1,329 @@ +"use server"; + +import { + createInviteCode, + getLoginSettings, + getSession, + getUserByID, + listAuthenticationMethodTypes, + verifyEmail, + verifyInviteCode, + verifyTOTPRegistration, + sendEmailCode as zitadelSendEmailCode, +} from "@/lib/zitadel"; +import crypto from "crypto"; + +import { create } from "@zitadel/client"; +import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; +import { ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; +import { cookies, headers } from "next/headers"; +import { getNextUrl } from "../client"; +import { getSessionCookieByLoginName } from "../cookies"; +import { getOrSetFingerprintId } from "../fingerprint"; +import { getServiceUrlFromHeaders } from "../service-url"; +import { loadMostRecentSession } from "../session"; +import { checkMFAFactors } from "../verify-helper"; +import { createSessionAndUpdateCookie } from "./cookie"; + +export async function verifyTOTP( + code: string, + loginName?: string, + organization?: string, +) { + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + + return loadMostRecentSession({ + serviceUrl, + sessionParams: { + loginName, + organization, + }, + }).then((session) => { + if (session?.factors?.user?.id) { + return verifyTOTPRegistration({ + serviceUrl, + code, + userId: session.factors.user.id, + }); + } else { + throw Error("No user id found in session."); + } + }); +} + +type VerifyUserByEmailCommand = { + userId: string; + loginName?: string; // to determine already existing session + organization?: string; + code: string; + isInvite: boolean; + requestId?: string; +}; + +export async function sendVerification(command: VerifyUserByEmailCommand) { + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + + const verifyResponse = command.isInvite + ? await verifyInviteCode({ + serviceUrl, + userId: command.userId, + verificationCode: command.code, + }).catch((error) => { + console.warn(error); + return { error: "Could not verify invite" }; + }) + : await verifyEmail({ + serviceUrl, + userId: command.userId, + verificationCode: command.code, + }).catch((error) => { + console.warn(error); + return { error: "Could not verify email" }; + }); + + if ("error" in verifyResponse) { + return verifyResponse; + } + + if (!verifyResponse) { + return { error: "Could not verify" }; + } + + let session: Session | undefined; + const userResponse = await getUserByID({ + serviceUrl, + userId: command.userId, + }); + + if (!userResponse || !userResponse.user) { + return { error: "Could not load user" }; + } + + const user = userResponse.user; + + const sessionCookie = await getSessionCookieByLoginName({ + loginName: + "loginName" in command ? command.loginName : user.preferredLoginName, + organization: command.organization, + }).catch((error) => { + console.warn("Ignored error:", error); // checked later + }); + + if (sessionCookie) { + session = await getSession({ + serviceUrl, + sessionId: sessionCookie.id, + sessionToken: sessionCookie.token, + }).then((response) => { + if (response?.session) { + return response.session; + } + }); + } + + // load auth methods for user + const authMethodResponse = await listAuthenticationMethodTypes({ + serviceUrl, + userId: user.userId, + }); + + if (!authMethodResponse || !authMethodResponse.authMethodTypes) { + return { error: "Could not load possible authenticators" }; + } + + // if no authmethods are found on the user, redirect to set one up + if ( + authMethodResponse && + authMethodResponse.authMethodTypes && + authMethodResponse.authMethodTypes.length == 0 + ) { + if (!sessionCookie) { + const checks = create(ChecksSchema, { + user: { + search: { + case: "loginName", + value: userResponse.user.preferredLoginName, + }, + }, + }); + + session = await createSessionAndUpdateCookie({ + checks, + requestId: command.requestId, + }); + } + + if (!session) { + return { error: "Could not create session" }; + } + + const params = new URLSearchParams({ + sessionId: session.id, + }); + + if (session.factors?.user?.loginName) { + params.set("loginName", session.factors?.user?.loginName); + } + + // set hash of userId and userAgentId to prevent attacks, checks are done for users with invalid sessions and invalid userAgentId + const cookiesList = await cookies(); + const userAgentId = await getOrSetFingerprintId(); + + const verificationCheck = crypto + .createHash("sha256") + .update(`${user.userId}:${userAgentId}`) + .digest("hex"); + + await cookiesList.set({ + name: "verificationCheck", + value: verificationCheck, + httpOnly: true, + path: "/", + maxAge: 300, // 5 minutes + }); + + return { redirect: `/authenticator/set?${params}` }; + } + + // if no session found only show success page, + // if user is invited, recreate invite flow to not depend on session + if (!session?.factors?.user?.id) { + const verifySuccessParams = new URLSearchParams({}); + + if (command.userId) { + verifySuccessParams.set("userId", command.userId); + } + + if ( + ("loginName" in command && command.loginName) || + user.preferredLoginName + ) { + verifySuccessParams.set( + "loginName", + "loginName" in command && command.loginName + ? command.loginName + : user.preferredLoginName, + ); + } + if (command.requestId) { + verifySuccessParams.set("requestId", command.requestId); + } + if (command.organization) { + verifySuccessParams.set("organization", command.organization); + } + + return { redirect: `/verify/success?${verifySuccessParams}` }; + } + + const loginSettings = await getLoginSettings({ + serviceUrl, + organization: user.details?.resourceOwner, + }); + + // redirect to mfa factor if user has one, or redirect to set one up + const mfaFactorCheck = await checkMFAFactors( + serviceUrl, + session, + loginSettings, + authMethodResponse.authMethodTypes, + command.organization, + command.requestId, + ); + + if (mfaFactorCheck?.redirect) { + return mfaFactorCheck; + } + + // login user if no additional steps are required + if (command.requestId && session.id) { + const nextUrl = await getNextUrl( + { + sessionId: session.id, + requestId: command.requestId, + organization: + command.organization ?? session.factors?.user?.organizationId, + }, + loginSettings?.defaultRedirectUri, + ); + + return { redirect: nextUrl }; + } + + const url = await getNextUrl( + { + loginName: session.factors.user.loginName, + organization: session.factors?.user?.organizationId, + }, + loginSettings?.defaultRedirectUri, + ); + + return { redirect: url }; +} + +type resendVerifyEmailCommand = { + userId: string; + isInvite: boolean; + requestId?: string; +}; + +export async function resendVerification(command: resendVerifyEmailCommand) { + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + const host = _headers.get("host"); + + if (!host) { + return { error: "No host found" }; + } + + const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? ""; + + return command.isInvite + ? createInviteCode({ + serviceUrl, + userId: command.userId, + urlTemplate: + `${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}&invite=true` + + (command.requestId ? `&requestId=${command.requestId}` : ""), + }).catch((error) => { + if (error.code === 9) { + return { error: "User is already verified!" }; + } + return { error: "Could not resend invite" }; + }) + : zitadelSendEmailCode({ + userId: command.userId, + serviceUrl, + urlTemplate: + `${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` + + (command.requestId ? `&requestId=${command.requestId}` : ""), + }); +} + +type SendEmailCommand = { + userId: string; + urlTemplate: string; +}; + +export async function sendEmailCode(command: SendEmailCommand) { + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + + return zitadelSendEmailCode({ + serviceUrl, + userId: command.userId, + urlTemplate: command.urlTemplate, + }); +} + +export async function sendInviteEmailCode(command: SendEmailCommand) { + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + + return createInviteCode({ + serviceUrl, + userId: command.userId, + urlTemplate: command.urlTemplate, + }); +} diff --git a/apps/login/src/lib/service-url.ts b/apps/login/src/lib/service-url.ts new file mode 100644 index 0000000000..cc5e3f04c4 --- /dev/null +++ b/apps/login/src/lib/service-url.ts @@ -0,0 +1,58 @@ +import { ReadonlyHeaders } from "next/dist/server/web/spec-extension/adapters/headers"; +import { NextRequest } from "next/server"; + +/** + * Extracts the service url and region from the headers if used in a multitenant context (host, x-zitadel-forward-host header) + * or falls back to the ZITADEL_API_URL for a self hosting deployment + * or falls back to the host header for a self hosting deployment using custom domains + * @param headers + * @returns the service url and region from the headers + * @throws if the service url could not be determined + * + */ +export function getServiceUrlFromHeaders(headers: ReadonlyHeaders): { + serviceUrl: string; +} { + let instanceUrl; + + const forwardedHost = headers.get("x-zitadel-forward-host"); + // use the forwarded host if available (multitenant), otherwise fall back to the host of the deployment itself + if (forwardedHost) { + instanceUrl = forwardedHost; + instanceUrl = instanceUrl.startsWith("http://") + ? instanceUrl + : `https://${instanceUrl}`; + } else if (process.env.ZITADEL_API_URL) { + instanceUrl = process.env.ZITADEL_API_URL; + } else { + const host = headers.get("host"); + + if (host) { + const [hostname] = host.split(":"); + if (hostname !== "localhost") { + instanceUrl = host.startsWith("http") ? host : `https://${host}`; + } + } + } + + if (!instanceUrl) { + throw new Error("Service URL could not be determined"); + } + + return { + serviceUrl: instanceUrl, + }; +} + +export function constructUrl(request: NextRequest, path: string) { + const forwardedProto = request.headers.get("x-forwarded-proto") + ? `${request.headers.get("x-forwarded-proto")}:` + : request.nextUrl.protocol; + + const forwardedHost = + request.headers.get("x-zitadel-forward-host") ?? + request.headers.get("x-forwarded-host") ?? + request.headers.get("host"); + const basePath = process.env.NEXT_PUBLIC_BASE_PATH || ""; + return new URL(`${basePath}${path}`, `${forwardedProto}//${forwardedHost}`); +} diff --git a/apps/login/src/lib/service.ts b/apps/login/src/lib/service.ts new file mode 100644 index 0000000000..f7e81cc9d6 --- /dev/null +++ b/apps/login/src/lib/service.ts @@ -0,0 +1,49 @@ +import { createClientFor } from "@zitadel/client"; +import { IdentityProviderService } from "@zitadel/proto/zitadel/idp/v2/idp_service_pb"; +import { OIDCService } from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb"; +import { OrganizationService } from "@zitadel/proto/zitadel/org/v2/org_service_pb"; +import { SAMLService } from "@zitadel/proto/zitadel/saml/v2/saml_service_pb"; +import { SessionService } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; +import { SettingsService } from "@zitadel/proto/zitadel/settings/v2/settings_service_pb"; +import { UserService } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; +import { systemAPIToken } from "./api"; +import { createServerTransport } from "./zitadel"; + +type ServiceClass = + | typeof IdentityProviderService + | typeof UserService + | typeof OrganizationService + | typeof SessionService + | typeof OIDCService + | typeof SettingsService + | typeof SAMLService; + +export async function createServiceForHost( + service: T, + serviceUrl: string, +) { + let token; + + // if we are running in a multitenancy context, use the system user token + if ( + process.env.AUDIENCE && + process.env.SYSTEM_USER_ID && + process.env.SYSTEM_USER_PRIVATE_KEY + ) { + token = await systemAPIToken(); + } else if (process.env.ZITADEL_SERVICE_USER_TOKEN) { + token = process.env.ZITADEL_SERVICE_USER_TOKEN; + } + + if (!serviceUrl) { + throw new Error("No instance url found"); + } + + if (!token) { + throw new Error("No token found"); + } + + const transport = createServerTransport(token, serviceUrl); + + return createClientFor(service)(transport); +} diff --git a/apps/login/src/lib/session.ts b/apps/login/src/lib/session.ts new file mode 100644 index 0000000000..8c2548b8fb --- /dev/null +++ b/apps/login/src/lib/session.ts @@ -0,0 +1,193 @@ +import { timestampDate } from "@zitadel/client"; +import { AuthRequest } from "@zitadel/proto/zitadel/oidc/v2/authorization_pb"; +import { SAMLRequest } from "@zitadel/proto/zitadel/saml/v2/authorization_pb"; +import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; +import { GetSessionResponse } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; +import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; +import { getMostRecentCookieWithLoginname } from "./cookies"; +import { + getLoginSettings, + getSession, + listAuthenticationMethodTypes, +} from "./zitadel"; + +type LoadMostRecentSessionParams = { + serviceUrl: string; + sessionParams: { + loginName?: string; + organization?: string; + }; +}; + +export async function loadMostRecentSession({ + serviceUrl, + sessionParams, +}: LoadMostRecentSessionParams): Promise { + const recent = await getMostRecentCookieWithLoginname({ + loginName: sessionParams.loginName, + organization: sessionParams.organization, + }); + + return getSession({ + serviceUrl, + sessionId: recent.id, + sessionToken: recent.token, + }).then((resp: GetSessionResponse) => resp.session); +} + +/** + * mfa is required, session is not valid anymore (e.g. session expired, user logged out, etc.) + * to check for mfa for automatically selected session -> const response = await listAuthenticationMethodTypes(userId); + **/ +export async function isSessionValid({ + serviceUrl, + session, +}: { + serviceUrl: string; + session: Session; +}): Promise { + // session can't be checked without user + if (!session.factors?.user) { + console.warn("Session has no user"); + return false; + } + + let mfaValid = true; + + const authMethodTypes = await listAuthenticationMethodTypes({ + serviceUrl, + userId: session.factors.user.id, + }); + + const authMethods = authMethodTypes.authMethodTypes; + if (authMethods && authMethods.includes(AuthenticationMethodType.TOTP)) { + mfaValid = !!session.factors.totp?.verifiedAt; + if (!mfaValid) { + console.warn( + "Session has no valid totpEmail factor", + session.factors.totp?.verifiedAt, + ); + } + } else if ( + authMethods && + authMethods.includes(AuthenticationMethodType.OTP_EMAIL) + ) { + mfaValid = !!session.factors.otpEmail?.verifiedAt; + if (!mfaValid) { + console.warn( + "Session has no valid otpEmail factor", + session.factors.otpEmail?.verifiedAt, + ); + } + } else if ( + authMethods && + authMethods.includes(AuthenticationMethodType.OTP_SMS) + ) { + mfaValid = !!session.factors.otpSms?.verifiedAt; + if (!mfaValid) { + console.warn( + "Session has no valid otpSms factor", + session.factors.otpSms?.verifiedAt, + ); + } + } else if ( + authMethods && + authMethods.includes(AuthenticationMethodType.U2F) + ) { + mfaValid = !!session.factors.webAuthN?.verifiedAt; + if (!mfaValid) { + console.warn( + "Session has no valid u2f factor", + session.factors.webAuthN?.verifiedAt, + ); + } + } else { + // only check settings if no auth methods are available, as this would require a setup + const loginSettings = await getLoginSettings({ + serviceUrl, + organization: session.factors?.user?.organizationId, + }); + if (loginSettings?.forceMfa || loginSettings?.forceMfaLocalOnly) { + const otpEmail = session.factors.otpEmail?.verifiedAt; + const otpSms = session.factors.otpSms?.verifiedAt; + const totp = session.factors.totp?.verifiedAt; + const webAuthN = session.factors.webAuthN?.verifiedAt; + const idp = session.factors.intent?.verifiedAt; // TODO: forceMFA should not consider this as valid factor + + // must have one single check + mfaValid = !!(otpEmail || otpSms || totp || webAuthN || idp); + if (!mfaValid) { + console.warn("Session has no valid multifactor", session.factors); + } + } else { + mfaValid = true; + } + } + + const validPassword = session?.factors?.password?.verifiedAt; + const validPasskey = session?.factors?.webAuthN?.verifiedAt; + const validIDP = session?.factors?.intent?.verifiedAt; + + const stillValid = session.expirationDate + ? timestampDate(session.expirationDate).getTime() > new Date().getTime() + : true; + + if (!stillValid) { + console.warn( + "Session is expired", + session.expirationDate + ? timestampDate(session.expirationDate).toDateString() + : "no expiration date", + ); + } + + const validChecks = !!(validPassword || validPasskey || validIDP); + + return stillValid && validChecks && mfaValid; +} + +export async function findValidSession({ + serviceUrl, + sessions, + authRequest, + samlRequest, +}: { + serviceUrl: string; + sessions: Session[]; + authRequest?: AuthRequest; + samlRequest?: SAMLRequest; +}): Promise { + const sessionsWithHint = sessions.filter((s) => { + if (authRequest && authRequest.hintUserId) { + return s.factors?.user?.id === authRequest.hintUserId; + } + if (authRequest && authRequest.loginHint) { + return s.factors?.user?.loginName === authRequest.loginHint; + } + if (samlRequest) { + // TODO: do whatever + return true; + } + return true; + }); + + if (sessionsWithHint.length === 0) { + return undefined; + } + + // sort by change date descending + sessionsWithHint.sort((a, b) => { + const dateA = a.changeDate ? timestampDate(a.changeDate).getTime() : 0; + const dateB = b.changeDate ? timestampDate(b.changeDate).getTime() : 0; + return dateB - dateA; + }); + + // return the first valid session according to settings + for (const session of sessionsWithHint) { + if (await isSessionValid({ serviceUrl, session })) { + return session; + } + } + + return undefined; +} diff --git a/apps/login/src/lib/verify-helper.ts b/apps/login/src/lib/verify-helper.ts new file mode 100644 index 0000000000..dbd9b2796b --- /dev/null +++ b/apps/login/src/lib/verify-helper.ts @@ -0,0 +1,289 @@ +import { timestampDate } from "@zitadel/client"; +import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; +import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; +import { PasswordExpirySettings } from "@zitadel/proto/zitadel/settings/v2/password_settings_pb"; +import { HumanUser } from "@zitadel/proto/zitadel/user/v2/user_pb"; +import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; +import crypto from "crypto"; +import moment from "moment"; +import { cookies } from "next/headers"; +import { getFingerprintIdCookie } from "./fingerprint"; +import { getUserByID } from "./zitadel"; + +export function checkPasswordChangeRequired( + expirySettings: PasswordExpirySettings | undefined, + session: Session, + humanUser: HumanUser | undefined, + organization?: string, + requestId?: string, +) { + let isOutdated = false; + if (expirySettings?.maxAgeDays && humanUser?.passwordChanged) { + const maxAgeDays = Number(expirySettings.maxAgeDays); // Convert bigint to number + const passwordChangedDate = moment( + timestampDate(humanUser.passwordChanged), + ); + const outdatedPassword = passwordChangedDate.add(maxAgeDays, "days"); + isOutdated = moment().isAfter(outdatedPassword); + } + + if (humanUser?.passwordChangeRequired || isOutdated) { + const params = new URLSearchParams({ + loginName: session.factors?.user?.loginName as string, + }); + + if (organization || session.factors?.user?.organizationId) { + params.append( + "organization", + session.factors?.user?.organizationId as string, + ); + } + + if (requestId) { + params.append("requestId", requestId); + } + + return { redirect: "/password/change?" + params }; + } +} + +export function checkEmailVerified( + session: Session, + humanUser?: HumanUser, + organization?: string, + requestId?: string, +) { + if (!humanUser?.email?.isVerified) { + const paramsVerify = new URLSearchParams({ + loginName: session.factors?.user?.loginName as string, + userId: session.factors?.user?.id as string, // verify needs user id + send: "true", // we request a new email code once the page is loaded + }); + + if (organization || session.factors?.user?.organizationId) { + paramsVerify.append( + "organization", + organization ?? (session.factors?.user?.organizationId as string), + ); + } + + if (requestId) { + paramsVerify.append("requestId", requestId); + } + + return { redirect: "/verify?" + paramsVerify }; + } +} + +export function checkEmailVerification( + session: Session, + humanUser?: HumanUser, + organization?: string, + requestId?: string, +) { + if ( + !humanUser?.email?.isVerified && + process.env.EMAIL_VERIFICATION === "true" + ) { + const params = new URLSearchParams({ + loginName: session.factors?.user?.loginName as string, + send: "true", // set this to true as we dont expect old email codes to be valid anymore + }); + + if (requestId) { + params.append("requestId", requestId); + } + + if (organization || session.factors?.user?.organizationId) { + params.append( + "organization", + organization ?? (session.factors?.user?.organizationId as string), + ); + } + + return { redirect: `/verify?` + params }; + } +} + +export async function checkMFAFactors( + serviceUrl: string, + session: Session, + loginSettings: LoginSettings | undefined, + authMethods: AuthenticationMethodType[], + organization?: string, + requestId?: string, +) { + const availableMultiFactors = authMethods?.filter( + (m: AuthenticationMethodType) => + m !== AuthenticationMethodType.PASSWORD && + m !== AuthenticationMethodType.PASSKEY, + ); + + const hasAuthenticatedWithPasskey = + session.factors?.webAuthN?.verifiedAt && + session.factors?.webAuthN?.userVerified; + + // escape further checks if user has authenticated with passkey + if (hasAuthenticatedWithPasskey) { + return; + } + + // if user has not authenticated with passkey and has only one additional mfa factor, redirect to that + if (availableMultiFactors?.length == 1) { + const params = new URLSearchParams({ + loginName: session.factors?.user?.loginName as string, + }); + + if (requestId) { + params.append("requestId", requestId); + } + + if (organization || session.factors?.user?.organizationId) { + params.append( + "organization", + organization ?? (session.factors?.user?.organizationId as string), + ); + } + + const factor = availableMultiFactors[0]; + // if passwordless is other method, but user selected password as alternative, perform a login + if (factor === AuthenticationMethodType.TOTP) { + return { redirect: `/otp/time-based?` + params }; + } else if (factor === AuthenticationMethodType.OTP_SMS) { + return { redirect: `/otp/sms?` + params }; + } else if (factor === AuthenticationMethodType.OTP_EMAIL) { + return { redirect: `/otp/email?` + params }; + } else if (factor === AuthenticationMethodType.U2F) { + return { redirect: `/u2f?` + params }; + } + } else if (availableMultiFactors?.length > 1) { + const params = new URLSearchParams({ + loginName: session.factors?.user?.loginName as string, + }); + + if (requestId) { + params.append("requestId", requestId); + } + + if (organization || session.factors?.user?.organizationId) { + params.append( + "organization", + organization ?? (session.factors?.user?.organizationId as string), + ); + } + + return { redirect: `/mfa?` + params }; + } else if ( + (loginSettings?.forceMfa || loginSettings?.forceMfaLocalOnly) && + !availableMultiFactors.length + ) { + const params = new URLSearchParams({ + loginName: session.factors?.user?.loginName as string, + force: "true", // this defines if the mfa is forced in the settings + checkAfter: "true", // this defines if the check is directly made after the setup + }); + + if (requestId) { + params.append("requestId", requestId); + } + + if (organization || session.factors?.user?.organizationId) { + params.append( + "organization", + organization ?? (session.factors?.user?.organizationId as string), + ); + } + + // TODO: provide a way to setup passkeys on mfa page? + return { redirect: `/mfa/set?` + params }; + } else if ( + loginSettings?.mfaInitSkipLifetime && + (loginSettings.mfaInitSkipLifetime.nanos > 0 || + loginSettings.mfaInitSkipLifetime.seconds > 0) && + !availableMultiFactors.length && + session?.factors?.user?.id + ) { + const userResponse = await getUserByID({ + serviceUrl, + userId: session.factors?.user?.id, + }); + + const humanUser = + userResponse?.user?.type.case === "human" + ? userResponse?.user.type.value + : undefined; + + if (humanUser?.mfaInitSkipped) { + const mfaInitSkippedTimestamp = timestampDate(humanUser.mfaInitSkipped); + + const mfaInitSkipLifetimeMillis = + Number(loginSettings.mfaInitSkipLifetime.seconds) * 1000 + + loginSettings.mfaInitSkipLifetime.nanos / 1000000; + const currentTime = Date.now(); + const mfaInitSkippedTime = mfaInitSkippedTimestamp.getTime(); + const timeDifference = currentTime - mfaInitSkippedTime; + + if (!(timeDifference > mfaInitSkipLifetimeMillis)) { + // if the time difference is smaller than the lifetime, skip the mfa setup + return; + } + } + + // the user has never skipped the mfa init but we have a setting so we redirect + + const params = new URLSearchParams({ + loginName: session.factors?.user?.loginName as string, + force: "false", // this defines if the mfa is not forced in the settings and can be skipped + checkAfter: "true", // this defines if the check is directly made after the setup + }); + + if (requestId) { + params.append("requestId", requestId); + } + + if (organization || session.factors?.user?.organizationId) { + params.append( + "organization", + organization ?? (session.factors?.user?.organizationId as string), + ); + } + + // TODO: provide a way to setup passkeys on mfa page? + return { redirect: `/mfa/set?` + params }; + } +} + +export async function checkUserVerification(userId: string): Promise { + // check if a verification was done earlier + const cookiesList = await cookies(); + + // only read cookie to prevent issues on page.tsx + const fingerPrintCookie = await getFingerprintIdCookie(); + + if (!fingerPrintCookie || !fingerPrintCookie.value) { + return false; + } + + const verificationCheck = crypto + .createHash("sha256") + .update(`${userId}:${fingerPrintCookie.value}`) + .digest("hex"); + + const cookieValue = await cookiesList.get("verificationCheck")?.value; + + if (!cookieValue) { + console.warn( + "User verification check cookie not found. User verification check failed.", + ); + return false; + } + + if (cookieValue !== verificationCheck) { + console.warn( + `User verification check failed. Expected ${verificationCheck} but got ${cookieValue}`, + ); + return false; + } + + return true; +} diff --git a/apps/login/src/lib/zitadel.ts b/apps/login/src/lib/zitadel.ts new file mode 100644 index 0000000000..5e4583f8af --- /dev/null +++ b/apps/login/src/lib/zitadel.ts @@ -0,0 +1,1551 @@ +import { Client, create, Duration } from "@zitadel/client"; +import { createServerTransport as libCreateServerTransport } from "@zitadel/client/node"; +import { makeReqCtx } from "@zitadel/client/v2"; +import { IdentityProviderService } from "@zitadel/proto/zitadel/idp/v2/idp_service_pb"; +import { + OrganizationSchema, + TextQueryMethod, +} from "@zitadel/proto/zitadel/object/v2/object_pb"; +import { + CreateCallbackRequest, + OIDCService, +} from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb"; +import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb"; +import { OrganizationService } from "@zitadel/proto/zitadel/org/v2/org_service_pb"; +import { + CreateResponseRequest, + SAMLService, +} from "@zitadel/proto/zitadel/saml/v2/saml_service_pb"; +import { RequestChallenges } from "@zitadel/proto/zitadel/session/v2/challenge_pb"; +import { + Checks, + SessionService, +} from "@zitadel/proto/zitadel/session/v2/session_service_pb"; +import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; +import { SettingsService } from "@zitadel/proto/zitadel/settings/v2/settings_service_pb"; +import { SendEmailVerificationCodeSchema } from "@zitadel/proto/zitadel/user/v2/email_pb"; +import type { + FormData, + RedirectURLsJson, +} from "@zitadel/proto/zitadel/user/v2/idp_pb"; +import { + NotificationType, + SendPasswordResetLinkSchema, +} from "@zitadel/proto/zitadel/user/v2/password_pb"; +import { + SearchQuery, + SearchQuerySchema, +} from "@zitadel/proto/zitadel/user/v2/query_pb"; +import { SendInviteCodeSchema } from "@zitadel/proto/zitadel/user/v2/user_pb"; +import { + AddHumanUserRequest, + AddHumanUserRequestSchema, + ResendEmailCodeRequest, + ResendEmailCodeRequestSchema, + SendEmailCodeRequestSchema, + SetPasswordRequest, + SetPasswordRequestSchema, + UpdateHumanUserRequest, + UserService, + VerifyPasskeyRegistrationRequest, + VerifyU2FRegistrationRequest, +} from "@zitadel/proto/zitadel/user/v2/user_service_pb"; +import { unstable_cacheLife as cacheLife } from "next/cache"; +import { getUserAgent } from "./fingerprint"; +import { setSAMLFormCookie } from "./saml"; +import { createServiceForHost } from "./service"; + +const useCache = process.env.DEBUG !== "true"; + +async function cacheWrapper(callback: Promise) { + "use cache"; + cacheLife("hours"); + + return callback; +} + +export async function getHostedLoginTranslation({ + serviceUrl, + organization, + locale, +}: { + serviceUrl: string; + organization?: string; + locale?: string; +}) { + const settingsService: Client = + await createServiceForHost(SettingsService, serviceUrl); + + const callback = settingsService + .getHostedLoginTranslation( + { + level: organization + ? { + case: "organizationId", + value: organization, + } + : { + case: "instance", + value: true, + }, + locale: locale, + }, + {}, + ) + .then((resp) => { + return resp.translations ? resp.translations : undefined; + }); + + return useCache ? cacheWrapper(callback) : callback; +} + +export async function getBrandingSettings({ + serviceUrl, + organization, +}: { + serviceUrl: string; + organization?: string; +}) { + const settingsService: Client = + await createServiceForHost(SettingsService, serviceUrl); + + const callback = settingsService + .getBrandingSettings({ ctx: makeReqCtx(organization) }, {}) + .then((resp) => (resp.settings ? resp.settings : undefined)); + + return useCache ? cacheWrapper(callback) : callback; +} + +export async function getLoginSettings({ + serviceUrl, + organization, +}: { + serviceUrl: string; + organization?: string; +}) { + const settingsService: Client = + await createServiceForHost(SettingsService, serviceUrl); + + const callback = settingsService + .getLoginSettings({ ctx: makeReqCtx(organization) }, {}) + .then((resp) => (resp.settings ? resp.settings : undefined)); + + return useCache ? cacheWrapper(callback) : callback; +} + +export async function getSecuritySettings({ + serviceUrl, +}: { + serviceUrl: string; +}) { + const settingsService: Client = + await createServiceForHost(SettingsService, serviceUrl); + + const callback = settingsService + .getSecuritySettings({}) + .then((resp) => (resp.settings ? resp.settings : undefined)); + + return useCache ? cacheWrapper(callback) : callback; +} + +export async function getLockoutSettings({ + serviceUrl, + orgId, +}: { + serviceUrl: string; + orgId?: string; +}) { + const settingsService: Client = + await createServiceForHost(SettingsService, serviceUrl); + + const callback = settingsService + .getLockoutSettings({ ctx: makeReqCtx(orgId) }, {}) + .then((resp) => (resp.settings ? resp.settings : undefined)); + + return useCache ? cacheWrapper(callback) : callback; +} + +export async function getPasswordExpirySettings({ + serviceUrl, + orgId, +}: { + serviceUrl: string; + orgId?: string; +}) { + const settingsService: Client = + await createServiceForHost(SettingsService, serviceUrl); + + const callback = settingsService + .getPasswordExpirySettings({ ctx: makeReqCtx(orgId) }, {}) + .then((resp) => (resp.settings ? resp.settings : undefined)); + + return useCache ? cacheWrapper(callback) : callback; +} + +export async function listIDPLinks({ + serviceUrl, + userId, +}: { + serviceUrl: string; + userId: string; +}) { + const userService: Client = await createServiceForHost( + UserService, + serviceUrl, + ); + + return userService.listIDPLinks({ userId }, {}); +} + +export async function addOTPEmail({ + serviceUrl, + userId, +}: { + serviceUrl: string; + userId: string; +}) { + const userService: Client = await createServiceForHost( + UserService, + serviceUrl, + ); + + return userService.addOTPEmail({ userId }, {}); +} + +export async function addOTPSMS({ + serviceUrl, + userId, +}: { + serviceUrl: string; + userId: string; +}) { + const userService: Client = await createServiceForHost( + UserService, + serviceUrl, + ); + + return userService.addOTPSMS({ userId }, {}); +} + +export async function registerTOTP({ + serviceUrl, + userId, +}: { + serviceUrl: string; + userId: string; +}) { + const userService: Client = await createServiceForHost( + UserService, + serviceUrl, + ); + + return userService.registerTOTP({ userId }, {}); +} + +export async function getGeneralSettings({ + serviceUrl, +}: { + serviceUrl: string; +}) { + const settingsService: Client = + await createServiceForHost(SettingsService, serviceUrl); + + const callback = settingsService + .getGeneralSettings({}, {}) + .then((resp) => resp.supportedLanguages); + + return useCache ? cacheWrapper(callback) : callback; +} + +export async function getLegalAndSupportSettings({ + serviceUrl, + organization, +}: { + serviceUrl: string; + organization?: string; +}) { + const settingsService: Client = + await createServiceForHost(SettingsService, serviceUrl); + + const callback = settingsService + .getLegalAndSupportSettings({ ctx: makeReqCtx(organization) }, {}) + .then((resp) => (resp.settings ? resp.settings : undefined)); + + return useCache ? cacheWrapper(callback) : callback; +} + +export async function getPasswordComplexitySettings({ + serviceUrl, + organization, +}: { + serviceUrl: string; + organization?: string; +}) { + const settingsService: Client = + await createServiceForHost(SettingsService, serviceUrl); + + const callback = settingsService + .getPasswordComplexitySettings({ ctx: makeReqCtx(organization) }) + .then((resp) => (resp.settings ? resp.settings : undefined)); + + return useCache ? cacheWrapper(callback) : callback; +} + +export async function createSessionFromChecks({ + serviceUrl, + checks, + lifetime, +}: { + serviceUrl: string; + checks: Checks; + lifetime: Duration; +}) { + const sessionService: Client = + await createServiceForHost(SessionService, serviceUrl); + + const userAgent = await getUserAgent(); + + return sessionService.createSession({ checks, lifetime, userAgent }, {}); +} + +export async function createSessionForUserIdAndIdpIntent({ + serviceUrl, + userId, + idpIntent, + lifetime, +}: { + serviceUrl: string; + userId: string; + idpIntent: { + idpIntentId?: string | undefined; + idpIntentToken?: string | undefined; + }; + lifetime: Duration; +}) { + const sessionService: Client = + await createServiceForHost(SessionService, serviceUrl); + + const userAgent = await getUserAgent(); + + return sessionService.createSession({ + checks: { + user: { + search: { + case: "userId", + value: userId, + }, + }, + idpIntent, + }, + lifetime, + userAgent, + }); +} + +export async function setSession({ + serviceUrl, + sessionId, + sessionToken, + challenges, + checks, + lifetime, +}: { + serviceUrl: string; + sessionId: string; + sessionToken: string; + challenges: RequestChallenges | undefined; + checks?: Checks; + lifetime: Duration; +}) { + const sessionService: Client = + await createServiceForHost(SessionService, serviceUrl); + + return sessionService.setSession( + { + sessionId, + sessionToken, + challenges, + checks: checks ? checks : {}, + metadata: {}, + lifetime, + }, + {}, + ); +} + +export async function getSession({ + serviceUrl, + sessionId, + sessionToken, +}: { + serviceUrl: string; + sessionId: string; + sessionToken: string; +}) { + const sessionService: Client = + await createServiceForHost(SessionService, serviceUrl); + + return sessionService.getSession({ sessionId, sessionToken }, {}); +} + +export async function deleteSession({ + serviceUrl, + sessionId, + sessionToken, +}: { + serviceUrl: string; + sessionId: string; + sessionToken: string; +}) { + const sessionService: Client = + await createServiceForHost(SessionService, serviceUrl); + + return sessionService.deleteSession({ sessionId, sessionToken }, {}); +} + +type ListSessionsCommand = { + serviceUrl: string; + ids: string[]; +}; + +export async function listSessions({ serviceUrl, ids }: ListSessionsCommand) { + const sessionService: Client = + await createServiceForHost(SessionService, serviceUrl); + + return sessionService.listSessions( + { + queries: [ + { + query: { + case: "idsQuery", + value: { ids }, + }, + }, + ], + }, + {}, + ); +} + +export type AddHumanUserData = { + serviceUrl: string; + firstName: string; + lastName: string; + email: string; + password?: string; + organization: string; +}; + +export async function addHumanUser({ + serviceUrl, + email, + firstName, + lastName, + password, + organization, +}: AddHumanUserData) { + const userService: Client = await createServiceForHost( + UserService, + serviceUrl, + ); + + let addHumanUserRequest: AddHumanUserRequest = create( + AddHumanUserRequestSchema, + { + email: { + email, + verification: { + case: "isVerified", + value: false, + }, + }, + username: email, + profile: { givenName: firstName, familyName: lastName }, + passwordType: password + ? { case: "password", value: { password } } + : undefined, + }, + ); + + if (organization) { + const organizationSchema = create(OrganizationSchema, { + org: { case: "orgId", value: organization }, + }); + + addHumanUserRequest = { + ...addHumanUserRequest, + organization: organizationSchema, + }; + } + + return userService.addHumanUser(addHumanUserRequest); +} + +export async function addHuman({ + serviceUrl, + request, +}: { + serviceUrl: string; + request: AddHumanUserRequest; +}) { + const userService: Client = await createServiceForHost( + UserService, + serviceUrl, + ); + + return userService.addHumanUser(request); +} + +export async function updateHuman({ + serviceUrl, + request, +}: { + serviceUrl: string; + request: UpdateHumanUserRequest; +}) { + const userService: Client = await createServiceForHost( + UserService, + serviceUrl, + ); + + return userService.updateHumanUser(request); +} + +export async function verifyTOTPRegistration({ + serviceUrl, + code, + userId, +}: { + serviceUrl: string; + code: string; + userId: string; +}) { + const userService: Client = await createServiceForHost( + UserService, + serviceUrl, + ); + + return userService.verifyTOTPRegistration({ code, userId }, {}); +} + +export async function getUserByID({ + serviceUrl, + userId, +}: { + serviceUrl: string; + userId: string; +}) { + const userService: Client = await createServiceForHost( + UserService, + serviceUrl, + ); + + return userService.getUserByID({ userId }, {}); +} + +export async function humanMFAInitSkipped({ + serviceUrl, + userId, +}: { + serviceUrl: string; + userId: string; +}) { + const userService: Client = await createServiceForHost( + UserService, + serviceUrl, + ); + + return userService.humanMFAInitSkipped({ userId }, {}); +} + +export async function verifyInviteCode({ + serviceUrl, + userId, + verificationCode, +}: { + serviceUrl: string; + userId: string; + verificationCode: string; +}) { + const userService: Client = await createServiceForHost( + UserService, + serviceUrl, + ); + + return userService.verifyInviteCode({ userId, verificationCode }, {}); +} + +export async function sendEmailCode({ + serviceUrl, + userId, + urlTemplate, +}: { + serviceUrl: string; + userId: string; + urlTemplate: string; +}) { + let medium = create(SendEmailCodeRequestSchema, { userId }); + + medium = create(SendEmailCodeRequestSchema, { + ...medium, + verification: { + case: "sendCode", + value: create(SendEmailVerificationCodeSchema, { + urlTemplate, + }), + }, + }); + + const userService: Client = await createServiceForHost( + UserService, + serviceUrl, + ); + + return userService.sendEmailCode(medium, {}); +} + +export async function createInviteCode({ + serviceUrl, + urlTemplate, + userId, +}: { + serviceUrl: string; + urlTemplate: string; + userId: string; +}) { + let medium = create(SendInviteCodeSchema, { + applicationName: "Typescript Login", + }); + + medium = { + ...medium, + urlTemplate, + }; + + const userService: Client = await createServiceForHost( + UserService, + serviceUrl, + ); + + return userService.createInviteCode( + { + userId, + verification: { + case: "sendCode", + value: medium, + }, + }, + {}, + ); +} + +export type ListUsersCommand = { + serviceUrl: string; + loginName?: string; + userName?: string; + email?: string; + phone?: string; + organizationId?: string; +}; + +export async function listUsers({ + serviceUrl, + loginName, + userName, + phone, + email, + organizationId, +}: ListUsersCommand) { + const queries: SearchQuery[] = []; + + // either use loginName or userName, email, phone + if (loginName) { + queries.push( + create(SearchQuerySchema, { + query: { + case: "loginNameQuery", + value: { + loginName, + method: TextQueryMethod.EQUALS, + }, + }, + }), + ); + } else if (userName || email || phone) { + const orQueries: SearchQuery[] = []; + + if (userName) { + const userNameQuery = create(SearchQuerySchema, { + query: { + case: "userNameQuery", + value: { + userName, + method: TextQueryMethod.EQUALS, + }, + }, + }); + orQueries.push(userNameQuery); + } + + if (email) { + const emailQuery = create(SearchQuerySchema, { + query: { + case: "emailQuery", + value: { + emailAddress: email, + method: TextQueryMethod.EQUALS, + }, + }, + }); + orQueries.push(emailQuery); + } + + if (phone) { + const phoneQuery = create(SearchQuerySchema, { + query: { + case: "phoneQuery", + value: { + number: phone, + method: TextQueryMethod.EQUALS, + }, + }, + }); + orQueries.push(phoneQuery); + } + + queries.push( + create(SearchQuerySchema, { + query: { + case: "orQuery", + value: { + queries: orQueries, + }, + }, + }), + ); + } + + if (organizationId) { + queries.push( + create(SearchQuerySchema, { + query: { + case: "organizationIdQuery", + value: { + organizationId, + }, + }, + }), + ); + } + + const userService: Client = await createServiceForHost( + UserService, + serviceUrl, + ); + + return userService.listUsers({ queries }); +} + +export type SearchUsersCommand = { + serviceUrl: string; + searchValue: string; + loginSettings: LoginSettings; + organizationId?: string; + suffix?: string; +}; + +const PhoneQuery = (searchValue: string) => + create(SearchQuerySchema, { + query: { + case: "phoneQuery", + value: { + number: searchValue, + method: TextQueryMethod.EQUALS, + }, + }, + }); + +const LoginNameQuery = (searchValue: string) => + create(SearchQuerySchema, { + query: { + case: "loginNameQuery", + value: { + loginName: searchValue, + method: TextQueryMethod.EQUALS, + }, + }, + }); + +const EmailQuery = (searchValue: string) => + create(SearchQuerySchema, { + query: { + case: "emailQuery", + value: { + emailAddress: searchValue, + method: TextQueryMethod.EQUALS, + }, + }, + }); + +/** + * this is a dedicated search function to search for users from the loginname page + * it searches users based on the loginName or userName and org suffix combination, and falls back to email and phone if no users are found + * */ +export async function searchUsers({ + serviceUrl, + searchValue, + loginSettings, + organizationId, + suffix, +}: SearchUsersCommand) { + const queries: SearchQuery[] = []; + + // if a suffix is provided, we search for the userName concatenated with the suffix + if (suffix) { + const searchValueWithSuffix = `${searchValue}@${suffix}`; + const loginNameQuery = LoginNameQuery(searchValueWithSuffix); + queries.push(loginNameQuery); + } else { + const loginNameQuery = LoginNameQuery(searchValue); + queries.push(loginNameQuery); + } + + if (organizationId) { + queries.push( + create(SearchQuerySchema, { + query: { + case: "organizationIdQuery", + value: { + organizationId, + }, + }, + }), + ); + } + + const userService: Client = await createServiceForHost( + UserService, + serviceUrl, + ); + + const loginNameResult = await userService.listUsers({ queries }); + + if (!loginNameResult || !loginNameResult.details) { + return { error: "An error occurred." }; + } + + if (loginNameResult.result.length > 1) { + return { error: "Multiple users found" }; + } + + if (loginNameResult.result.length == 1) { + return loginNameResult; + } + + const emailAndPhoneQueries: SearchQuery[] = []; + if ( + loginSettings.disableLoginWithEmail && + loginSettings.disableLoginWithPhone + ) { + return { error: "User not found in the system" }; + } else if (loginSettings.disableLoginWithEmail && searchValue.length <= 20) { + const phoneQuery = PhoneQuery(searchValue); + emailAndPhoneQueries.push(phoneQuery); + } else if (loginSettings.disableLoginWithPhone) { + const emailQuery = EmailQuery(searchValue); + emailAndPhoneQueries.push(emailQuery); + } else { + const orQuery: SearchQuery[] = []; + + const emailQuery = EmailQuery(searchValue); + orQuery.push(emailQuery); + + let phoneQuery; + if (searchValue.length <= 20) { + phoneQuery = PhoneQuery(searchValue); + orQuery.push(phoneQuery); + } + + emailAndPhoneQueries.push( + create(SearchQuerySchema, { + query: { + case: "orQuery", + value: { + queries: orQuery, + }, + }, + }), + ); + } + + if (organizationId) { + queries.push( + create(SearchQuerySchema, { + query: { + case: "organizationIdQuery", + value: { + organizationId, + }, + }, + }), + ); + } + + const emailOrPhoneResult = await userService.listUsers({ + queries: emailAndPhoneQueries, + }); + + if (!emailOrPhoneResult || !emailOrPhoneResult.details) { + return { error: "An error occurred." }; + } + + if (emailOrPhoneResult.result.length > 1) { + return { error: "Multiple users found." }; + } + + if (emailOrPhoneResult.result.length == 1) { + return emailOrPhoneResult; + } + + return { error: "User not found in the system" }; +} + +export async function getDefaultOrg({ + serviceUrl, +}: { + serviceUrl: string; +}): Promise { + const orgService: Client = + await createServiceForHost(OrganizationService, serviceUrl); + + return orgService + .listOrganizations( + { + queries: [ + { + query: { + case: "defaultQuery", + value: {}, + }, + }, + ], + }, + {}, + ) + .then((resp) => (resp?.result && resp.result[0] ? resp.result[0] : null)); +} + +export async function getOrgsByDomain({ + serviceUrl, + domain, +}: { + serviceUrl: string; + domain: string; +}) { + const orgService: Client = + await createServiceForHost(OrganizationService, serviceUrl); + + return orgService.listOrganizations( + { + queries: [ + { + query: { + case: "domainQuery", + value: { domain, method: TextQueryMethod.EQUALS }, + }, + }, + ], + }, + {}, + ); +} + +export async function startIdentityProviderFlow({ + serviceUrl, + idpId, + urls, +}: { + serviceUrl: string; + idpId: string; + urls: RedirectURLsJson; +}): Promise { + const userService: Client = await createServiceForHost( + UserService, + serviceUrl, + ); + + return userService + .startIdentityProviderIntent({ + idpId, + content: { + case: "urls", + value: urls, + }, + }) + .then(async (resp) => { + if (resp.nextStep.case === "authUrl" && resp.nextStep.value) { + return resp.nextStep.value; + } else if (resp.nextStep.case === "formData" && resp.nextStep.value) { + const formData: FormData = resp.nextStep.value; + const redirectUrl = "/saml-post"; + + try { + // Log the attempt with structure inspection + console.log("Attempting to stringify formData.fields:", { + fields: formData.fields, + fieldsType: typeof formData.fields, + fieldsKeys: Object.keys(formData.fields || {}), + fieldsEntries: Object.entries(formData.fields || {}), + }); + + const stringifiedFields = JSON.stringify(formData.fields); + console.log( + "Successfully stringified formData.fields, length:", + stringifiedFields.length, + ); + + // Check cookie size limits (typical limit is 4KB) + if (stringifiedFields.length > 4000) { + console.warn( + `SAML form cookie value is large (${stringifiedFields.length} characters), may exceed browser limits`, + ); + } + + const dataId = await setSAMLFormCookie(stringifiedFields); + const params = new URLSearchParams({ url: formData.url, id: dataId }); + + return `${redirectUrl}?${params.toString()}`; + } catch (stringifyError) { + console.error("JSON serialization failed:", stringifyError); + throw new Error( + `Failed to serialize SAML form data: ${stringifyError instanceof Error ? stringifyError.message : String(stringifyError)}`, + ); + } + } else { + return null; + } + }); +} + +export async function startLDAPIdentityProviderFlow({ + serviceUrl, + idpId, + username, + password, +}: { + serviceUrl: string; + idpId: string; + username: string; + password: string; +}) { + const userService: Client = await createServiceForHost( + UserService, + serviceUrl, + ); + + return userService.startIdentityProviderIntent({ + idpId, + content: { + case: "ldap", + value: { + username, + password, + }, + }, + }); +} + +export async function getAuthRequest({ + serviceUrl, + authRequestId, +}: { + serviceUrl: string; + authRequestId: string; +}) { + const oidcService = await createServiceForHost(OIDCService, serviceUrl); + + return oidcService.getAuthRequest({ + authRequestId, + }); +} + +export async function getDeviceAuthorizationRequest({ + serviceUrl, + userCode, +}: { + serviceUrl: string; + userCode: string; +}) { + const oidcService = await createServiceForHost(OIDCService, serviceUrl); + + return oidcService.getDeviceAuthorizationRequest({ + userCode, + }); +} + +export async function authorizeOrDenyDeviceAuthorization({ + serviceUrl, + deviceAuthorizationId, + session, +}: { + serviceUrl: string; + deviceAuthorizationId: string; + session?: { sessionId: string; sessionToken: string }; +}) { + const oidcService = await createServiceForHost(OIDCService, serviceUrl); + + return oidcService.authorizeOrDenyDeviceAuthorization({ + deviceAuthorizationId, + decision: session + ? { + case: "session", + value: session, + } + : { + case: "deny", + value: {}, + }, + }); +} + +export async function createCallback({ + serviceUrl, + req, +}: { + serviceUrl: string; + req: CreateCallbackRequest; +}) { + const oidcService = await createServiceForHost(OIDCService, serviceUrl); + + return oidcService.createCallback(req); +} + +export async function getSAMLRequest({ + serviceUrl, + samlRequestId, +}: { + serviceUrl: string; + samlRequestId: string; +}) { + const samlService = await createServiceForHost(SAMLService, serviceUrl); + + return samlService.getSAMLRequest({ + samlRequestId, + }); +} + +export async function createResponse({ + serviceUrl, + req, +}: { + serviceUrl: string; + req: CreateResponseRequest; +}) { + const samlService = await createServiceForHost(SAMLService, serviceUrl); + + return samlService.createResponse(req); +} + +export async function verifyEmail({ + serviceUrl, + userId, + verificationCode, +}: { + serviceUrl: string; + userId: string; + verificationCode: string; +}) { + const userService: Client = await createServiceForHost( + UserService, + serviceUrl, + ); + + return userService.verifyEmail( + { + userId, + verificationCode, + }, + {}, + ); +} + +export async function resendEmailCode({ + serviceUrl, + userId, + urlTemplate, +}: { + serviceUrl: string; + userId: string; + urlTemplate: string; +}) { + let request: ResendEmailCodeRequest = create(ResendEmailCodeRequestSchema, { + userId, + }); + + const medium = create(SendEmailVerificationCodeSchema, { + urlTemplate, + }); + + request = { ...request, verification: { case: "sendCode", value: medium } }; + + const userService: Client = await createServiceForHost( + UserService, + serviceUrl, + ); + + return userService.resendEmailCode(request, {}); +} + +export async function retrieveIDPIntent({ + serviceUrl, + id, + token, +}: { + serviceUrl: string; + id: string; + token: string; +}) { + const userService: Client = await createServiceForHost( + UserService, + serviceUrl, + ); + + return userService.retrieveIdentityProviderIntent( + { idpIntentId: id, idpIntentToken: token }, + {}, + ); +} + +export async function getIDPByID({ + serviceUrl, + id, +}: { + serviceUrl: string; + id: string; +}) { + const idpService: Client = + await createServiceForHost(IdentityProviderService, serviceUrl); + + return idpService.getIDPByID({ id }, {}).then((resp) => resp.idp); +} + +export async function addIDPLink({ + serviceUrl, + idp, + userId, +}: { + serviceUrl: string; + idp: { id: string; userId: string; userName: string }; + userId: string; +}) { + const userService: Client = await createServiceForHost( + UserService, + serviceUrl, + ); + + return userService.addIDPLink( + { + idpLink: { + userId: idp.userId, + idpId: idp.id, + userName: idp.userName, + }, + userId, + }, + {}, + ); +} + +export async function passwordReset({ + serviceUrl, + userId, + urlTemplate, +}: { + serviceUrl: string; + userId: string; + urlTemplate?: string; +}) { + let medium = create(SendPasswordResetLinkSchema, { + notificationType: NotificationType.Email, + }); + + medium = { + ...medium, + urlTemplate, + }; + + const userService: Client = await createServiceForHost( + UserService, + serviceUrl, + ); + + return userService.passwordReset( + { + userId, + medium: { + case: "sendLink", + value: medium, + }, + }, + {}, + ); +} + +export async function setUserPassword({ + serviceUrl, + userId, + password, + code, +}: { + serviceUrl: string; + userId: string; + password: string; + code?: string; +}) { + let payload = create(SetPasswordRequestSchema, { + userId, + newPassword: { + password, + }, + }); + + if (code) { + payload = { + ...payload, + verification: { + case: "verificationCode", + value: code, + }, + }; + } + + const userService: Client = await createServiceForHost( + UserService, + serviceUrl, + ); + + return userService.setPassword(payload, {}).catch((error) => { + // throw error if failed precondition (ex. User is not yet initialized) + if (error.code === 9 && error.message) { + return { error: error.message }; + } else { + throw error; + } + }); +} + +export async function setPassword({ + serviceUrl, + payload, +}: { + serviceUrl: string; + payload: SetPasswordRequest; +}) { + const userService: Client = await createServiceForHost( + UserService, + serviceUrl, + ); + + return userService.setPassword(payload, {}); +} + +/** + * + * @param host + * @param userId the id of the user where the email should be set + * @returns the newly set email + */ +export async function createPasskeyRegistrationLink({ + serviceUrl, + userId, +}: { + serviceUrl: string; + userId: string; +}) { + const userService: Client = await createServiceForHost( + UserService, + serviceUrl, + ); + + return userService.createPasskeyRegistrationLink({ + userId, + medium: { + case: "returnCode", + value: {}, + }, + }); +} + +/** + * + * @param host + * @param userId the id of the user where the email should be set + * @param domain the domain on which the factor is registered + * @returns the newly set email + */ +export async function registerU2F({ + serviceUrl, + userId, + domain, +}: { + serviceUrl: string; + userId: string; + domain: string; +}) { + const userService: Client = await createServiceForHost( + UserService, + serviceUrl, + ); + + return userService.registerU2F({ + userId, + domain, + }); +} + +/** + * + * @param host + * @param request the request object for verifying U2F registration + * @returns the result of the verification + */ +export async function verifyU2FRegistration({ + serviceUrl, + request, +}: { + serviceUrl: string; + request: VerifyU2FRegistrationRequest; +}) { + const userService: Client = await createServiceForHost( + UserService, + serviceUrl, + ); + + return userService.verifyU2FRegistration(request, {}); +} + +/** + * + * @param host + * @param orgId the organization ID + * @param linking_allowed whether linking is allowed + * @returns the active identity providers + */ +export async function getActiveIdentityProviders({ + serviceUrl, + orgId, + linking_allowed, +}: { + serviceUrl: string; + orgId?: string; + linking_allowed?: boolean; +}) { + const props: any = { ctx: makeReqCtx(orgId) }; + if (linking_allowed) { + props.linkingAllowed = linking_allowed; + } + const settingsService: Client = + await createServiceForHost(SettingsService, serviceUrl); + + return settingsService.getActiveIdentityProviders(props, {}); +} + +/** + * + * @param host + * @param request the request object for verifying passkey registration + * @returns the result of the verification + */ +export async function verifyPasskeyRegistration({ + serviceUrl, + request, +}: { + serviceUrl: string; + request: VerifyPasskeyRegistrationRequest; +}) { + const userService: Client = await createServiceForHost( + UserService, + serviceUrl, + ); + + return userService.verifyPasskeyRegistration(request, {}); +} + +/** + * + * @param host + * @param userId the id of the user where the email should be set + * @param code the code for registering the passkey + * @param domain the domain on which the factor is registered + * @returns the newly set email + */ +export async function registerPasskey({ + serviceUrl, + userId, + code, + domain, +}: { + serviceUrl: string; + userId: string; + code: { id: string; code: string }; + domain: string; +}) { + const userService: Client = await createServiceForHost( + UserService, + serviceUrl, + ); + + return userService.registerPasskey({ + userId, + code, + domain, + }); +} + +/** + * + * @param host + * @param userId the id of the user where the email should be set + * @returns the list of authentication method types + */ +export async function listAuthenticationMethodTypes({ + serviceUrl, + userId, +}: { + serviceUrl: string; + userId: string; +}) { + const userService: Client = await createServiceForHost( + UserService, + serviceUrl, + ); + + return userService.listAuthenticationMethodTypes({ + userId, + }); +} + +export function createServerTransport(token: string, baseUrl: string) { + return libCreateServerTransport(token, { + baseUrl, + interceptors: !process.env.CUSTOM_REQUEST_HEADERS + ? undefined + : [ + (next) => { + return (req) => { + process.env + .CUSTOM_REQUEST_HEADERS!.split(",") + .forEach((header) => { + const kv = header.split(":"); + if (kv.length === 2) { + req.header.set(kv[0].trim(), kv[1].trim()); + } else { + console.warn(`Skipping malformed header: ${header}`); + } + }); + return next(req); + }; + }, + ], + }); +} diff --git a/apps/login/src/middleware.ts b/apps/login/src/middleware.ts new file mode 100644 index 0000000000..8eca00510e --- /dev/null +++ b/apps/login/src/middleware.ts @@ -0,0 +1,109 @@ +import { SecuritySettings } from "@zitadel/proto/zitadel/settings/v2/security_settings_pb"; + +import { headers } from "next/headers"; +import { NextRequest, NextResponse } from "next/server"; +import { DEFAULT_CSP } from "../constants/csp"; +import { getServiceUrlFromHeaders } from "./lib/service-url"; +export const config = { + matcher: [ + "/.well-known/:path*", + "/oauth/:path*", + "/oidc/:path*", + "/idps/callback/:path*", + "/saml/:path*", + "/:path*", + ], +}; + +async function loadSecuritySettings( + request: NextRequest, +): Promise { + const securityResponse = await fetch(`${request.nextUrl.origin}/security`); + + if (!securityResponse.ok) { + console.error( + "Failed to fetch security settings:", + securityResponse.statusText, + ); + return null; + } + + const response = await securityResponse.json(); + + if (!response || !response.settings) { + console.error("No security settings found in the response."); + return null; + } + + return response.settings; +} + +export async function middleware(request: NextRequest) { + // Add the original URL as a header to all requests + const requestHeaders = new Headers(request.headers); + + // Extract "organization" search param from the URL and set it as a header if available + const organization = request.nextUrl.searchParams.get("organization"); + if (organization) { + requestHeaders.set("x-zitadel-i18n-organization", organization); + } + + // Only run the rest of the logic for the original matcher paths + const proxyPaths = [ + "/.well-known/", + "/oauth/", + "/oidc/", + "/idps/callback/", + "/saml/", + ]; + + const isMatched = proxyPaths.some((prefix) => + request.nextUrl.pathname.startsWith(prefix), + ); + + // escape proxy if the environment is setup for multitenancy + if ( + !isMatched || + !process.env.ZITADEL_API_URL || + !process.env.ZITADEL_SERVICE_USER_TOKEN + ) { + // For all other routes, just add the header and continue + return NextResponse.next({ + request: { headers: requestHeaders }, + }); + } + + const _headers = await headers(); + const { serviceUrl } = getServiceUrlFromHeaders(_headers); + + const instanceHost = `${serviceUrl}` + .replace("https://", "") + .replace("http://", ""); + + // Add additional headers as before + requestHeaders.set("x-zitadel-public-host", `${request.nextUrl.host}`); + requestHeaders.set("x-zitadel-instance-host", instanceHost); + + const responseHeaders = new Headers(); + responseHeaders.set("Access-Control-Allow-Origin", "*"); + responseHeaders.set("Access-Control-Allow-Headers", "*"); + + const securitySettings = await loadSecuritySettings(request); + + if (securitySettings?.embeddedIframe?.enabled) { + responseHeaders.set( + "Content-Security-Policy", + `${DEFAULT_CSP} frame-ancestors ${securitySettings.embeddedIframe.allowedOrigins.join(" ")};`, + ); + responseHeaders.delete("X-Frame-Options"); + } + + request.nextUrl.href = `${serviceUrl}${request.nextUrl.pathname}${request.nextUrl.search}`; + + return NextResponse.rewrite(request.nextUrl, { + request: { + headers: requestHeaders, + }, + headers: responseHeaders, + }); +} diff --git a/apps/login/src/styles/globals.scss b/apps/login/src/styles/globals.scss new file mode 100755 index 0000000000..f1242eb573 --- /dev/null +++ b/apps/login/src/styles/globals.scss @@ -0,0 +1,65 @@ +// include styles from the ui package +@use "./vars.scss"; + +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + h1, + .ztdl-h1 { + @apply text-center text-2xl; + } + + .ztdl-p { + @apply text-center text-sm text-text-light-secondary-500 dark:text-text-dark-secondary-500; + } +} + +html { + --background-color: #ffffff; + --dark-background-color: #000000; +} + +.form-checkbox:checked { + background-image: url("/checkbox.svg"); +} + +.skeleton { + --accents-2: var(--theme-light-background-400); + --accents-1: var(--theme-light-background-500); + + background-image: linear-gradient( + 270deg, + var(--accents-1), + var(--accents-2), + var(--accents-2), + var(--accents-1) + ); + background-size: 400% 100%; + animation: skeleton_loading 8s ease-in-out infinite; +} + +.dark .skeleton { + --accents-2: var(--theme-dark-background-400); + --accents-1: var(--theme-dark-background-500); + + background-image: linear-gradient( + 270deg, + var(--accents-1), + var(--accents-2), + var(--accents-2), + var(--accents-1) + ); + background-size: 400% 100%; + animation: skeleton_loading 8s ease-in-out infinite; +} + +@keyframes skeleton_loading { + 0% { + background-position: 200% 0; + } + 100% { + background-position: -200% 0; + } +} diff --git a/apps/login/src/styles/vars.scss b/apps/login/src/styles/vars.scss new file mode 100644 index 0000000000..71c6a28782 --- /dev/null +++ b/apps/login/src/styles/vars.scss @@ -0,0 +1,174 @@ +:root { + --theme-dark-primary-50: #f1f7fd; + --theme-dark-primary-contrast-50: hsla(0, 0%, 0%, 0.87); + --theme-dark-primary-100: #afd1f2; + --theme-dark-primary-contrast-100: hsla(0, 0%, 0%, 0.87); + --theme-dark-primary-200: #7fb5ea; + --theme-dark-primary-contrast-200: hsla(0, 0%, 0%, 0.87); + --theme-dark-primary-300: #4192e0; + --theme-dark-primary-contrast-300: hsla(0, 0%, 0%, 0.87); + --theme-dark-primary-400: #2782dc; + --theme-dark-primary-contrast-400: hsla(0, 0%, 0%, 0.87); + --theme-dark-primary-500: #2073c4; + --theme-dark-primary-contrast-500: #ffffff; + --theme-dark-primary-600: #1c64aa; + --theme-dark-primary-contrast-600: #ffffff; + --theme-dark-primary-700: #17548f; + --theme-dark-primary-contrast-700: #ffffff; + --theme-dark-primary-800: #134575; + --theme-dark-primary-contrast-800: #ffffff; + --theme-dark-primary-900: #0f355b; + --theme-dark-primary-contrast-900: #ffffff; + --theme-dark-primary-A100: #e4f2ff; + --theme-dark-primary-contrast-A100: hsla(0, 0%, 0%, 0.87); + --theme-dark-primary-A200: #7ebfff; + --theme-dark-primary-contrast-A200: hsla(0, 0%, 0%, 0.87); + --theme-dark-primary-A400: #278df0; + --theme-dark-primary-contrast-A400: hsla(0, 0%, 0%, 0.87); + --theme-dark-primary-A700: #1d80e0; + --theme-dark-primary-contrast-A700: hsla(0, 0%, 0%, 0.87); + --theme-light-primary-50: #ffffff; + --theme-light-primary-contrast-50: hsla(0, 0%, 0%, 0.87); + --theme-light-primary-100: #ebedfa; + --theme-light-primary-contrast-100: hsla(0, 0%, 0%, 0.87); + --theme-light-primary-200: #bec6ef; + --theme-light-primary-contrast-200: hsla(0, 0%, 0%, 0.87); + --theme-light-primary-300: #8594e0; + --theme-light-primary-contrast-300: hsla(0, 0%, 0%, 0.87); + --theme-light-primary-400: #6c7eda; + --theme-light-primary-contrast-400: hsla(0, 0%, 0%, 0.87); + --theme-light-primary-500: #5469d4; + --theme-light-primary-contrast-500: #ffffff; + --theme-light-primary-600: #3c54ce; + --theme-light-primary-contrast-600: #ffffff; + --theme-light-primary-700: #2f46bc; + --theme-light-primary-contrast-700: #ffffff; + --theme-light-primary-800: #293da3; + --theme-light-primary-contrast-800: #ffffff; + --theme-light-primary-900: #23348b; + --theme-light-primary-contrast-900: #ffffff; + --theme-light-primary-A100: #ffffff; + --theme-light-primary-contrast-A100: hsla(0, 0%, 0%, 0.87); + --theme-light-primary-A200: #c5cefc; + --theme-light-primary-contrast-A200: hsla(0, 0%, 0%, 0.87); + --theme-light-primary-A400: #7085ea; + --theme-light-primary-contrast-A400: hsla(0, 0%, 0%, 0.87); + --theme-light-primary-A700: #6478de; + --theme-light-primary-contrast-A700: hsla(0, 0%, 0%, 0.87); + --theme-dark-warn-50: #ffffff; + --theme-dark-warn-contrast-50: hsla(0, 0%, 0%, 0.87); + --theme-dark-warn-100: #fff8f9; + --theme-dark-warn-contrast-100: hsla(0, 0%, 0%, 0.87); + --theme-dark-warn-200: #ffc0ca; + --theme-dark-warn-contrast-200: hsla(0, 0%, 0%, 0.87); + --theme-dark-warn-300: #ff788e; + --theme-dark-warn-contrast-300: hsla(0, 0%, 0%, 0.87); + --theme-dark-warn-400: #ff5a75; + --theme-dark-warn-contrast-400: hsla(0, 0%, 0%, 0.87); + --theme-dark-warn-500: #ff3b5b; + --theme-dark-warn-contrast-500: hsla(0, 0%, 0%, 0.87); + --theme-dark-warn-600: #ff1c41; + --theme-dark-warn-contrast-600: hsla(0, 0%, 0%, 0.87); + --theme-dark-warn-700: #fd0029; + --theme-dark-warn-contrast-700: hsla(0, 0%, 0%, 0.87); + --theme-dark-warn-800: #de0024; + --theme-dark-warn-contrast-800: #ffffff; + --theme-dark-warn-900: #c0001f; + --theme-dark-warn-contrast-900: #ffffff; + --theme-dark-warn-A100: #ffffff; + --theme-dark-warn-contrast-A100: hsla(0, 0%, 0%, 0.87); + --theme-dark-warn-A200: #ffd4db; + --theme-dark-warn-contrast-A200: hsla(0, 0%, 0%, 0.87); + --theme-dark-warn-A400: #ff6e86; + --theme-dark-warn-contrast-A400: hsla(0, 0%, 0%, 0.87); + --theme-dark-warn-A700: #ff5470; + --theme-dark-warn-contrast-A700: hsla(0, 0%, 0%, 0.87); + --theme-light-warn-50: #ffffff; + --theme-light-warn-contrast-50: hsla(0, 0%, 0%, 0.87); + --theme-light-warn-100: #f4d3d9; + --theme-light-warn-contrast-100: hsla(0, 0%, 0%, 0.87); + --theme-light-warn-200: #e8a6b2; + --theme-light-warn-contrast-200: hsla(0, 0%, 0%, 0.87); + --theme-light-warn-300: #da6e80; + --theme-light-warn-contrast-300: hsla(0, 0%, 0%, 0.87); + --theme-light-warn-400: #d3556b; + --theme-light-warn-contrast-400: hsla(0, 0%, 0%, 0.87); + --theme-light-warn-500: #cd3d56; + --theme-light-warn-contrast-500: #ffffff; + --theme-light-warn-600: #bb3048; + --theme-light-warn-contrast-600: #ffffff; + --theme-light-warn-700: #a32a3f; + --theme-light-warn-contrast-700: #ffffff; + --theme-light-warn-800: #8a2436; + --theme-light-warn-contrast-800: #ffffff; + --theme-light-warn-900: #721d2c; + --theme-light-warn-contrast-900: #ffffff; + --theme-light-warn-A100: #ffffff; + --theme-light-warn-contrast-A100: hsla(0, 0%, 0%, 0.87); + --theme-light-warn-A200: #faa9b7; + --theme-light-warn-contrast-A200: hsla(0, 0%, 0%, 0.87); + --theme-light-warn-A400: #e65770; + --theme-light-warn-contrast-A400: hsla(0, 0%, 0%, 0.87); + --theme-light-warn-A700: #d84c64; + --theme-light-warn-contrast-A700: hsla(0, 0%, 0%, 0.87); + --theme-dark-background-50: #7c93c6; + --theme-dark-background-contrast-50: hsla(0, 0%, 0%, 0.87); + --theme-dark-background-100: #4a69aa; + --theme-dark-background-contrast-100: #ffffff; + --theme-dark-background-200: #395183; + --theme-dark-background-contrast-200: #ffffff; + --theme-dark-background-300: #243252; + --theme-dark-background-contrast-300: #ffffff; + --theme-dark-background-400: #1a253c; + --theme-dark-background-contrast-400: #ffffff; + --theme-dark-background-500: #111827; + --theme-dark-background-contrast-500: #ffffff; + --theme-dark-background-600: #080b12; + --theme-dark-background-contrast-600: #ffffff; + --theme-dark-background-700: #000000; + --theme-dark-background-contrast-700: #ffffff; + --theme-dark-background-800: #000000; + --theme-dark-background-contrast-800: #ffffff; + --theme-dark-background-900: #000000; + --theme-dark-background-contrast-900: #ffffff; + --theme-dark-background-A100: #5782e0; + --theme-dark-background-contrast-A100: hsla(0, 0%, 0%, 0.87); + --theme-dark-background-A200: #204eb1; + --theme-dark-background-contrast-A200: #ffffff; + --theme-dark-background-A400: #182b53; + --theme-dark-background-contrast-A400: #ffffff; + --theme-dark-background-A700: #17223b; + --theme-dark-background-contrast-A700: #ffffff; + --theme-light-background-50: #ffffff; + --theme-light-background-contrast-50: hsla(0, 0%, 0%, 0.87); + --theme-light-background-100: #ffffff; + --theme-light-background-contrast-100: hsla(0, 0%, 0%, 0.87); + --theme-light-background-200: #ffffff; + --theme-light-background-contrast-200: hsla(0, 0%, 0%, 0.87); + --theme-light-background-300: #ffffff; + --theme-light-background-contrast-300: hsla(0, 0%, 0%, 0.87); + --theme-light-background-400: #ffffff; + --theme-light-background-contrast-400: hsla(0, 0%, 0%, 0.87); + --theme-light-background-500: #fafafa; + --theme-light-background-contrast-500: hsla(0, 0%, 0%, 0.87); + --theme-light-background-600: #ebebeb; + --theme-light-background-contrast-600: hsla(0, 0%, 0%, 0.87); + --theme-light-background-700: #dbdbdb; + --theme-light-background-contrast-700: hsla(0, 0%, 0%, 0.87); + --theme-light-background-800: #cccccc; + --theme-light-background-contrast-800: hsla(0, 0%, 0%, 0.87); + --theme-light-background-900: #bdbdbd; + --theme-light-background-contrast-900: hsla(0, 0%, 0%, 0.87); + --theme-light-background-A100: #ffffff; + --theme-light-background-contrast-A100: hsla(0, 0%, 0%, 0.87); + --theme-light-background-A200: #ffffff; + --theme-light-background-contrast-A200: hsla(0, 0%, 0%, 0.87); + --theme-light-background-A400: #ffffff; + --theme-light-background-contrast-A400: hsla(0, 0%, 0%, 0.87); + --theme-light-background-A700: #ffffff; + --theme-light-background-contrast-A700: hsla(0, 0%, 0%, 0.87); + --theme-dark-text: #ffffff; + --theme-dark-secondary-text: #ffffffc7; + --theme-light-text: #000000; + --theme-light-secondary-text: #000000c7; +} diff --git a/apps/login/tailwind.config.mjs b/apps/login/tailwind.config.mjs new file mode 100644 index 0000000000..87595a43bd --- /dev/null +++ b/apps/login/tailwind.config.mjs @@ -0,0 +1,197 @@ +import colors from "tailwindcss/colors"; + +// Generate dynamic theme colors +let themeColors = { + background: { light: { contrast: {} }, dark: { contrast: {} } }, + primary: { light: { contrast: {} }, dark: { contrast: {} } }, + warn: { light: { contrast: {} }, dark: { contrast: {} } }, + text: { light: { contrast: {} }, dark: { contrast: {} } }, + link: { light: { contrast: {} }, dark: { contrast: {} } }, +}; + +const shades = [ + "50", + "100", + "200", + "300", + "400", + "500", + "600", + "700", + "800", + "900", +]; +const themes = ["light", "dark"]; +const types = ["background", "primary", "warn", "text", "link"]; + +types.forEach((type) => { + themes.forEach((theme) => { + shades.forEach((shade) => { + themeColors[type][theme][shade] = + `var(--theme-${theme}-${type}-${shade})`; + themeColors[type][theme][`contrast-${shade}`] = + `var(--theme-${theme}-${type}-contrast-${shade})`; + themeColors[type][theme][`secondary-${shade}`] = + `var(--theme-${theme}-${type}-secondary-${shade})`; + }); + }); +}); + +/** @type {import('tailwindcss').Config} */ +export default { + content: ["./src/**/*.{js,ts,jsx,tsx}"], + darkMode: "class", + future: { + hoverOnlyWhenSupported: true, + }, + theme: { + extend: { + // https://vercel.com/design/color + fontSize: { + "12px": "12px", + "14px": "14px", + }, + colors: { + gray: colors.zinc, + // Dynamic theme colors + ...themeColors, + // State colors + state: { + success: { + light: { + background: "#cbf4c9", + color: "#0e6245", + }, + dark: { + background: "#68cf8340", + color: "#cbf4c9", + }, + }, + error: { + light: { + background: "#ffc1c1", + color: "#620e0e", + }, + dark: { + background: "#af455359", + color: "#ffc1c1", + }, + }, + neutral: { + light: { + background: "#e4e7e4", + color: "#000000", + }, + dark: { + background: "#1a253c", + color: "#ffffff", + }, + }, + alert: { + light: { + background: "#fbbf24", + color: "#92400e", + }, + dark: { + background: "#92400e50", + color: "#fbbf24", + }, + }, + }, + divider: { + dark: "rgba(135,149,161,.2)", + light: "rgba(135,149,161,.2)", + }, + input: { + light: { + label: "#000000c7", + background: "#00000004", + border: "#1a191954", + hoverborder: "1a1b1b", + }, + dark: { + label: "#ffffffc7", + background: "#00000020", + border: "#f9f7f775", + hoverborder: "#e0e0e0", + }, + }, + button: { + light: { + border: "#0000001f", + }, + dark: { + border: "#ffffff1f", + }, + }, + }, + backgroundImage: ({ theme }) => ({ + "dark-vc-border-gradient": `radial-gradient(at left top, ${theme( + "colors.gray.800", + )}, 50px, ${theme("colors.gray.800")} 50%)`, + "vc-border-gradient": `radial-gradient(at left top, ${theme( + "colors.gray.200", + )}, 50px, ${theme("colors.gray.300")} 50%)`, + }), + animation: { + shake: "shake .8s cubic-bezier(.36,.07,.19,.97) both;", + }, + keyframes: ({ theme }) => ({ + rerender: { + "0%": { + ["border-color"]: theme("colors.pink.500"), + }, + "40%": { + ["border-color"]: theme("colors.pink.500"), + }, + }, + highlight: { + "0%": { + background: theme("colors.pink.500"), + color: theme("colors.white"), + }, + "40%": { + background: theme("colors.pink.500"), + color: theme("colors.white"), + }, + }, + shimmer: { + "100%": { + transform: "translateX(100%)", + }, + }, + translateXReset: { + "100%": { + transform: "translateX(0)", + }, + }, + fadeToTransparent: { + "0%": { + opacity: 1, + }, + "40%": { + opacity: 1, + }, + "100%": { + opacity: 0, + }, + }, + shake: { + "10%, 90%": { + transform: "translate3d(-1px, 0, 0)", + }, + "20%, 80%": { + transform: "translate3d(2px, 0, 0)", + }, + "30%, 50%, 70%": { + transform: "translate3d(-4px, 0, 0)", + }, + "40%, 60%": { + transform: "translate3d(4px, 0, 0)", + }, + }, + }), + }, + }, + plugins: [require("@tailwindcss/forms")], +}; diff --git a/apps/login/test-setup.ts b/apps/login/test-setup.ts new file mode 100644 index 0000000000..f149f27ae4 --- /dev/null +++ b/apps/login/test-setup.ts @@ -0,0 +1 @@ +import "@testing-library/jest-dom/vitest"; diff --git a/apps/login/tsconfig.json b/apps/login/tsconfig.json new file mode 100644 index 0000000000..4cec65b3dc --- /dev/null +++ b/apps/login/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules", "integration", "acceptance"] +} diff --git a/apps/login/turbo.json b/apps/login/turbo.json new file mode 100644 index 0000000000..b7b4e5fd27 --- /dev/null +++ b/apps/login/turbo.json @@ -0,0 +1,22 @@ +{ + "extends": ["//"], + "tasks": { + "build": { + "outputs": ["dist/**", ".next/**", "!.next/cache/**"], + "dependsOn": ["@zitadel/client#build"] + }, + "build:login:standalone": { + "outputs": ["dist/**", ".next/**", "!.next/cache/**"], + "dependsOn": ["@zitadel/client#build"] + }, + "dev": { + "dependsOn": ["@zitadel/client#build"] + }, + "test": { + "dependsOn": ["@zitadel/client#build"] + }, + "test:unit": { + "dependsOn": ["@zitadel/client#build"] + } + } +} diff --git a/apps/login/vitest.config.mts b/apps/login/vitest.config.mts new file mode 100644 index 0000000000..a45dd4c0ee --- /dev/null +++ b/apps/login/vitest.config.mts @@ -0,0 +1,12 @@ +import react from "@vitejs/plugin-react"; +import tsconfigPaths from "vite-tsconfig-paths"; +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + plugins: [tsconfigPaths(), react()], + test: { + include: ["src/**/*.test.ts", "src/**/*.test.tsx"], + environment: "jsdom", + setupFiles: ["./test-setup.ts"], + }, +}); diff --git a/buf.gen.yaml b/buf.gen.yaml index 858a1e6404..5a29ba9cd3 100644 --- a/buf.gen.yaml +++ b/buf.gen.yaml @@ -19,3 +19,5 @@ plugins: out: .artifacts/grpc - plugin: zitadel out: .artifacts/grpc + - plugin: connect-go + out: .artifacts/grpc diff --git a/build/login/Dockerfile b/build/login/Dockerfile new file mode 100644 index 0000000000..cd75728738 --- /dev/null +++ b/build/login/Dockerfile @@ -0,0 +1,45 @@ +FROM node:20-alpine AS base + +FROM base AS build +ENV PNPM_HOME="/pnpm" +ENV PATH="$PNPM_HOME:$PATH" +RUN corepack enable && COREPACK_ENABLE_DOWNLOAD_PROMPT=0 corepack prepare pnpm@9.1.2 --activate && \ + apk update && apk add --no-cache && \ + rm -rf /var/cache/apk/* +WORKDIR /app +COPY pnpm-lock.yaml pnpm-workspace.yaml ./ +RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store pnpm fetch --frozen-lockfile \ + --filter @zitadel/login \ + --filter @zitadel/client \ + --filter @zitadel/proto +COPY package.json ./ +COPY apps/login/package.json ./apps/login/package.json +COPY packages/zitadel-proto/package.json ./packages/zitadel-proto/package.json +COPY packages/zitadel-client/package.json ./packages/zitadel-client/package.json +RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store pnpm install --frozen-lockfile \ + --filter @zitadel/login \ + --filter @zitadel/client \ + --filter @zitadel/proto +COPY . . +RUN pnpm turbo build:login:standalone + +FROM scratch AS build-out +COPY /apps/login/public ./apps/login/public +COPY --from=build /app/apps/login/.next/standalone ./ +COPY --from=build /app/apps/login/.next/static ./apps/login/.next/static + +FROM base AS login-standalone +WORKDIR /runtime +RUN addgroup --system --gid 1001 nodejs && \ + adduser --system --uid 1001 nextjs +# If /.env-file/.env is mounted into the container, its variables are made available to the server before it starts up. +RUN mkdir -p /.env-file && touch /.env-file/.env && chown -R nextjs:nodejs /.env-file +COPY --chown=nextjs:nodejs apps/login/scripts ./ +COPY --chown=nextjs:nodejs --from=build-out . . +USER nextjs +ENV HOSTNAME="0.0.0.0" +ENV PORT=3000 +# TODO: Check healthy, not ready +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD ["/bin/sh", "-c", "node ./healthcheck.js http://localhost:${PORT}/ui/v2/login/healthy"] +ENTRYPOINT ["./entrypoint.sh"] diff --git a/build/login/Dockerfile.dockerignore b/build/login/Dockerfile.dockerignore new file mode 100644 index 0000000000..c2a0ee4a18 --- /dev/null +++ b/build/login/Dockerfile.dockerignore @@ -0,0 +1,38 @@ +* + +!apps/login/constants +!apps/login/scripts +!apps/login/src +!apps/login/public +!apps/login/locales +!apps/login/next.config.mjs +!apps/login/next-env-vars.d.ts +!apps/login/next-env.d.ts +!apps/login/tailwind.config.mjs +!apps/login/postcss.config.cjs +!apps/login/tsconfig.json +!apps/login/package.json +!apps/login/turbo.json + +!package.json +!pnpm-lock.yaml +!pnpm-workspace.yaml +!turbo.json + +!packages/zitadel-proto/package.json +!packages/zitadel-proto/buf.gen.yaml +!packages/zitadel-proto/turbo.json + +!packages/zitadel-client/package.json +!packages/zitadel-client/**/package.json +!packages/zitadel-client/src +!packages/zitadel-client/tsconfig.json +!packages/zitadel-client/tsup.config.ts +!packages/zitadel-client/turbo.json + +!proto + +**/*.md +**/node_modules +**/*.test.ts +**/*.test.tsx diff --git a/build/workflow.Dockerfile b/build/workflow.Dockerfile deleted file mode 100644 index 2286531192..0000000000 --- a/build/workflow.Dockerfile +++ /dev/null @@ -1,295 +0,0 @@ -# ############################################################################## -# core -# ############################################################################## - -# ####################################### -# download dependencies -# ####################################### -FROM golang:buster AS core-deps - -WORKDIR /go/src/github.com/zitadel/zitadel - -COPY go.mod . -COPY go.sum . - -RUN go mod download - -# ####################################### -# compile custom protoc plugins -# ####################################### -FROM golang:buster AS core-api-generator - -WORKDIR /go/src/github.com/zitadel/zitadel - -COPY go.mod . -COPY go.sum . -COPY internal/protoc internal/protoc -COPY pkg/grpc/protoc/v2 pkg/grpc/protoc/v2 - -RUN go install internal/protoc/protoc-gen-authoption/main.go \ - && mv $(go env GOPATH)/bin/main $(go env GOPATH)/bin/protoc-gen-authoption \ - && go install internal/protoc/protoc-gen-zitadel/main.go \ - && mv $(go env GOPATH)/bin/main $(go env GOPATH)/bin/protoc-gen-zitadel - -# ####################################### -# build backend stub -# ####################################### -FROM golang:buster AS core-api - -WORKDIR /go/src/github.com/zitadel/zitadel - -COPY go.mod . -COPY go.sum . -COPY proto proto -COPY buf.*.yaml . -COPY Makefile Makefile -COPY --from=core-api-generator /go/bin /usr/local/bin - -RUN make grpc - -# ####################################### -# generate code for login ui -# ####################################### -FROM golang:buster AS core-login - -WORKDIR /go/src/github.com/zitadel/zitadel - -COPY Makefile Makefile -COPY internal/api/ui/login/static internal/api/ui/login/static -COPY internal/api/ui/login/statik internal/api/ui/login/statik -COPY internal/notification/static internal/notification/static -COPY internal/notification/statik internal/notification/statik -COPY internal/static internal/static -COPY internal/statik internal/statik - -RUN make static - -# ####################################### -# generate code for assets -# ####################################### -FROM golang:buster AS core-assets -WORKDIR /go/src/github.com/zitadel/zitadel - -COPY go.mod . -COPY go.sum . -COPY Makefile Makefile -COPY internal/api/assets/generator internal/api/assets/generator -COPY internal/config internal/config -COPY internal/errors internal/errors -COPY --from=core-api /go/src/github.com/zitadel/zitadel/openapi/v2 openapi/v2 - -RUN make assets - -# ####################################### -# Gather all core files -# ####################################### -FROM core-deps AS core-gathered - -COPY cmd cmd -COPY internal internal -COPY pkg pkg -COPY proto proto -COPY openapi openapi -COPY statik statik -COPY main.go main.go -COPY --from=core-api /go/src/github.com/zitadel/zitadel . -COPY --from=core-login /go/src/github.com/zitadel/zitadel . -COPY --from=core-assets /go/src/github.com/zitadel/zitadel/internal ./internal - -# ############################################################################## -# build console -# ############################################################################## - -# ####################################### -# download console dependencies -# ####################################### -FROM node:20-buster AS console-deps - -WORKDIR /zitadel/console - -COPY console/package.json . -COPY console/yarn.lock . - -RUN yarn install --frozen-lockfile - -# ####################################### -# generate console client -# ####################################### -FROM node:20-buster AS console-client - -WORKDIR /zitadel/console - -# install buf -COPY --from=bufbuild/buf:latest /usr/local/bin/* /usr/local/bin/ -ENV PATH="/usr/local/bin:${PATH}" - -COPY console/package.json . -COPY console/buf.*.yaml . -COPY proto ../proto - -RUN yarn generate - -# ####################################### -# Gather all console files -# ####################################### -FROM console-deps as console-gathered - -COPY --from=console-client /zitadel/console/src/app/proto/generated src/app/proto/generated - -COPY console/src src -COPY console/angular.json . -COPY console/ngsw-config.json . -COPY console/tsconfig* . - -# ####################################### -# Build console -# ####################################### -FROM console-gathered AS console -RUN yarn build - -# ############################################################################## -# build the executable -# ############################################################################## - -# ####################################### -# build executable -# ####################################### -FROM core-gathered AS compile - -ARG GOOS -ARG GOARCH - -COPY --from=console /zitadel/console/dist/console internal/api/ui/console/static/ - -RUN go build -o zitadel -ldflags="-s -w -race" \ - && chmod +x zitadel - -ENTRYPOINT [ "./zitadel" ] - -# ####################################### -# copy executable -# ####################################### -FROM scratch AS copy-executable -ARG GOOS -ARG GOARCH - -COPY --from=compile /go/src/github.com/zitadel/zitadel/zitadel /.artifacts/zitadel - -# ############################################################################## -# tests -# ############################################################################## -FROM ubuntu/postgres:latest AS test-core-base - -ARG DEBIAN_FRONTEND=noninteractive - -RUN apt-get update && \ - apt-get install -y --no-install-recommends \ - gcc \ - make \ - ca-certificates \ - gcc \ - && \ - update-ca-certificates; \ - rm -rf /var/lib/apt/lists/* - -# install go -COPY --from=golang:latest /usr/local/go/ /usr/local/go/ -ENV PATH="/go/bin:/usr/local/go/bin:${PATH}" - -WORKDIR /go/src/github.com/zitadel/zitadel - -# default vars -ENV POSTGRES_USER=zitadel -ENV POSTGRES_DB=zitadel -ENV POSTGRES_PASSWORD=postgres -ENV POSTGRES_HOST_AUTH_METHOD=trust - -ENV PGUSER=zitadel -ENV PGDATABASE=zitadel -ENV PGPASSWORD=postgres - -ENV CGO_ENABLED=1 - -# copy zitadel files -COPY --from=core-deps /go/pkg/mod /root/go/pkg/mod -COPY --from=core-gathered /go/src/github.com/zitadel/zitadel . - -# ####################################### -# unit test core -# ####################################### -FROM test-core-base AS test-core-unit -RUN go test -race -v -coverprofile=profile.cov ./... - -# ####################################### -# coverage output -# ####################################### -FROM scratch AS coverage-core-unit -COPY --from=test-core-unit /go/src/github.com/zitadel/zitadel/profile.cov /coverage/ - -# ####################################### -# integration test core -# ####################################### -FROM test-core-base AS test-core-integration -ENV ZITADEL_MASTERKEY=MasterkeyNeedsToHave32Characters - -COPY build/core-integration-test.sh /usr/local/bin/run-tests.sh -RUN chmod +x /usr/local/bin/run-tests.sh - -RUN run-tests.sh - -# ####################################### -# coverage output -# ####################################### -FROM scratch AS coverage-core-integration -COPY --from=test-core-integration /go/src/github.com/zitadel/zitadel/profile.cov /coverage/ - -# ############################################################################## -# linting -# ############################################################################## - -# ####################################### -# api -# ####################################### -FROM bufbuild/buf:latest AS lint-api - -COPY proto proto -COPY buf.*.yaml . - -RUN buf lint - -# ####################################### -# console -# ####################################### -FROM console-gathered AS lint-console - -COPY console/.eslintrc.js . -COPY console/.prettier* . -RUN yarn lint - -# ####################################### -# core -# ####################################### -FROM golangci/golangci-lint:latest AS lint-core -ARG LINT_EXIT_CODE=1 - -WORKDIR /go/src/github.com/zitadel/zitadel - -COPY .golangci.yaml . -COPY .git/ .git/ -COPY --from=core-deps /go/pkg/mod /go/pkg/mod -COPY --from=core-gathered /go/src/github.com/zitadel/zitadel . - -RUN git fetch https://github.com/zitadel/zitadel main:main - -RUN golangci-lint run \ - --timeout 10m \ - --config ./.golangci.yaml \ - --out-format=github-actions:report,colored-line-number \ - --issues-exit-code=${LINT_EXIT_CODE} \ - --concurrency=$(getconf _NPROCESSORS_ONLN) - -# ####################################### -# report output -# ####################################### -FROM scratch AS lint-core-report -COPY --from=lint-core /go/src/github.com/zitadel/zitadel/report . \ No newline at end of file diff --git a/build/zitadel/.dockerignore b/build/zitadel/.dockerignore new file mode 100644 index 0000000000..83c401b28a --- /dev/null +++ b/build/zitadel/.dockerignore @@ -0,0 +1,3 @@ +* +!build/zitadel/entrypoint.sh +!zitadel diff --git a/build/Dockerfile b/build/zitadel/Dockerfile similarity index 93% rename from build/Dockerfile rename to build/zitadel/Dockerfile index 769f04023e..4abc1f388c 100644 --- a/build/Dockerfile +++ b/build/zitadel/Dockerfile @@ -4,7 +4,7 @@ ARG TARGETPLATFORM RUN apt-get update && apt-get install ca-certificates -y -COPY build/entrypoint.sh /app/entrypoint.sh +COPY build/zitadel/entrypoint.sh /app/entrypoint.sh COPY zitadel /app/zitadel RUN useradd -s "" --home / zitadel && \ diff --git a/build/entrypoint.sh b/build/zitadel/entrypoint.sh similarity index 100% rename from build/entrypoint.sh rename to build/zitadel/entrypoint.sh diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml index c576a4993a..5b3c91ec6f 100644 --- a/cmd/defaults.yaml +++ b/cmd/defaults.yaml @@ -526,13 +526,13 @@ OIDC: CharSet: "BCDFGHJKLMNPQRSTVWXZ" # ZITADEL_OIDC_DEVICEAUTH_USERCODE_CHARSET CharAmount: 8 # ZITADEL_OIDC_DEVICEAUTH_USERCODE_CHARARMOUNT DashInterval: 4 # ZITADEL_OIDC_DEVICEAUTH_USERCODE_DASHINTERVAL - DefaultLoginURLV2: "/login?authRequest=" # ZITADEL_OIDC_DEFAULTLOGINURLV2 - DefaultLogoutURLV2: "/logout?post_logout_redirect=" # ZITADEL_OIDC_DEFAULTLOGOUTURLV2 + DefaultLoginURLV2: "/ui/v2/login/login?authRequest=" # ZITADEL_OIDC_DEFAULTLOGINURLV2 + DefaultLogoutURLV2: "/ui/v2/login/logout?post_logout_redirect=" # ZITADEL_OIDC_DEFAULTLOGOUTURLV2 PublicKeyCacheMaxAge: 24h # ZITADEL_OIDC_PUBLICKEYCACHEMAXAGE DefaultBackChannelLogoutLifetime: 15m # ZITADEL_OIDC_DEFAULTBACKCHANNELLOGOUTLIFETIME SAML: - DefaultLoginURLV2: "/login?authRequest=" # ZITADEL_SAML_DEFAULTLOGINURLV2 + DefaultLoginURLV2: "/ui/v2/login/login?samlRequest=" # ZITADEL_SAML_DEFAULTLOGINURLV2 ProviderConfig: MetadataConfig: Path: "/metadata" # ZITADEL_SAML_PROVIDERCONFIG_METADATACONFIG_PATH @@ -618,6 +618,16 @@ EncryptionKeys: UserAgentCookieKeyID: "userAgentCookieKey" # ZITADEL_ENCRYPTIONKEYS_USERAGENTCOOKIEKEYID SystemAPIUsers: + # - superuser: + # Path: /path/to/superuser/key.pem + # Memberships: + # - MemberType: Organization + # Roles: "ORG_OWNER" + # AggregateID: "123456789012345678" + # - MemberType: Project + # Roles: "PROJECT_OWNER" + + # # Add keys for authentication of the systemAPI here: # # you can specify any name for the user, but they will have to match the `issuer` and `sub` claim in the JWT: # - superuser: @@ -829,6 +839,13 @@ DefaultInstance: Pat: # date format: 2023-01-01T00:00:00Z ExpirationDate: # ZITADEL_DEFAULTINSTANCE_ORG_MACHINE_PAT_EXPIRATIONDATE + LoginClient: + Machine: + Username: # ZITADEL_DEFAULTINSTANCE_ORG_LOGINCLIENT_MACHINE_USERNAME + Name: # ZITADEL_DEFAULTINSTANCE_ORG_LOGINCLIENT_MACHINE_NAME + Pat: + # date format: 2023-01-01T00:00:00Z + ExpirationDate: # ZITADEL_DEFAULTINSTANCE_ORG_LOGINCLIENT_PAT_EXPIRATIONDATE SecretGenerators: ClientSecret: Length: 64 # ZITADEL_DEFAULTINSTANCE_SECRETGENERATORS_CLIENTSECRET_LENGTH @@ -1121,8 +1138,8 @@ DefaultInstance: # OIDCSingleV1SessionTermination: false # ZITADEL_DEFAULTINSTANCE_FEATURES_OIDCSINGLEV1SESSIONTERMINATION # DisableUserTokenEvent: false # ZITADEL_DEFAULTINSTANCE_FEATURES_DISABLEUSERTOKENEVENT # EnableBackChannelLogout: false # ZITADEL_DEFAULTINSTANCE_FEATURES_ENABLEBACKCHANNELLOGOUT - # LoginV2: - # Required: false # ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2_REQUIRED + LoginV2: + Required: true # ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2_REQUIRED # BaseURI: "" # ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2_BASEURI # PermissionCheckV2: false # ZITADEL_DEFAULTINSTANCE_FEATURES_PERMISSIONCHECKV2 # ConsoleUseV2UserApi: false # ZITADEL_DEFAULTINSTANCE_FEATURES_CONSOLEUSEV2USERAPI @@ -1186,6 +1203,40 @@ DefaultInstance: # If an audit log retention is set using an instance limit, it will overwrite the system default. AuditLogRetention: 0s # ZITADEL_AUDITLOGRETENTION +# The ServicePing are periodic reports of analytics data and the usage of ZITADEL. +# It is sent to a central endpoint to help us improve ZITADEL. +# It's enabled by default, but you can opt out either completely or by disabling specific telemetry data. +ServicePing: + # By setting Enabled to false, the service ping is disabled completely. + Enabled: true # ZITADEL_SERVICEPING_ENABLED + # The endpoint to which the reports are sent. The endpoint is used as a base path. Individual reports are sent to the endpoint with a specific path. + Endpoint: "https://zitadel.com/api/ping" # ZITADEL_SERVICEPING_ENDPOINT + # Interval at which the service ping is sent to the endpoint. + # The interval is in the format of a cron expression. + # By default, it is set to every daily. + # Note that if the interval is set to `@daily`, we randomize the time to prevent all systems from sending their reports at the same time. + # If you want to send the service ping at a specific time, you can set the interval to a cron expression like "@midnight" or "15 4 * * *". + Interval: "@daily" # ZITADEL_SERVICEPING_INTERVAL + # Maximum number of attempts for each individual report to be sent. + # If one report fails, it will be retried up to this number of times. + # Other reports will still be handled in parallel and have their own retry count. + # This means if the base information only succeeded after 3 attempts, + # the resource count still has 5 attempts to be sent. + MaxAttempts: 5 # ZITADEL_SERVICEPING_MAXATTEMPTS + # The following features can be enabled or disabled individually. + # By default, all features are enabled. + # Note that if the service ping is enabled, base information about the system is always sent. + # This includes the version and the id, creation date and domains of all instances. + # If you disable a feature, it will not be sent in the service ping. + # Some features provide additional configuration options, if enabled. + Telemetry: + # ResourceCount is a periodic report of the number of resources in ZITADEL. + # This includes the number of users, organizations, projects, and other resources. + ResourceCount: + Enabled: true # ZITADEL_SERVICEPING_TELEMETRY_RESOURCECOUNT_ENABLED + # The number of counts that are sent in one batch. + BulkSize: 10000 # ZITADEL_SERVICEPING_TELEMETRY_RESOURCECOUNT_BULKSIZE + InternalAuthZ: # Configure the RolePermissionMappings by environment variable using JSON notation: # ZITADEL_INTERNALAUTHZ_ROLEPERMISSIONMAPPINGS='[{"role": "IAM_OWNER", "permissions": ["iam.write"]}, {"role": "ORG_OWNER", "permissions": ["org.write"]}]' diff --git a/cmd/setup/03.go b/cmd/setup/03.go index 588ac71610..e8c51c79c6 100644 --- a/cmd/setup/03.go +++ b/cmd/setup/03.go @@ -20,12 +20,13 @@ import ( ) type FirstInstance struct { - InstanceName string - DefaultLanguage language.Tag - Org command.InstanceOrgSetup - MachineKeyPath string - PatPath string - Features *command.InstanceFeatures + InstanceName string + DefaultLanguage language.Tag + Org command.InstanceOrgSetup + MachineKeyPath string + PatPath string + LoginClientPatPath string + Features *command.InstanceFeatures Skip bool @@ -121,16 +122,18 @@ func (mig *FirstInstance) Execute(ctx context.Context, _ eventstore.Event) error } } - _, token, key, _, err := cmd.SetUpInstance(ctx, &mig.instanceSetup) + _, token, key, loginClientToken, _, err := cmd.SetUpInstance(ctx, &mig.instanceSetup) if err != nil { return err } - if mig.instanceSetup.Org.Machine != nil && + if (mig.instanceSetup.Org.Machine != nil && ((mig.instanceSetup.Org.Machine.Pat != nil && token == "") || - (mig.instanceSetup.Org.Machine.MachineKey != nil && key == nil)) { + (mig.instanceSetup.Org.Machine.MachineKey != nil && key == nil))) || + (mig.instanceSetup.Org.LoginClient != nil && + (mig.instanceSetup.Org.LoginClient.Pat != nil && loginClientToken == "")) { return err } - return mig.outputMachineAuthentication(key, token) + return mig.outputMachineAuthentication(key, token, loginClientToken) } func (mig *FirstInstance) verifyEncryptionKeys(ctx context.Context) (*crypto_db.Database, error) { @@ -150,7 +153,7 @@ func (mig *FirstInstance) verifyEncryptionKeys(ctx context.Context) (*crypto_db. return keyStorage, nil } -func (mig *FirstInstance) outputMachineAuthentication(key *command.MachineKey, token string) error { +func (mig *FirstInstance) outputMachineAuthentication(key *command.MachineKey, token, loginClientToken string) error { if key != nil { keyDetails, err := key.Detail() if err != nil { @@ -165,6 +168,11 @@ func (mig *FirstInstance) outputMachineAuthentication(key *command.MachineKey, t return err } } + if loginClientToken != "" { + if err := outputStdoutOrPath(mig.LoginClientPatPath, loginClientToken); err != nil { + return err + } + } return nil } diff --git a/cmd/setup/53.go b/cmd/setup/53.go index 83a7b1c0e2..952fc37916 100644 --- a/cmd/setup/53.go +++ b/cmd/setup/53.go @@ -33,5 +33,5 @@ func (mig *InitPermittedOrgsFunction53) Execute(ctx context.Context, _ eventstor } func (*InitPermittedOrgsFunction53) String() string { - return "53_init_permitted_orgs_function" + return "53_init_permitted_orgs_function_v2" } diff --git a/cmd/setup/53/01-get-permissions-from-JSON.sql b/cmd/setup/53/01-get-permissions-from-JSON.sql index b6415fa180..531184dbe4 100644 --- a/cmd/setup/53/01-get-permissions-from-JSON.sql +++ b/cmd/setup/53/01-get-permissions-from-JSON.sql @@ -1,23 +1,28 @@ +DROP FUNCTION IF EXISTS eventstore.check_system_user_perms; DROP FUNCTION IF EXISTS eventstore.get_system_permissions; +DROP TYPE IF EXISTS eventstore.project_grant; + +/* + Function get_system_permissions unpacks an JSON array of system member permissions, + into a table format. Each array entry maps to one row representing a membership which + contained the req_permission. + + [ + { + "member_type": "IAM", + "aggregate_id": "310716990375453665", + "object_id": "", + "permissions": ["iam.read", "iam.write", "iam.policy.read"] + }, + ... + ] + + | member_type | aggregate_id | object_id | + | "IAM" | "310716990375453665" | null | +*/ CREATE OR REPLACE FUNCTION eventstore.get_system_permissions( permissions_json JSONB - /* - [ - { - "member_type": "System", - "aggregate_id": "", - "object_id": "", - "permissions": ["iam.read", "iam.write", "iam.polic.read"] - }, - { - "member_type": "IAM", - "aggregate_id": "310716990375453665", - "object_id": "", - "permissions": ["iam.read", "iam.write", "iam.polic.read"] - } - ] - */ , permm TEXT ) RETURNS TABLE ( @@ -25,7 +30,7 @@ RETURNS TABLE ( aggregate_id TEXT, object_id TEXT ) - LANGUAGE 'plpgsql' + LANGUAGE 'plpgsql' IMMUTABLE AS $$ BEGIN RETURN QUERY @@ -37,7 +42,73 @@ BEGIN permission FROM jsonb_array_elements(permissions_json) AS perm CROSS JOIN jsonb_array_elements_text(perm->'permissions') AS permission) AS res - WHERE res. permission= permm; + WHERE res.permission = permm; END; $$; +/* + Type project_grant is composite identifier using its project and grant IDs. +*/ +CREATE TYPE eventstore.project_grant AS ( + project_id TEXT -- mapped from a permission's aggregate_id + , grant_id TEXT -- mapped from a permission's object_id +); + +/* + Function check_system_user_perms uses system member permissions to establish + on which organization, project or project grant the user has the requested permission. + The permission can also apply to the complete instance when a IAM membership matches + the requested instance ID, or through system membership. + + See eventstore.get_system_permissions() on the supported JSON format. +*/ +CREATE OR REPLACE FUNCTION eventstore.check_system_user_perms( + system_user_perms JSONB + , req_instance_id TEXT + , perm TEXT + + , instance_permitted OUT BOOLEAN + , org_ids OUT TEXT[] + , project_ids OUT TEXT[] + , project_grants OUT eventstore.project_grant[] +) + LANGUAGE 'plpgsql' IMMUTABLE +AS $$ +BEGIN + -- make sure no nulls are returned + instance_permitted := FALSE; + org_ids := ARRAY[]::TEXT[]; + project_ids := ARRAY[]::TEXT[]; + project_grants := ARRAY[]::eventstore.project_grant[]; + DECLARE + p RECORD; + BEGIN + FOR p IN SELECT member_type, aggregate_id, object_id + FROM eventstore.get_system_permissions(system_user_perms, perm) + LOOP + CASE p.member_type + WHEN 'System' THEN + instance_permitted := TRUE; + RETURN; + WHEN 'IAM' THEN + IF p.aggregate_id = req_instance_id THEN + instance_permitted := TRUE; + RETURN; + END IF; + WHEN 'Organization' THEN + IF p.aggregate_id != '' THEN + org_ids := array_append(org_ids, p.aggregate_id); + END IF; + WHEN 'Project' THEN + IF p.aggregate_id != '' THEN + project_ids := array_append(project_ids, p.aggregate_id); + END IF; + WHEN 'ProjectGrant' THEN + IF p.aggregate_id != '' THEN + project_grants := array_append(project_grants, ROW(p.aggregate_id, p.object_id)::eventstore.project_grant); + END IF; + END CASE; + END LOOP; + END; +END; +$$; diff --git a/cmd/setup/53/02-permitted_orgs_function.sql b/cmd/setup/53/02-permitted_orgs_function.sql index b6f61c6225..fbc7eaee59 100644 --- a/cmd/setup/53/02-permitted_orgs_function.sql +++ b/cmd/setup/53/02-permitted_orgs_function.sql @@ -1,144 +1,71 @@ -DROP FUNCTION IF EXISTS eventstore.check_system_user_perms; +DROP FUNCTION IF EXISTS eventstore.permitted_orgs; +DROP FUNCTION IF EXISTS eventstore.find_roles; -CREATE OR REPLACE FUNCTION eventstore.check_system_user_perms( - system_user_perms JSONB +-- find_roles finds all roles containing the permission +CREATE OR REPLACE FUNCTION eventstore.find_roles( + req_instance_id TEXT , perm TEXT - , filter_orgs TEXT - , org_ids OUT TEXT[] + , roles OUT TEXT[] ) - LANGUAGE 'plpgsql' +LANGUAGE 'plpgsql' STABLE AS $$ BEGIN - - WITH found_permissions(member_type, aggregate_id, object_id ) AS ( - SELECT * FROM eventstore.get_system_permissions( - system_user_perms, - perm) - ) - - SELECT array_agg(DISTINCT o.org_id) INTO org_ids - FROM eventstore.instance_orgs o, found_permissions - WHERE - CASE WHEN (SELECT TRUE WHERE found_permissions.member_type = 'System' LIMIT 1) THEN - TRUE - WHEN (SELECT TRUE WHERE found_permissions.member_type = 'IAM' LIMIT 1) THEN - -- aggregate_id not present - CASE WHEN (SELECT TRUE WHERE '' = ANY ( - ( - SELECT array_agg(found_permissions.aggregate_id) - FROM found_permissions - WHERE member_type = 'IAM' - GROUP BY member_type - LIMIT 1 - )::TEXT[])) THEN - TRUE - -- aggregate_id is present - ELSE - o.instance_id = ANY ( - ( - SELECT array_agg(found_permissions.aggregate_id) - FROM found_permissions - WHERE member_type = 'IAM' - GROUP BY member_type - LIMIT 1 - )::TEXT[]) - END - WHEN (SELECT TRUE WHERE found_permissions.member_type = 'Organization' LIMIT 1) THEN - -- aggregate_id not present - CASE WHEN (SELECT TRUE WHERE '' = ANY ( - ( - SELECT array_agg(found_permissions.aggregate_id) - FROM found_permissions - WHERE member_type = 'Organization' - GROUP BY member_type - LIMIT 1 - )::TEXT[])) THEN - TRUE - -- aggregate_id is present - ELSE - o.org_id = ANY ( - ( - SELECT array_agg(found_permissions.aggregate_id) - FROM found_permissions - WHERE member_type = 'Organization' - GROUP BY member_type - LIMIT 1 - )::TEXT[]) - END - END - AND - CASE WHEN filter_orgs != '' - THEN o.org_id IN (filter_orgs) - ELSE TRUE END - LIMIT 1; + SELECT array_agg(rp.role) INTO roles + FROM eventstore.role_permissions rp + WHERE rp.instance_id = req_instance_id + AND rp.permission = perm; END; $$; - -DROP FUNCTION IF EXISTS eventstore.permitted_orgs; - CREATE OR REPLACE FUNCTION eventstore.permitted_orgs( - instanceId TEXT - , userId TEXT + req_instance_id TEXT + , auth_user_id TEXT , system_user_perms JSONB , perm TEXT - , filter_orgs TEXT + , filter_org TEXT + , instance_permitted OUT BOOLEAN , org_ids OUT TEXT[] ) - LANGUAGE 'plpgsql' + LANGUAGE 'plpgsql' STABLE AS $$ BEGIN - - -- if system user - IF system_user_perms IS NOT NULL THEN - org_ids := eventstore.check_system_user_perms(system_user_perms, perm, filter_orgs); - -- if human/machine user - ELSE + -- if system user + IF system_user_perms IS NOT NULL THEN + SELECT p.instance_permitted, p.org_ids INTO instance_permitted, org_ids + FROM eventstore.check_system_user_perms(system_user_perms, req_instance_id, perm) p; + RETURN; + END IF; + + -- if human/machine user DECLARE - matched_roles TEXT[]; -- roles containing permission - BEGIN - - SELECT array_agg(rp.role) INTO matched_roles - FROM eventstore.role_permissions rp - WHERE rp.instance_id = instanceId - AND rp.permission = perm; - - -- First try if the permission was granted thru an instance-level role - DECLARE - has_instance_permission bool; - BEGIN - SELECT true INTO has_instance_permission - FROM eventstore.instance_members im - WHERE im.role = ANY(matched_roles) - AND im.instance_id = instanceId - AND im.user_id = userId - LIMIT 1; + matched_roles TEXT[] := eventstore.find_roles(req_instance_id, perm); + BEGIN + -- First try if the permission was granted thru an instance-level role + SELECT true INTO instance_permitted + FROM eventstore.instance_members im + WHERE im.role = ANY(matched_roles) + AND im.instance_id = req_instance_id + AND im.user_id = auth_user_id + LIMIT 1; - IF has_instance_permission THEN - -- Return all organizations or only those in filter_orgs - SELECT array_agg(o.org_id) INTO org_ids - FROM eventstore.instance_orgs o - WHERE o.instance_id = instanceId - AND CASE WHEN filter_orgs != '' - THEN o.org_id IN (filter_orgs) - ELSE TRUE END; - RETURN; + org_ids := ARRAY[]::TEXT[]; + IF instance_permitted THEN + RETURN; END IF; - END; - - -- Return the organizations where permission were granted thru org-level roles - SELECT array_agg(sub.org_id) INTO org_ids - FROM ( - SELECT DISTINCT om.org_id - FROM eventstore.org_members om - WHERE om.role = ANY(matched_roles) - AND om.instance_id = instanceID - AND om.user_id = userId - ) AS sub; + instance_permitted := FALSE; + + -- Return the organizations where permission were granted thru org-level roles + SELECT array_agg(sub.org_id) INTO org_ids + FROM ( + SELECT DISTINCT om.org_id + FROM eventstore.org_members om + WHERE om.role = ANY(matched_roles) + AND om.instance_id = req_instance_id + AND om.user_id = auth_user_id + AND (filter_org IS NULL OR om.org_id = filter_org) + ) AS sub; END; - END IF; END; $$; - diff --git a/cmd/setup/53/03-permitted_projects_func.sql b/cmd/setup/53/03-permitted_projects_func.sql new file mode 100644 index 0000000000..8c17481ce8 --- /dev/null +++ b/cmd/setup/53/03-permitted_projects_func.sql @@ -0,0 +1,58 @@ +-- recreate the view to include the resource_owner +CREATE OR REPLACE VIEW eventstore.project_members AS +SELECT instance_id, aggregate_id as project_id, object_id as user_id, text_value as role, resource_owner as org_id +FROM eventstore.fields +WHERE aggregate_type = 'project' +AND object_type = 'project_member_role' +AND field_name = 'project_role'; + +DROP FUNCTION IF EXISTS eventstore.permitted_projects; + +CREATE OR REPLACE FUNCTION eventstore.permitted_projects( + req_instance_id TEXT + , auth_user_id TEXT + , system_user_perms JSONB + , perm TEXT + , filter_org TEXT + + , instance_permitted OUT BOOLEAN + , org_ids OUT TEXT[] + , project_ids OUT TEXT[] +) + LANGUAGE 'plpgsql' STABLE +AS $$ +BEGIN + -- if system user + IF system_user_perms IS NOT NULL THEN + SELECT p.instance_permitted, p.org_ids INTO instance_permitted, org_ids, project_ids + FROM eventstore.check_system_user_perms(system_user_perms, req_instance_id, perm) p; + RETURN; + END IF; + + -- if human/machine user + SELECT * FROM eventstore.permitted_orgs( + req_instance_id + , auth_user_id + , system_user_perms + , perm + , filter_org + ) INTO instance_permitted, org_ids; + IF instance_permitted THEN + RETURN; + END IF; + DECLARE + matched_roles TEXT[] := eventstore.find_roles(req_instance_id, perm); + BEGIN + -- Get the projects where permission were granted thru project-level roles + SELECT array_agg(sub.project_id) INTO project_ids + FROM ( + SELECT DISTINCT pm.project_id + FROM eventstore.project_members pm + WHERE pm.role = ANY(matched_roles) + AND pm.instance_id = req_instance_id + AND pm.user_id = auth_user_id + AND (filter_org IS NULL OR pm.org_id = filter_org) + ) AS sub; + END; +END; +$$; diff --git a/cmd/setup/57.go b/cmd/setup/57.go new file mode 100644 index 0000000000..4c52018f1e --- /dev/null +++ b/cmd/setup/57.go @@ -0,0 +1,27 @@ +package setup + +import ( + "context" + _ "embed" + + "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/eventstore" +) + +var ( + //go:embed 57.sql + createResourceCounts string +) + +type CreateResourceCounts struct { + dbClient *database.DB +} + +func (mig *CreateResourceCounts) Execute(ctx context.Context, _ eventstore.Event) error { + _, err := mig.dbClient.ExecContext(ctx, createResourceCounts) + return err +} + +func (mig *CreateResourceCounts) String() string { + return "57_create_resource_counts" +} diff --git a/cmd/setup/57.sql b/cmd/setup/57.sql new file mode 100644 index 0000000000..f2f0a40202 --- /dev/null +++ b/cmd/setup/57.sql @@ -0,0 +1,106 @@ +CREATE TABLE IF NOT EXISTS projections.resource_counts +( + id SERIAL PRIMARY KEY, -- allows for easy pagination + instance_id TEXT NOT NULL, + table_name TEXT NOT NULL, -- needed for trigger matching, not in reports + parent_type TEXT NOT NULL, + parent_id TEXT NOT NULL, + resource_name TEXT NOT NULL, -- friendly name for reporting + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + amount INTEGER NOT NULL DEFAULT 1 CHECK (amount >= 0), + + UNIQUE (instance_id, parent_type, parent_id, table_name) +); + +-- count_resource is a trigger function which increases or decreases the count of a resource. +-- When creating the trigger the following required arguments (TG_ARGV) can be passed: +-- 1. The type of the parent +-- 2. The column name of the instance id +-- 3. The column name of the owner id +-- 4. The name of the resource +CREATE OR REPLACE FUNCTION projections.count_resource() + RETURNS trigger + LANGUAGE 'plpgsql' VOLATILE +AS $$ +DECLARE + -- trigger variables + tg_table_name TEXT := TG_TABLE_SCHEMA || '.' || TG_TABLE_NAME; + tg_parent_type TEXT := TG_ARGV[0]; + tg_instance_id_column TEXT := TG_ARGV[1]; + tg_parent_id_column TEXT := TG_ARGV[2]; + tg_resource_name TEXT := TG_ARGV[3]; + + tg_instance_id TEXT; + tg_parent_id TEXT; + + select_ids TEXT := format('SELECT ($1).%I, ($1).%I', tg_instance_id_column, tg_parent_id_column); +BEGIN + IF (TG_OP = 'INSERT') THEN + EXECUTE select_ids INTO tg_instance_id, tg_parent_id USING NEW; + + INSERT INTO projections.resource_counts(instance_id, table_name, parent_type, parent_id, resource_name) + VALUES (tg_instance_id, tg_table_name, tg_parent_type, tg_parent_id, tg_resource_name) + ON CONFLICT (instance_id, table_name, parent_type, parent_id) DO + UPDATE SET updated_at = now(), amount = projections.resource_counts.amount + 1; + + RETURN NEW; + ELSEIF (TG_OP = 'DELETE') THEN + EXECUTE select_ids INTO tg_instance_id, tg_parent_id USING OLD; + + UPDATE projections.resource_counts + SET updated_at = now(), amount = amount - 1 + WHERE instance_id = tg_instance_id + AND table_name = tg_table_name + AND parent_type = tg_parent_type + AND parent_id = tg_parent_id + AND resource_name = tg_resource_name + AND amount > 0; -- prevent check failure on negative amount. + + RETURN OLD; + END IF; +END +$$; + +-- delete_table_counts removes all resource counts for a TRUNCATED table. +CREATE OR REPLACE FUNCTION projections.delete_table_counts() + RETURNS trigger + LANGUAGE 'plpgsql' +AS $$ +DECLARE + -- trigger variables + tg_table_name TEXT := TG_TABLE_SCHEMA || '.' || TG_TABLE_NAME; +BEGIN + DELETE FROM projections.resource_counts + WHERE table_name = tg_table_name; +END +$$; + +-- delete_parent_counts removes all resource counts for a deleted parent. +-- 1. The type of the parent +-- 2. The column name of the instance id +-- 3. The column name of the owner id +CREATE OR REPLACE FUNCTION projections.delete_parent_counts() + RETURNS trigger + LANGUAGE 'plpgsql' +AS $$ +DECLARE + -- trigger variables + tg_parent_type TEXT := TG_ARGV[0]; + tg_instance_id_column TEXT := TG_ARGV[1]; + tg_parent_id_column TEXT := TG_ARGV[2]; + + tg_instance_id TEXT; + tg_parent_id TEXT; + + select_ids TEXT := format('SELECT ($1).%I, ($1).%I', tg_instance_id_column, tg_parent_id_column); +BEGIN + EXECUTE select_ids INTO tg_instance_id, tg_parent_id USING OLD; + + DELETE FROM projections.resource_counts + WHERE instance_id = tg_instance_id + AND parent_type = tg_parent_type + AND parent_id = tg_parent_id; + + RETURN OLD; +END +$$; diff --git a/cmd/setup/59.go b/cmd/setup/59.go new file mode 100644 index 0000000000..530937d1a5 --- /dev/null +++ b/cmd/setup/59.go @@ -0,0 +1,54 @@ +package setup + +import ( + "context" + "fmt" + + "github.com/zitadel/logging" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/repository/instance" +) + +type SetupWebkeys struct { + eventstore *eventstore.Eventstore + commands *command.Commands +} + +func (mig *SetupWebkeys) Execute(ctx context.Context, _ eventstore.Event) error { + instances, err := mig.eventstore.InstanceIDs( + ctx, + eventstore.NewSearchQueryBuilder(eventstore.ColumnsInstanceIDs). + OrderDesc(). + AddQuery(). + AggregateTypes(instance.AggregateType). + EventTypes(instance.InstanceAddedEventType). + Builder().ExcludeAggregateIDs(). + AggregateTypes(instance.AggregateType). + EventTypes(instance.InstanceRemovedEventType). + Builder(), + ) + if err != nil { + return fmt.Errorf("%s get instance IDs: %w", mig, err) + } + conf := &crypto.WebKeyRSAConfig{ + Bits: crypto.RSABits2048, + Hasher: crypto.RSAHasherSHA256, + } + + for _, instance := range instances { + ctx := authz.WithInstanceID(ctx, instance) + logging.Info("prepare initial webkeys for instance", "instance_id", instance, "migration", mig) + if err := mig.commands.GenerateInitialWebKeys(ctx, conf); err != nil { + return fmt.Errorf("%s generate initial webkeys: %w", mig, err) + } + } + return nil +} + +func (mig *SetupWebkeys) String() string { + return "59_setup_webkeys" +} diff --git a/cmd/setup/60.go b/cmd/setup/60.go new file mode 100644 index 0000000000..3f606c2212 --- /dev/null +++ b/cmd/setup/60.go @@ -0,0 +1,27 @@ +package setup + +import ( + "context" + _ "embed" + + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/serviceping" + "github.com/zitadel/zitadel/internal/v2/system" +) + +type GenerateSystemID struct { + eventstore *eventstore.Eventstore +} + +func (mig *GenerateSystemID) Execute(ctx context.Context, _ eventstore.Event) error { + id, err := serviceping.GenerateSystemID() + if err != nil { + return err + } + _, err = mig.eventstore.Push(ctx, system.NewIDGeneratedEvent(ctx, id)) + return err +} + +func (mig *GenerateSystemID) String() string { + return "60_generate_system_id" +} diff --git a/cmd/setup/config.go b/cmd/setup/config.go index 3493262611..bac73b0ae5 100644 --- a/cmd/setup/config.go +++ b/cmd/setup/config.go @@ -153,7 +153,10 @@ type Steps struct { s54InstancePositionIndex *InstancePositionIndex s55ExecutionHandlerStart *ExecutionHandlerStart s56IDPTemplate6SAMLFederatedLogout *IDPTemplate6SAMLFederatedLogout + s57CreateResourceCounts *CreateResourceCounts s58ReplaceLoginNames3View *ReplaceLoginNames3View + s59SetupWebkeys *SetupWebkeys + s60GenerateSystemID *GenerateSystemID } func MustNewSteps(v *viper.Viper) *Steps { diff --git a/cmd/setup/config_test.go b/cmd/setup/config_test.go index 8cf241cd6a..6c087fe402 100644 --- a/cmd/setup/config_test.go +++ b/cmd/setup/config_test.go @@ -36,8 +36,6 @@ func TestMustNewConfig(t *testing.T) { DefaultInstance: Features: LoginDefaultOrg: true - LegacyIntrospection: true - TriggerIntrospectionProjections: true UserSchema: true Log: Level: info @@ -47,10 +45,8 @@ Actions: `}, want: func(t *testing.T, config *Config) { assert.Equal(t, config.DefaultInstance.Features, &command.InstanceFeatures{ - LoginDefaultOrg: gu.Ptr(true), - LegacyIntrospection: gu.Ptr(true), - TriggerIntrospectionProjections: gu.Ptr(true), - UserSchema: gu.Ptr(true), + LoginDefaultOrg: gu.Ptr(true), + UserSchema: gu.Ptr(true), }) }, }, { diff --git a/cmd/setup/integration_test/permission_test.go b/cmd/setup/integration_test/permission_test.go new file mode 100644 index 0000000000..2b0c56865f --- /dev/null +++ b/cmd/setup/integration_test/permission_test.go @@ -0,0 +1,871 @@ +//go:build integration + +package setup_test + +import ( + "encoding/json" + "testing" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgtype" + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/repository/instance" + "github.com/zitadel/zitadel/internal/repository/org" + "github.com/zitadel/zitadel/internal/repository/permission" + "github.com/zitadel/zitadel/internal/repository/project" +) + +func TestGetSystemPermissions(t *testing.T) { + const query = "SELECT * FROM eventstore.get_system_permissions($1, $2);" + t.Parallel() + permissions := []authz.SystemUserPermissions{ + { + MemberType: authz.MemberTypeSystem, + Permissions: []string{"iam.read", "iam.write", "iam.policy.read"}, + }, + { + MemberType: authz.MemberTypeIAM, + AggregateID: "instanceID", + Permissions: []string{"iam.read", "iam.write", "iam.policy.read", "org.read", "project.read", "project.write"}, + }, + { + MemberType: authz.MemberTypeOrganization, + AggregateID: "orgID", + Permissions: []string{"org.read", "org.write", "org.policy.read", "project.read", "project.write"}, + }, + { + MemberType: authz.MemberTypeProject, + AggregateID: "projectID", + Permissions: []string{"project.read", "project.write"}, + }, + { + MemberType: authz.MemberTypeProjectGrant, + AggregateID: "projectID", + ObjectID: "grantID", + Permissions: []string{"project.read", "project.write"}, + }, + } + type result struct { + MemberType authz.MemberType + AggregateID string + ObjectID string + } + tests := []struct { + permm string + want []result + }{ + { + permm: "iam.read", + want: []result{ + { + MemberType: authz.MemberTypeSystem, + }, + { + MemberType: authz.MemberTypeIAM, + AggregateID: "instanceID", + }, + }, + }, + { + permm: "org.read", + want: []result{ + { + MemberType: authz.MemberTypeIAM, + AggregateID: "instanceID", + }, + { + MemberType: authz.MemberTypeOrganization, + AggregateID: "orgID", + }, + }, + }, + { + permm: "project.write", + want: []result{ + { + MemberType: authz.MemberTypeIAM, + AggregateID: "instanceID", + }, + { + MemberType: authz.MemberTypeOrganization, + AggregateID: "orgID", + }, + { + MemberType: authz.MemberTypeProject, + AggregateID: "projectID", + }, + { + MemberType: authz.MemberTypeProjectGrant, + AggregateID: "projectID", + ObjectID: "grantID", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.permm, func(t *testing.T) { + t.Parallel() + rows, err := dbPool.Query(CTX, query, database.NewJSONArray(permissions), tt.permm) + require.NoError(t, err) + got, err := pgx.CollectRows(rows, pgx.RowToStructByPos[result]) + require.NoError(t, err) + assert.ElementsMatch(t, tt.want, got) + }) + } +} + +func TestCheckSystemUserPerms(t *testing.T) { + // Use JSON because of the composite project_grants SQL type + const query = "SELECT row_to_json(eventstore.check_system_user_perms($1, $2, $3));" + t.Parallel() + type args struct { + reqInstanceID string + permissions []authz.SystemUserPermissions + permm string + } + tests := []struct { + name string + args args + want string + }{ + { + name: "iam.read, instance permitted from system", + args: args{ + reqInstanceID: "instanceID", + permissions: []authz.SystemUserPermissions{ + { + MemberType: authz.MemberTypeSystem, + Permissions: []string{"iam.read", "iam.write", "iam.policy.read"}, + }, + { + MemberType: authz.MemberTypeIAM, + AggregateID: "instanceID", + Permissions: []string{"iam.read", "iam.write", "iam.policy.read", "org.read"}, + }, + { + MemberType: authz.MemberTypeOrganization, + AggregateID: "orgID", + Permissions: []string{"org.read", "org.write", "org.policy.read", "project.read", "project.write"}, + }, + { + MemberType: authz.MemberTypeProject, + AggregateID: "projectID", + Permissions: []string{"project.read", "project.write"}, + }, + { + MemberType: authz.MemberTypeProjectGrant, + AggregateID: "projectID", + ObjectID: "grantID", + Permissions: []string{"project.read", "project.write"}, + }, + }, + permm: "iam.read", + }, + want: `{ + "instance_permitted": true, + "org_ids": [], + "project_grants": [], + "project_ids": [] + }`, + }, + { + name: "org.read, instance permitted", + args: args{ + reqInstanceID: "instanceID", + permissions: []authz.SystemUserPermissions{ + { + MemberType: authz.MemberTypeSystem, + Permissions: []string{"iam.read", "iam.write", "iam.policy.read"}, + }, + { + MemberType: authz.MemberTypeIAM, + AggregateID: "instanceID", + Permissions: []string{"iam.read", "iam.write", "iam.policy.read", "org.read"}, + }, + { + MemberType: authz.MemberTypeOrganization, + AggregateID: "orgID", + Permissions: []string{"org.read", "org.write", "org.policy.read", "project.read", "project.write"}, + }, + { + MemberType: authz.MemberTypeProject, + AggregateID: "projectID", + Permissions: []string{"project.read", "project.write"}, + }, + { + MemberType: authz.MemberTypeProjectGrant, + AggregateID: "projectID", + ObjectID: "grantID", + Permissions: []string{"project.read", "project.write"}, + }, + }, + permm: "org.read", + }, + want: `{ + "instance_permitted": true, + "org_ids": [], + "project_grants": [], + "project_ids": [] + }`, + }, + { + name: "project.read, org ID and project ID permitted", + args: args{ + reqInstanceID: "instanceID", + permissions: []authz.SystemUserPermissions{ + { + MemberType: authz.MemberTypeSystem, + Permissions: []string{"iam.read", "iam.write", "iam.policy.read"}, + }, + { + MemberType: authz.MemberTypeIAM, + AggregateID: "instanceID", + Permissions: []string{"iam.read", "iam.write", "iam.policy.read", "org.read"}, + }, + { + MemberType: authz.MemberTypeOrganization, + AggregateID: "orgID", + Permissions: []string{"org.read", "org.write", "org.policy.read", "project.read", "project.write"}, + }, + { + MemberType: authz.MemberTypeProject, + AggregateID: "projectID", + Permissions: []string{"project.read", "project.write"}, + }, + { + MemberType: authz.MemberTypeProjectGrant, + AggregateID: "projectID", + ObjectID: "grantID", + Permissions: []string{"project_grant.read", "project_grant.write"}, + }, + }, + permm: "project.read", + }, + want: `{ + "instance_permitted": false, + "org_ids": ["orgID"], + "project_ids": ["projectID"], + "project_grants": [] + }`, + }, + { + name: "project_grant.read, project grant ID permitted", + args: args{ + reqInstanceID: "instanceID", + permissions: []authz.SystemUserPermissions{ + { + MemberType: authz.MemberTypeSystem, + Permissions: []string{"iam.read", "iam.write", "iam.policy.read"}, + }, + { + MemberType: authz.MemberTypeIAM, + AggregateID: "instanceID", + Permissions: []string{"iam.read", "iam.write", "iam.policy.read", "org.read"}, + }, + { + MemberType: authz.MemberTypeOrganization, + AggregateID: "orgID", + Permissions: []string{"org.read", "org.write", "org.policy.read", "project.read", "project.write"}, + }, + { + MemberType: authz.MemberTypeProject, + AggregateID: "projectID", + Permissions: []string{"project.read", "project.write"}, + }, + { + MemberType: authz.MemberTypeProjectGrant, + AggregateID: "projectID", + ObjectID: "grantID", + Permissions: []string{"project_grant.read", "project_grant.write"}, + }, + }, + permm: "project_grant.read", + }, + want: `{ + "instance_permitted": false, + "org_ids": [], + "project_ids": [], + "project_grants": [ + { + "project_id": "projectID", + "grant_id": "grantID" + } + ] + }`, + }, + { + name: "instance without aggregate ID", + args: args{ + reqInstanceID: "instanceID", + permissions: []authz.SystemUserPermissions{ + { + MemberType: authz.MemberTypeIAM, + AggregateID: "", + Permissions: []string{"foo.bar", "bar.foo"}, + }, + }, + permm: "foo.bar", + }, + want: `{ + "instance_permitted": false, + "org_ids": [], + "project_ids": [], + "project_grants": [] + }`, + }, + { + name: "wrong instance ID", + args: args{ + reqInstanceID: "instanceID", + permissions: []authz.SystemUserPermissions{ + { + MemberType: authz.MemberTypeIAM, + AggregateID: "wrong", + Permissions: []string{"foo.bar", "bar.foo"}, + }, + }, + permm: "foo.bar", + }, + want: `{ + "instance_permitted": false, + "org_ids": [], + "project_ids": [], + "project_grants": [] + }`, + }, + { + name: "permission on other instance", + args: args{ + reqInstanceID: "instanceID", + permissions: []authz.SystemUserPermissions{ + { + MemberType: authz.MemberTypeIAM, + AggregateID: "instanceID", + Permissions: []string{"bar.foo"}, + }, + { + MemberType: authz.MemberTypeIAM, + AggregateID: "wrong", + Permissions: []string{"foo.bar"}, + }, + }, + permm: "foo.bar", + }, + want: `{ + "instance_permitted": false, + "org_ids": [], + "project_ids": [], + "project_grants": [] + }`, + }, + { + name: "org ID missing", + args: args{ + reqInstanceID: "instanceID", + permissions: []authz.SystemUserPermissions{ + { + MemberType: authz.MemberTypeOrganization, + AggregateID: "", + Permissions: []string{"foo.bar"}, + }, + }, + permm: "foo.bar", + }, + want: `{ + "instance_permitted": false, + "org_ids": [], + "project_ids": [], + "project_grants": [] + }`, + }, + { + name: "multiple org IDs", + args: args{ + reqInstanceID: "instanceID", + permissions: []authz.SystemUserPermissions{ + { + MemberType: authz.MemberTypeOrganization, + AggregateID: "Org1", + Permissions: []string{"foo.bar"}, + }, + { + MemberType: authz.MemberTypeOrganization, + AggregateID: "Org2", + Permissions: []string{"foo.bar"}, + }, + }, + permm: "foo.bar", + }, + want: `{ + "instance_permitted": false, + "org_ids": ["Org1", "Org2"], + "project_ids": [], + "project_grants": [] + }`, + }, + { + name: "project ID missing", + args: args{ + reqInstanceID: "instanceID", + permissions: []authz.SystemUserPermissions{ + { + MemberType: authz.MemberTypeProject, + AggregateID: "", + Permissions: []string{"foo.bar"}, + }, + }, + permm: "foo.bar", + }, + want: `{ + "instance_permitted": false, + "org_ids": [], + "project_ids": [], + "project_grants": [] + }`, + }, + { + name: "multiple project IDs", + args: args{ + reqInstanceID: "instanceID", + permissions: []authz.SystemUserPermissions{ + { + MemberType: authz.MemberTypeProject, + AggregateID: "P1", + Permissions: []string{"foo.bar"}, + }, + { + MemberType: authz.MemberTypeProject, + AggregateID: "P2", + Permissions: []string{"foo.bar"}, + }, + }, + permm: "foo.bar", + }, + want: `{ + "instance_permitted": false, + "org_ids": [], + "project_ids": ["P1", "P2"], + "project_grants": [] + }`, + }, + { + name: "project grant ID missing", + args: args{ + reqInstanceID: "instanceID", + permissions: []authz.SystemUserPermissions{ + { + MemberType: authz.MemberTypeProjectGrant, + AggregateID: "", + ObjectID: "", + Permissions: []string{"foo.bar"}, + }, + }, + permm: "foo.bar", + }, + want: `{ + "instance_permitted": false, + "org_ids": [], + "project_ids": [], + "project_grants": [] + }`, + }, + { + name: "multiple project IDs", + args: args{ + reqInstanceID: "instanceID", + permissions: []authz.SystemUserPermissions{ + { + MemberType: authz.MemberTypeProjectGrant, + AggregateID: "P1", + ObjectID: "O1", + Permissions: []string{"foo.bar"}, + }, + { + MemberType: authz.MemberTypeProjectGrant, + AggregateID: "P2", + ObjectID: "O2", + Permissions: []string{"foo.bar"}, + }, + }, + permm: "foo.bar", + }, + want: `{ + "instance_permitted": false, + "org_ids": [], + "project_ids": [], + "project_grants": [ + { + "project_id": "P1", + "grant_id": "O1" + }, + { + "project_id": "P2", + "grant_id": "O2" + } + ] + }`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + rows, err := dbPool.Query(CTX, query, database.NewJSONArray(tt.args.permissions), tt.args.reqInstanceID, tt.args.permm) + require.NoError(t, err) + got, err := pgx.CollectOneRow(rows, pgx.RowTo[string]) + require.NoError(t, err) + assert.JSONEq(t, tt.want, got) + }) + } +} + +const ( + instanceID = "instanceID" + orgID = "orgID" + projectID = "projectID" +) + +func TestPermittedOrgs(t *testing.T) { + t.Parallel() + + tx, err := dbPool.Begin(CTX) + require.NoError(t, err) + defer tx.Rollback(CTX) + + // Insert a couple of deterministic field rows to test the function. + // Data will not persist, because the transaction is rolled back. + createRolePermission(t, tx, "IAM_OWNER", []string{"org.write", "org.read"}) + createRolePermission(t, tx, "ORG_OWNER", []string{"org.write", "org.read"}) + createMember(t, tx, instance.AggregateType, "instance_user") + createMember(t, tx, org.AggregateType, "org_user") + + const query = "SELECT instance_permitted, org_ids FROM eventstore.permitted_orgs($1,$2,$3,$4,$5);" + type args struct { + reqInstanceID string + authUserID string + systemUserPerms []authz.SystemUserPermissions + perm string + filterOrg *string + } + type result struct { + InstancePermitted bool + OrgIDs pgtype.FlatArray[string] + } + tests := []struct { + name string + args args + want result + }{ + { + name: "system user, instance", + args: args{ + reqInstanceID: instanceID, + systemUserPerms: []authz.SystemUserPermissions{{ + MemberType: authz.MemberTypeSystem, + Permissions: []string{"org.write", "org.read"}, + }}, + perm: "org.read", + }, + want: result{ + InstancePermitted: true, + }, + }, + { + name: "system user, orgs", + args: args{ + reqInstanceID: instanceID, + systemUserPerms: []authz.SystemUserPermissions{{ + MemberType: authz.MemberTypeOrganization, + AggregateID: orgID, + Permissions: []string{"org.read", "org.write", "org.policy.read", "project.read", "project.write"}, + }}, + perm: "org.read", + }, + want: result{ + OrgIDs: pgtype.FlatArray[string]{orgID}, + }, + }, + { + name: "instance member", + args: args{ + reqInstanceID: instanceID, + authUserID: "instance_user", + perm: "org.read", + }, + want: result{ + InstancePermitted: true, + }, + }, + { + name: "org member", + args: args{ + reqInstanceID: instanceID, + authUserID: "org_user", + perm: "org.read", + }, + want: result{ + OrgIDs: pgtype.FlatArray[string]{orgID}, + }, + }, + { + name: "org member, filter", + args: args{ + reqInstanceID: instanceID, + authUserID: "org_user", + perm: "org.read", + filterOrg: gu.Ptr(orgID), + }, + want: result{ + OrgIDs: pgtype.FlatArray[string]{orgID}, + }, + }, + { + name: "org member, filter wrong org", + args: args{ + reqInstanceID: instanceID, + authUserID: "org_user", + perm: "org.read", + filterOrg: gu.Ptr("foobar"), + }, + want: result{}, + }, + { + name: "no permission", + args: args{ + reqInstanceID: instanceID, + authUserID: "foobar", + perm: "org.read", + filterOrg: gu.Ptr(orgID), + }, + want: result{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rows, err := tx.Query(CTX, query, tt.args.reqInstanceID, tt.args.authUserID, database.NewJSONArray(tt.args.systemUserPerms), tt.args.perm, tt.args.filterOrg) + require.NoError(t, err) + got, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByPos[result]) + require.NoError(t, err) + assert.Equal(t, tt.want.InstancePermitted, got.InstancePermitted) + assert.ElementsMatch(t, tt.want.OrgIDs, got.OrgIDs) + }) + } +} + +func TestPermittedProjects(t *testing.T) { + t.Parallel() + + tx, err := dbPool.Begin(CTX) + require.NoError(t, err) + defer tx.Rollback(CTX) + + // Insert a couple of deterministic field rows to test the function. + // Data will not persist, because the transaction is rolled back. + createRolePermission(t, tx, "IAM_OWNER", []string{"project.write", "project.read"}) + createRolePermission(t, tx, "ORG_OWNER", []string{"project.write", "project.read"}) + createRolePermission(t, tx, "PROJECT_OWNER", []string{"project.write", "project.read"}) + createMember(t, tx, instance.AggregateType, "instance_user") + createMember(t, tx, org.AggregateType, "org_user") + createMember(t, tx, project.AggregateType, "project_user") + + const query = "SELECT instance_permitted, org_ids, project_ids FROM eventstore.permitted_projects($1,$2,$3,$4,$5);" + type args struct { + reqInstanceID string + authUserID string + systemUserPerms []authz.SystemUserPermissions + perm string + filterOrg *string + } + type result struct { + InstancePermitted bool + OrgIDs pgtype.FlatArray[string] + ProjectIDs pgtype.FlatArray[string] + } + tests := []struct { + name string + args args + want result + }{ + { + name: "system user, instance", + args: args{ + reqInstanceID: instanceID, + systemUserPerms: []authz.SystemUserPermissions{{ + MemberType: authz.MemberTypeSystem, + Permissions: []string{"project.write", "project.read"}, + }}, + perm: "project.read", + }, + want: result{ + InstancePermitted: true, + }, + }, + { + name: "system user, orgs", + args: args{ + reqInstanceID: instanceID, + systemUserPerms: []authz.SystemUserPermissions{{ + MemberType: authz.MemberTypeOrganization, + AggregateID: orgID, + Permissions: []string{"project.read", "project.write"}, + }}, + perm: "project.read", + }, + want: result{ + OrgIDs: pgtype.FlatArray[string]{orgID}, + }, + }, + { + name: "system user, projects", + args: args{ + reqInstanceID: instanceID, + systemUserPerms: []authz.SystemUserPermissions{{ + MemberType: authz.MemberTypeProject, + AggregateID: projectID, + Permissions: []string{"project.read", "project.write"}, + }}, + perm: "project.read", + }, + want: result{ + ProjectIDs: pgtype.FlatArray[string]{projectID}, + }, + }, + { + name: "system user, org and project", + args: args{ + reqInstanceID: instanceID, + systemUserPerms: []authz.SystemUserPermissions{ + { + MemberType: authz.MemberTypeOrganization, + AggregateID: orgID, + Permissions: []string{"project.read", "project.write"}, + }, + { + MemberType: authz.MemberTypeProject, + AggregateID: projectID, + Permissions: []string{"project.read", "project.write"}, + }, + }, + perm: "project.read", + }, + want: result{ + OrgIDs: pgtype.FlatArray[string]{orgID}, + ProjectIDs: pgtype.FlatArray[string]{projectID}, + }, + }, + { + name: "instance member", + args: args{ + reqInstanceID: instanceID, + authUserID: "instance_user", + perm: "project.read", + }, + want: result{ + InstancePermitted: true, + }, + }, + { + name: "org member", + args: args{ + reqInstanceID: instanceID, + authUserID: "org_user", + perm: "project.read", + }, + want: result{ + InstancePermitted: false, + OrgIDs: pgtype.FlatArray[string]{orgID}, + }, + }, + { + name: "org member, filter", + args: args{ + reqInstanceID: instanceID, + authUserID: "org_user", + perm: "project.read", + filterOrg: gu.Ptr(orgID), + }, + want: result{ + InstancePermitted: false, + OrgIDs: pgtype.FlatArray[string]{orgID}, + }, + }, + { + name: "org member, filter wrong org", + args: args{ + reqInstanceID: instanceID, + authUserID: "org_user", + perm: "project.read", + filterOrg: gu.Ptr("foobar"), + }, + want: result{}, + }, + { + name: "project member", + args: args{ + reqInstanceID: instanceID, + authUserID: "project_user", + perm: "project.read", + }, + want: result{ + ProjectIDs: pgtype.FlatArray[string]{projectID}, + }, + }, + { + name: "no permission", + args: args{ + reqInstanceID: instanceID, + authUserID: "foobar", + perm: "project.read", + filterOrg: gu.Ptr(orgID), + }, + want: result{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rows, err := tx.Query(CTX, query, tt.args.reqInstanceID, tt.args.authUserID, database.NewJSONArray(tt.args.systemUserPerms), tt.args.perm, tt.args.filterOrg) + require.NoError(t, err) + got, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByPos[result]) + require.NoError(t, err) + assert.Equal(t, tt.want.InstancePermitted, got.InstancePermitted) + assert.ElementsMatch(t, tt.want.OrgIDs, got.OrgIDs) + }) + } +} + +func createRolePermission(t *testing.T, tx pgx.Tx, role string, permissions []string) { + for _, perm := range permissions { + createTestField(t, tx, instanceID, permission.AggregateType, instanceID, "role_permission", role, "permission", perm) + } +} + +func createMember(t *testing.T, tx pgx.Tx, aggregateType eventstore.AggregateType, userID string) { + var err error + switch aggregateType { + case instance.AggregateType: + createTestField(t, tx, instanceID, aggregateType, instanceID, "instance_member_role", userID, "instance_role", "IAM_OWNER") + case org.AggregateType: + createTestField(t, tx, orgID, aggregateType, orgID, "org_member_role", userID, "org_role", "ORG_OWNER") + case project.AggregateType: + createTestField(t, tx, orgID, aggregateType, orgID, "project_member_role", userID, "project_role", "PROJECT_OWNER") + default: + panic("unknown aggregate type " + aggregateType) + } + require.NoError(t, err) +} + +func createTestField(t *testing.T, tx pgx.Tx, resourceOwner string, aggregateType eventstore.AggregateType, aggregateID, objectType, objectID, fieldName string, value any) { + const query = `INSERT INTO eventstore.fields( + instance_id, resource_owner, aggregate_type, aggregate_id, object_type, object_id, field_name, value, value_must_be_unique, should_index, object_revision) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, false, true, 1);` + encValue, err := json.Marshal(value) + require.NoError(t, err) + _, err = tx.Exec(CTX, query, instanceID, resourceOwner, aggregateType, aggregateID, objectType, objectID, fieldName, encValue) + require.NoError(t, err) + +} diff --git a/cmd/setup/integration_test/setup_test.go b/cmd/setup/integration_test/setup_test.go new file mode 100644 index 0000000000..42b8502841 --- /dev/null +++ b/cmd/setup/integration_test/setup_test.go @@ -0,0 +1,41 @@ +// Package setup_test implements tests for procedural PostgreSQL functions, +// created in the database during Zitadel setup. +// Tests depend on `zitadel setup` being run first and therefore is run as integration tests. +// A PGX connection is used directly to the integration test database. +// This package assumes the database server available as per integration test defaults. +// See the [ConnString] constant. + +//go:build integration + +package setup_test + +import ( + "context" + "os" + "testing" + "time" + + "github.com/jackc/pgx/v5/pgxpool" +) + +const ConnString = "host=localhost port=5432 user=zitadel dbname=zitadel sslmode=disable" + +var ( + CTX context.Context + dbPool *pgxpool.Pool +) + +func TestMain(m *testing.M) { + var cancel context.CancelFunc + CTX, cancel = context.WithTimeout(context.Background(), time.Second*10) + + var err error + dbPool, err = pgxpool.New(context.Background(), ConnString) + if err != nil { + panic(err) + } + exit := m.Run() + cancel() + dbPool.Close() + os.Exit(exit) +} diff --git a/cmd/setup/setup.go b/cmd/setup/setup.go index 17ce553779..15236a73e9 100644 --- a/cmd/setup/setup.go +++ b/cmd/setup/setup.go @@ -215,7 +215,9 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string) steps.s54InstancePositionIndex = &InstancePositionIndex{dbClient: dbClient} steps.s55ExecutionHandlerStart = &ExecutionHandlerStart{dbClient: dbClient} steps.s56IDPTemplate6SAMLFederatedLogout = &IDPTemplate6SAMLFederatedLogout{dbClient: dbClient} + steps.s57CreateResourceCounts = &CreateResourceCounts{dbClient: dbClient} steps.s58ReplaceLoginNames3View = &ReplaceLoginNames3View{dbClient: dbClient} + steps.s60GenerateSystemID = &GenerateSystemID{eventstore: eventstoreClient} err = projection.Create(ctx, dbClient, eventstoreClient, config.Projections, nil, nil, nil) logging.OnError(err).Fatal("unable to start projections") @@ -261,7 +263,9 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string) steps.s54InstancePositionIndex, steps.s55ExecutionHandlerStart, steps.s56IDPTemplate6SAMLFederatedLogout, + steps.s57CreateResourceCounts, steps.s58ReplaceLoginNames3View, + steps.s60GenerateSystemID, } { setupErr = executeMigration(ctx, eventstoreClient, step, "migration failed") if setupErr != nil { @@ -270,6 +274,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string) } commands, _, _, _ := startCommandsQueries(ctx, eventstoreClient, eventstoreV4, dbClient, masterKey, config) + steps.s59SetupWebkeys = &SetupWebkeys{eventstore: eventstoreClient, commands: commands} repeatableSteps := []migration.RepeatableMigration{ &externalConfigChange{ @@ -298,6 +303,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string) client: dbClient, }, } + repeatableSteps = append(repeatableSteps, triggerSteps(dbClient)...) for _, repeatableStep := range repeatableSteps { setupErr = executeMigration(ctx, eventstoreClient, repeatableStep, "unable to migrate repeatable step") @@ -318,6 +324,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string) steps.s42Apps7OIDCConfigsLoginVersion, steps.s43CreateFieldsDomainIndex, steps.s48Apps7SAMLConfigsLoginVersion, + steps.s59SetupWebkeys, // this step needs commands. } { setupErr = executeMigration(ctx, eventstoreClient, step, "migration failed") if setupErr != nil { diff --git a/cmd/setup/steps.yaml b/cmd/setup/steps.yaml index d2a7cc68dd..709becf2c3 100644 --- a/cmd/setup/steps.yaml +++ b/cmd/setup/steps.yaml @@ -6,6 +6,7 @@ FirstInstance: MachineKeyPath: # ZITADEL_FIRSTINSTANCE_MACHINEKEYPATH # The personal access token from the section FirstInstance.Org.Machine.Pat is written to the PatPath. PatPath: # ZITADEL_FIRSTINSTANCE_PATPATH + LoginClientPatPath: # ZITADEL_FIRSTINSTANCE_LOGINCLIENTPATPATH InstanceName: ZITADEL # ZITADEL_FIRSTINSTANCE_INSTANCENAME DefaultLanguage: en # ZITADEL_FIRSTINSTANCE_DEFAULTLANGUAGE Org: @@ -46,6 +47,13 @@ FirstInstance: Pat: # date format: 2023-01-01T00:00:00Z ExpirationDate: # ZITADEL_FIRSTINSTANCE_ORG_MACHINE_PAT_EXPIRATIONDATE + LoginClient: + Machine: + Username: # ZITADEL_FIRSTINSTANCE_ORG_LOGINCLIENT_MACHINE_USERNAME + Name: # ZITADEL_FIRSTINSTANCE_ORG_LOGINCLIENT_MACHINE_NAME + Pat: + # date format: 2023-01-01T00:00:00Z + ExpirationDate: # ZITADEL_FIRSTINSTANCE_ORG_LOGINCLIENT_PAT_EXPIRATIONDATE CorrectCreationDate: FailAfter: 5m # ZITADEL_CORRECTCREATIONDATE_FAILAFTER diff --git a/cmd/setup/trigger_steps.go b/cmd/setup/trigger_steps.go new file mode 100644 index 0000000000..163a8fdb59 --- /dev/null +++ b/cmd/setup/trigger_steps.go @@ -0,0 +1,125 @@ +package setup + +import ( + "fmt" + + "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/migration" + "github.com/zitadel/zitadel/internal/query/projection" +) + +// triggerSteps defines the repeatable migrations that set up triggers +// for counting resources in the database. +func triggerSteps(db *database.DB) []migration.RepeatableMigration { + return []migration.RepeatableMigration{ + // Delete parent count triggers for instances and organizations + migration.DeleteParentCountsTrigger(db, + projection.InstanceProjectionTable, + domain.CountParentTypeInstance, + projection.InstanceColumnID, + projection.InstanceColumnID, + "instance", + ), + migration.DeleteParentCountsTrigger(db, + projection.OrgProjectionTable, + domain.CountParentTypeOrganization, + projection.OrgColumnInstanceID, + projection.OrgColumnID, + "organization", + ), + + // Count triggers for all the resources + migration.CountTrigger(db, + projection.OrgProjectionTable, + domain.CountParentTypeInstance, + projection.OrgColumnInstanceID, + projection.OrgColumnInstanceID, + "organization", + ), + migration.CountTrigger(db, + projection.ProjectProjectionTable, + domain.CountParentTypeOrganization, + projection.ProjectColumnInstanceID, + projection.ProjectColumnResourceOwner, + "project", + ), + migration.CountTrigger(db, + projection.UserTable, + domain.CountParentTypeOrganization, + projection.UserInstanceIDCol, + projection.UserResourceOwnerCol, + "user", + ), + migration.CountTrigger(db, + projection.InstanceMemberProjectionTable, + domain.CountParentTypeInstance, + projection.MemberInstanceID, + projection.MemberResourceOwner, + "iam_admin", + ), + migration.CountTrigger(db, + projection.IDPTable, + domain.CountParentTypeInstance, + projection.IDPInstanceIDCol, + projection.IDPInstanceIDCol, + "identity_provider", + ), + migration.CountTrigger(db, + projection.IDPTemplateLDAPTable, + domain.CountParentTypeInstance, + projection.LDAPInstanceIDCol, + projection.LDAPInstanceIDCol, + "identity_provider_ldap", + ), + migration.CountTrigger(db, + projection.ActionTable, + domain.CountParentTypeInstance, + projection.ActionInstanceIDCol, + projection.ActionInstanceIDCol, + "action_v1", + ), + migration.CountTrigger(db, + projection.ExecutionTable, + domain.CountParentTypeInstance, + projection.ExecutionInstanceIDCol, + projection.ExecutionInstanceIDCol, + "execution", + ), + migration.CountTrigger(db, + fmt.Sprintf("%s_%s", projection.ExecutionTable, projection.ExecutionTargetSuffix), + domain.CountParentTypeInstance, + projection.ExecutionTargetInstanceIDCol, + projection.ExecutionTargetInstanceIDCol, + "execution_target", + ), + migration.CountTrigger(db, + projection.LoginPolicyTable, + domain.CountParentTypeInstance, + projection.LoginPolicyInstanceIDCol, + projection.LoginPolicyInstanceIDCol, + "login_policy", + ), + migration.CountTrigger(db, + projection.PasswordComplexityTable, + domain.CountParentTypeInstance, + projection.ComplexityPolicyInstanceIDCol, + projection.ComplexityPolicyInstanceIDCol, + "password_complexity_policy", + ), + migration.CountTrigger(db, + projection.PasswordAgeTable, + domain.CountParentTypeInstance, + projection.AgePolicyInstanceIDCol, + projection.AgePolicyInstanceIDCol, + "password_expiry_policy", + ), + migration.CountTrigger(db, + projection.LockoutPolicyTable, + domain.CountParentTypeInstance, + projection.LockoutPolicyInstanceIDCol, + projection.LockoutPolicyInstanceIDCol, + "lockout_policy", + ), + } +} diff --git a/cmd/start/config.go b/cmd/start/config.go index 78b6f0afe0..c680bf7c05 100644 --- a/cmd/start/config.go +++ b/cmd/start/config.go @@ -32,6 +32,7 @@ import ( "github.com/zitadel/zitadel/internal/logstore" "github.com/zitadel/zitadel/internal/notification/handlers" "github.com/zitadel/zitadel/internal/query/projection" + "github.com/zitadel/zitadel/internal/serviceping" static_config "github.com/zitadel/zitadel/internal/static/config" metrics "github.com/zitadel/zitadel/internal/telemetry/metrics/config" profiler "github.com/zitadel/zitadel/internal/telemetry/profiler/config" @@ -81,6 +82,7 @@ type Config struct { LogStore *logstore.Configs Quotas *QuotasConfig Telemetry *handlers.TelemetryPusherConfig + ServicePing *serviceping.Config } type QuotasConfig struct { diff --git a/cmd/start/config_test.go b/cmd/start/config_test.go index 53c95d35ab..3c8328e557 100644 --- a/cmd/start/config_test.go +++ b/cmd/start/config_test.go @@ -73,8 +73,6 @@ Log: DefaultInstance: Features: LoginDefaultOrg: true - LegacyIntrospection: true - TriggerIntrospectionProjections: true UserSchema: true Log: Level: info @@ -84,10 +82,8 @@ Actions: `}, want: func(t *testing.T, config *Config) { assert.Equal(t, config.DefaultInstance.Features, &command.InstanceFeatures{ - LoginDefaultOrg: gu.Ptr(true), - LegacyIntrospection: gu.Ptr(true), - TriggerIntrospectionProjections: gu.Ptr(true), - UserSchema: gu.Ptr(true), + LoginDefaultOrg: gu.Ptr(true), + UserSchema: gu.Ptr(true), }) }, }, { diff --git a/cmd/start/start.go b/cmd/start/start.go index 8a16a5e3be..e68b68f0bb 100644 --- a/cmd/start/start.go +++ b/cmd/start/start.go @@ -34,17 +34,23 @@ import ( "github.com/zitadel/zitadel/internal/api" "github.com/zitadel/zitadel/internal/api/assets" internal_authz "github.com/zitadel/zitadel/internal/api/authz" + action_v2 "github.com/zitadel/zitadel/internal/api/grpc/action/v2" action_v2_beta "github.com/zitadel/zitadel/internal/api/grpc/action/v2beta" "github.com/zitadel/zitadel/internal/api/grpc/admin" + app "github.com/zitadel/zitadel/internal/api/grpc/app/v2beta" "github.com/zitadel/zitadel/internal/api/grpc/auth" + authorization_v2beta "github.com/zitadel/zitadel/internal/api/grpc/authorization/v2beta" feature_v2 "github.com/zitadel/zitadel/internal/api/grpc/feature/v2" feature_v2beta "github.com/zitadel/zitadel/internal/api/grpc/feature/v2beta" idp_v2 "github.com/zitadel/zitadel/internal/api/grpc/idp/v2" + instance "github.com/zitadel/zitadel/internal/api/grpc/instance/v2beta" + internal_permission_v2beta "github.com/zitadel/zitadel/internal/api/grpc/internal_permission/v2beta" "github.com/zitadel/zitadel/internal/api/grpc/management" oidc_v2 "github.com/zitadel/zitadel/internal/api/grpc/oidc/v2" oidc_v2beta "github.com/zitadel/zitadel/internal/api/grpc/oidc/v2beta" org_v2 "github.com/zitadel/zitadel/internal/api/grpc/org/v2" org_v2beta "github.com/zitadel/zitadel/internal/api/grpc/org/v2beta" + project_v2beta "github.com/zitadel/zitadel/internal/api/grpc/project/v2beta" "github.com/zitadel/zitadel/internal/api/grpc/resources/debug_events/debug_events" user_v3_alpha "github.com/zitadel/zitadel/internal/api/grpc/resources/user/v3alpha" userschema_v3_alpha "github.com/zitadel/zitadel/internal/api/grpc/resources/userschema/v3alpha" @@ -56,7 +62,8 @@ import ( "github.com/zitadel/zitadel/internal/api/grpc/system" user_v2 "github.com/zitadel/zitadel/internal/api/grpc/user/v2" user_v2beta "github.com/zitadel/zitadel/internal/api/grpc/user/v2beta" - webkey "github.com/zitadel/zitadel/internal/api/grpc/webkey/v2beta" + webkey_v2 "github.com/zitadel/zitadel/internal/api/grpc/webkey/v2" + webkey_v2beta "github.com/zitadel/zitadel/internal/api/grpc/webkey/v2beta" http_util "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/api/http/middleware" "github.com/zitadel/zitadel/internal/api/idp" @@ -96,6 +103,7 @@ import ( "github.com/zitadel/zitadel/internal/notification" "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/queue" + "github.com/zitadel/zitadel/internal/serviceping" "github.com/zitadel/zitadel/internal/static" es_v4 "github.com/zitadel/zitadel/internal/v2/eventstore" es_v4_pg "github.com/zitadel/zitadel/internal/v2/eventstore/postgres" @@ -314,10 +322,20 @@ func startZitadel(ctx context.Context, config *Config, masterKey string, server ) execution.Start(ctx) + // the service ping and it's workers need to be registered before starting the queue + if err := serviceping.Register(ctx, q, queries, eventstoreClient, config.ServicePing); err != nil { + return err + } + if err = q.Start(ctx); err != nil { return err } + // the scheduler / periodic jobs need to be started after the queue already runs + if err = serviceping.Start(config.ServicePing, q); err != nil { + return err + } + router := mux.NewRouter() tlsConfig, err := config.TLS.Config() if err != nil { @@ -444,6 +462,9 @@ func startAPIs( if err := apis.RegisterServer(ctx, system.CreateServer(commands, queries, config.Database.DatabaseName(), config.DefaultInstance, config.ExternalDomain), tlsConfig); err != nil { return nil, err } + if err := apis.RegisterService(ctx, instance.CreateServer(commands, queries, config.Database.DatabaseName(), config.DefaultInstance, config.ExternalDomain)); err != nil { + return nil, err + } if err := apis.RegisterServer(ctx, admin.CreateServer(config.Database.DatabaseName(), commands, queries, keys.User, config.AuditLogRetention), tlsConfig); err != nil { return nil, err } @@ -456,7 +477,7 @@ func startAPIs( if err := apis.RegisterService(ctx, user_v2beta.CreateServer(commands, queries, keys.User, keys.IDPConfig, idp.CallbackURL(), idp.SAMLRootURL(), assets.AssetAPI(), permissionCheck)); err != nil { return nil, err } - if err := apis.RegisterService(ctx, user_v2.CreateServer(commands, queries, keys.User, keys.IDPConfig, idp.CallbackURL(), idp.SAMLRootURL(), assets.AssetAPI(), permissionCheck)); err != nil { + if err := apis.RegisterService(ctx, user_v2.CreateServer(commands, queries, config.SystemDefaults, keys.User, keys.IDPConfig, idp.CallbackURL(), idp.SAMLRootURL(), assets.AssetAPI(), permissionCheck)); err != nil { return nil, err } if err := apis.RegisterService(ctx, session_v2beta.CreateServer(commands, queries, permissionCheck)); err != nil { @@ -465,7 +486,7 @@ func startAPIs( if err := apis.RegisterService(ctx, settings_v2beta.CreateServer(commands, queries)); err != nil { return nil, err } - if err := apis.RegisterService(ctx, org_v2beta.CreateServer(commands, queries, permissionCheck)); err != nil { + if err := apis.RegisterService(ctx, org_v2beta.CreateServer(config.SystemDefaults, commands, queries, permissionCheck)); err != nil { return nil, err } if err := apis.RegisterService(ctx, feature_v2beta.CreateServer(commands, queries)); err != nil { @@ -489,18 +510,37 @@ func startAPIs( if err := apis.RegisterService(ctx, action_v2_beta.CreateServer(config.SystemDefaults, commands, queries, domain.AllActionFunctions, apis.ListGrpcMethods, apis.ListGrpcServices)); err != nil { return nil, err } + if err := apis.RegisterService(ctx, action_v2.CreateServer(config.SystemDefaults, commands, queries, domain.AllActionFunctions, apis.ListGrpcMethods, apis.ListGrpcServices)); err != nil { + return nil, err + } + if err := apis.RegisterService(ctx, project_v2beta.CreateServer(config.SystemDefaults, commands, queries, permissionCheck)); err != nil { + return nil, err + } + if err := apis.RegisterService(ctx, internal_permission_v2beta.CreateServer(config.SystemDefaults, commands, queries, permissionCheck)); err != nil { + return nil, err + } if err := apis.RegisterService(ctx, userschema_v3_alpha.CreateServer(config.SystemDefaults, commands, queries)); err != nil { return nil, err } if err := apis.RegisterService(ctx, user_v3_alpha.CreateServer(commands)); err != nil { return nil, err } - if err := apis.RegisterService(ctx, webkey.CreateServer(commands, queries)); err != nil { + if err := apis.RegisterService(ctx, webkey_v2beta.CreateServer(commands, queries)); err != nil { + return nil, err + } + if err := apis.RegisterService(ctx, webkey_v2.CreateServer(commands, queries)); err != nil { return nil, err } if err := apis.RegisterService(ctx, debug_events.CreateServer(commands, queries)); err != nil { return nil, err } + if err := apis.RegisterService(ctx, authorization_v2beta.CreateServer(config.SystemDefaults, commands, queries, permissionCheck)); err != nil { + return nil, err + } + if err := apis.RegisterService(ctx, app.CreateServer(commands, queries, permissionCheck)); err != nil { + return nil, err + } + instanceInterceptor := middleware.InstanceInterceptor(queries, config.ExternalDomain, login.IgnoreInstanceEndpoints...) assetsCache := middleware.AssetsCacheInterceptor(config.AssetStorage.Cache.MaxAge, config.AssetStorage.Cache.SharedMaxAge) apis.RegisterHandlerOnPrefix(assets.HandlerPrefix, assets.NewHandler(commands, verifier, config.SystemAuthZ, config.InternalAuthZ, id.SonyFlakeGenerator(), store, queries, middleware.CallDurationHandler, instanceInterceptor.Handler, assetsCache.Handler, limitingAccessInterceptor.Handle)) @@ -542,7 +582,6 @@ func startAPIs( keys.OIDC, keys.OIDCKey, eventstore, - dbClient, userAgentInterceptor, instanceInterceptor.Handler, limitingAccessInterceptor, diff --git a/console/.gitignore b/console/.gitignore index 6616a44530..5edbdebe90 100644 --- a/console/.gitignore +++ b/console/.gitignore @@ -36,7 +36,7 @@ speed-measure-plugin*.json /coverage /libpeerconnection.log npm-debug.log -yarn-error.log +pnpm-debug.log testem.log /typings diff --git a/console/README.md b/console/README.md index 7147c0487b..2d958aa48c 100644 --- a/console/README.md +++ b/console/README.md @@ -1,27 +1,137 @@ -# Console +# Console Angular App -This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 8.3.20. +This is the ZITADEL Console Angular application. -## Development server +## Development -Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. +### Prerequisites -## Code scaffolding +- Node.js 18 or later +- pnpm (latest) -Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. +### Installation -## Build +```bash +pnpm install +``` -Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build. +### Proto Generation -## Running unit tests +The Console app uses **dual proto generation** with Turbo dependency management: -Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). +1. **`@zitadel/proto` generation**: Modern ES modules with `@bufbuild/protobuf` for v2 APIs +2. **Local `buf.gen.yaml` generation**: Traditional protobuf JavaScript classes for v1 APIs -## Running end-to-end tests +The Console app's `turbo.json` ensures that `@zitadel/proto#generate` runs before the Console's own generation, providing both: -Please refer to the [contributing guide](../CONTRIBUTING.md#console) +- Modern schemas from `@zitadel/proto` (e.g., `UserSchema`, `DetailsSchema`) +- Legacy classes from `src/app/proto/generated` (e.g., `User`, `Project`) -## Further help +Generated files: -To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). +- **`@zitadel/proto`**: Modern ES modules in `login/packages/zitadel-proto/` +- **Local generation**: Traditional protobuf files in `src/app/proto/generated/` + - TypeScript definition files (`.d.ts`) + - JavaScript files (`.js`) + - gRPC client files (`*ServiceClientPb.ts`) + - OpenAPI/Swagger JSON files (`.swagger.json`) + +To generate proto files: + +```bash +pnpm run generate +``` + +This automatically runs both generations in the correct order via Turbo dependencies. + +### Development Server + +To start the development server: + +```bash +pnpm start +``` + +This will: + +1. Fetch the environment configuration from the server +2. Serve the app on the default port + +### Building + +To build for production: + +```bash +pnpm run build +``` + +This will: + +1. Generate proto files (via `prebuild` script) +2. Build the Angular app with production optimizations + +### Linting + +To run linting and formatting checks: + +```bash +pnpm run lint +``` + +To auto-fix formatting issues: + +```bash +pnpm run lint:fix +``` + +## Project Structure + +- `src/app/proto/generated/` - Generated proto files (Angular-specific format) +- `buf.gen.yaml` - Local proto generation configuration +- `turbo.json` - Turbo dependency configuration for proto generation +- `prebuild.development.js` - Development environment configuration script + +## Proto Generation Details + +The Console app uses **dual proto generation** managed by Turbo dependencies: + +### Dependency Chain + +The Console app has the following build dependencies managed by Turbo: + +1. `@zitadel/proto#generate` - Generates modern protobuf files +2. `@zitadel/client#build` - Builds the TypeScript gRPC client library +3. `console#generate` - Generates Console-specific protobuf files +4. `console#build` - Builds the Angular application + +This ensures that the Console always has access to the latest client library and protobuf definitions. + +### Legacy v1 API (Traditional Protobuf) + +- Uses local `buf.gen.yaml` configuration +- Generates traditional Google protobuf JavaScript classes extending `jspb.Message` +- Uses plugins: `protocolbuffers/js`, `grpc/web`, `grpc-ecosystem/openapiv2` +- Output: `src/app/proto/generated/` +- Used for: Most existing Console functionality + +### Modern v2 API (ES Modules) + +- Uses `@zitadel/proto` package generation +- Generates modern ES modules with `@bufbuild/protobuf` +- Uses plugin: `@bufbuild/es` with ES modules and JSON types +- Output: `login/packages/zitadel-proto/` +- Used for: New user v2 API and services + +### Dependency Management + +The Console's `turbo.json` ensures proper execution order: + +1. `@zitadel/proto#generate` runs first (modern ES modules) +2. Console's local generation runs second (traditional protobuf) +3. Build/lint/start tasks depend on both generations being complete + +This approach allows the Console app to use both v1 and v2 APIs while maintaining proper build dependencies. + +## Legacy Information + +This project was originally generated with Angular CLI version 8.3.20 and has been updated over time. diff --git a/console/package.json b/console/package.json index 0095360017..1eafec0502 100644 --- a/console/package.json +++ b/console/package.json @@ -3,12 +3,15 @@ "version": "0.0.0", "scripts": { "ng": "ng", + "dev": "node prebuild.development.js && ng serve", "start": "node prebuild.development.js && ng serve", "build": "ng build --configuration production --base-href=/ui/console/", - "prelint": "npm run generate", - "lint": "ng lint && prettier --check src", + "lint": "pnpm run '/^lint:check:.*$/'", + "lint:check:ng": "ng lint", + "lint:check:prettier": "prettier --check src", "lint:fix": "prettier --write src", - "generate": "buf generate ../proto --include-imports --include-wkt" + "generate": "pnpm exec buf generate ../proto --include-imports --include-wkt", + "clean": "rm -rf dist .angular .turbo node_modules src/app/proto/generated" }, "private": true, "dependencies": { @@ -24,6 +27,7 @@ "@angular/platform-browser-dynamic": "^16.2.12", "@angular/router": "^16.2.12", "@angular/service-worker": "^16.2.12", + "@bufbuild/protobuf": "^2.6.1", "@connectrpc/connect": "^2.0.0", "@connectrpc/connect-web": "^2.0.0", "@ctrl/ngx-codemirror": "^6.1.0", @@ -31,8 +35,8 @@ "@fortawesome/fontawesome-svg-core": "^6.7.2", "@fortawesome/free-brands-svg-icons": "^6.7.2", "@ngx-translate/core": "^15.0.0", - "@zitadel/client": "1.2.0", - "@zitadel/proto": "1.2.0", + "@zitadel/client": "workspace:*", + "@zitadel/proto": "workspace:*", "angular-oauth2-oidc": "^15.0.1", "angularx-qrcode": "^16.0.2", "buffer": "^6.0.3", @@ -64,7 +68,7 @@ "@angular/cli": "^16.2.15", "@angular/compiler-cli": "^16.2.5", "@angular/language-service": "^18.2.4", - "@bufbuild/buf": "^1.41.0", + "@bufbuild/buf": "^1.55.1", "@netlify/framework-info": "^9.8.13", "@types/file-saver": "^2.0.7", "@types/google-protobuf": "^3.15.3", diff --git a/console/src/app/app.module.ts b/console/src/app/app.module.ts index d6e7e60bea..d344ae288f 100644 --- a/console/src/app/app.module.ts +++ b/console/src/app/app.module.ts @@ -19,6 +19,7 @@ import localeSv from '@angular/common/locales/sv'; import localeHu from '@angular/common/locales/hu'; import localeKo from '@angular/common/locales/ko'; import localeRo from '@angular/common/locales/ro'; +import localeTr from '@angular/common/locales/tr'; import { APP_INITIALIZER, NgModule } from '@angular/core'; import { MatNativeDateModule } from '@angular/material/core'; import { MatDialogModule } from '@angular/material/dialog'; @@ -112,6 +113,8 @@ registerLocaleData(localeKo); i18nIsoCountries.registerLocale(require('i18n-iso-countries/langs/ko.json')); registerLocaleData(localeRo); i18nIsoCountries.registerLocale(require('i18n-iso-countries/langs/ro.json')); +registerLocaleData(localeTr); +i18nIsoCountries.registerLocale(require('i18n-iso-countries/langs/tr.json')); export class WebpackTranslateLoader implements TranslateLoader { getTranslation(lang: string): Observable { diff --git a/console/src/app/components/features/features.component.ts b/console/src/app/components/features/features.component.ts index d95bbdde43..72e1234653 100644 --- a/console/src/app/components/features/features.component.ts +++ b/console/src/app/components/features/features.component.ts @@ -33,13 +33,10 @@ const FEATURE_KEYS = [ 'enableBackChannelLogout', // 'improvedPerformance', 'loginDefaultOrg', - 'oidcLegacyIntrospection', 'oidcSingleV1SessionTermination', 'oidcTokenExchange', - 'oidcTriggerIntrospectionProjections', 'permissionCheckV2', 'userSchema', - 'webKey', ] as const; export type ToggleState = { source: Source; enabled: boolean }; @@ -142,7 +139,7 @@ export class FeaturesComponent { }, {}); // to save special flags they have to be handled here - req.loginV2 = { + req['loginV2'] = { required: toggleStates.loginV2.enabled, baseUri: toggleStates.loginV2.baseUri, }; diff --git a/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions.component.ts b/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions.component.ts index 7e0d457dd5..6fa6c2f307 100644 --- a/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions.component.ts +++ b/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions.component.ts @@ -45,7 +45,7 @@ export class ActionsTwoActionsComponent { switchMap(() => { return this.actionService.listExecutions({ sortingColumn: ExecutionFieldName.ID, pagination: { asc: true } }); }), - map(({ result }) => result.map(correctlyTypeExecution)), + map(({ executions }) => executions.map(correctlyTypeExecution)), catchError((err) => { this.toast.showError(err); return of([]); @@ -59,7 +59,7 @@ export class ActionsTwoActionsComponent { switchMap(() => { return this.actionService.listTargets({}); }), - map(({ result }) => result), + map(({ targets }) => targets), catchError((err) => { this.toast.showError(err); return of([]); diff --git a/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-target/actions-two-add-action-target.component.ts b/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-target/actions-two-add-action-target.component.ts index 60b4025650..e791227c99 100644 --- a/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-target/actions-two-add-action-target.component.ts +++ b/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-target/actions-two-add-action-target.component.ts @@ -115,13 +115,13 @@ export class ActionsTwoAddActionTargetComponent { this.actionService .listTargets({}) - .then(({ result }) => { - const targets = result.reduce((acc, target) => { + .then(({ targets }) => { + const result = targets.reduce((acc, target) => { acc.set(target.id, target); return acc; }, new Map()); - targetsSignal.set({ state: 'loaded', targets }); + targetsSignal.set({ state: 'loaded', targets: result }); }) .catch((error) => { this.toast.showError(error); diff --git a/console/src/app/modules/actions-two/actions-two-add-target/actions-two-add-target-dialog.component.ts b/console/src/app/modules/actions-two/actions-two-add-target/actions-two-add-target-dialog.component.ts index 7d3ad0e86c..642bc9c134 100644 --- a/console/src/app/modules/actions-two/actions-two-add-target/actions-two-add-target-dialog.component.ts +++ b/console/src/app/modules/actions-two/actions-two-add-target/actions-two-add-target-dialog.component.ts @@ -89,7 +89,7 @@ export class ActionTwoAddTargetDialogComponent { nanos: 0, }; - const targetType: Extract['targetType'], { case: TargetTypes }> = + const targetType: MessageInitShape['targetType'] = type === 'restWebhook' ? { case: type, value: { interruptOnError } } : type === 'restCall' diff --git a/console/src/app/modules/actions-two/actions-two-targets/actions-two-targets.component.ts b/console/src/app/modules/actions-two/actions-two-targets/actions-two-targets.component.ts index f678b86482..074d6a910d 100644 --- a/console/src/app/modules/actions-two/actions-two-targets/actions-two-targets.component.ts +++ b/console/src/app/modules/actions-two/actions-two-targets/actions-two-targets.component.ts @@ -39,7 +39,7 @@ export class ActionsTwoTargetsComponent { switchMap(() => { return this.actionService.listTargets({}); }), - map(({ result }) => result), + map(({ targets }) => targets), catchError((err) => { this.toast.showError(err); return of([]); diff --git a/console/src/app/modules/metadata/metadata-dialog/metadata-dialog.component.ts b/console/src/app/modules/metadata/metadata-dialog/metadata-dialog.component.ts index 8deff09eee..c75e15bf04 100644 --- a/console/src/app/modules/metadata/metadata-dialog/metadata-dialog.component.ts +++ b/console/src/app/modules/metadata/metadata-dialog/metadata-dialog.component.ts @@ -4,7 +4,6 @@ import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb'; import { ToastService } from 'src/app/services/toast.service'; import { Metadata as MetadataV2 } from '@zitadel/proto/zitadel/metadata_pb'; import { Metadata } from 'src/app/proto/generated/zitadel/metadata_pb'; -import { Buffer } from 'buffer'; export type MetadataDialogData = { metadata: (Metadata.AsObject | MetadataV2)[]; @@ -26,9 +25,10 @@ export class MetadataDialogComponent { public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: MetadataDialogData, ) { + const decoder = new TextDecoder(); this.metadata = data.metadata.map(({ key, value }) => ({ key, - value: typeof value === 'string' ? value : Buffer.from(value as unknown as string, 'base64').toString('utf8'), + value: typeof value === 'string' ? value : decoder.decode(value), })); } diff --git a/console/src/app/modules/metadata/metadata/metadata.component.ts b/console/src/app/modules/metadata/metadata/metadata.component.ts index 7f72297c00..bdb2c7734c 100644 --- a/console/src/app/modules/metadata/metadata/metadata.component.ts +++ b/console/src/app/modules/metadata/metadata/metadata.component.ts @@ -5,7 +5,6 @@ import { Observable, ReplaySubject } from 'rxjs'; import { Metadata as MetadataV2 } from '@zitadel/proto/zitadel/metadata_pb'; import { map, startWith } from 'rxjs/operators'; import { Metadata } from 'src/app/proto/generated/zitadel/metadata_pb'; -import { Buffer } from 'buffer'; type StringMetadata = { key: string; @@ -37,12 +36,13 @@ export class MetadataComponent implements OnInit { ngOnInit() { this.dataSource$ = this.metadata$.pipe( - map((metadata) => - metadata.map(({ key, value }) => ({ + map((metadata) => { + const decoder = new TextDecoder(); + return metadata.map(({ key, value }) => ({ key, - value: Buffer.from(value as any as string, 'base64').toString('utf-8'), - })), - ), + value: typeof value === 'string' ? value : decoder.decode(value), + })); + }), startWith([] as StringMetadata[]), map((metadata) => new MatTableDataSource(metadata)), ); diff --git a/console/src/app/modules/policies/oidc-webkeys/oidc-webkeys.component.ts b/console/src/app/modules/policies/oidc-webkeys/oidc-webkeys.component.ts index 65197b4ad4..1b2e8d743d 100644 --- a/console/src/app/modules/policies/oidc-webkeys/oidc-webkeys.component.ts +++ b/console/src/app/modules/policies/oidc-webkeys/oidc-webkeys.component.ts @@ -12,8 +12,6 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { State, WebKey } from '@zitadel/proto/zitadel/webkey/v2beta/key_pb'; import { CreateWebKeyRequestSchema } from '@zitadel/proto/zitadel/webkey/v2beta/webkey_service_pb'; import { RSAHasher, RSABits, ECDSACurve } from '@zitadel/proto/zitadel/webkey/v2beta/key_pb'; -import { NewFeatureService } from 'src/app/services/new-feature.service'; -import { ActivatedRoute, Router } from '@angular/router'; const CACHE_WARNING_MS = 5 * 60 * 1000; // 5 minutes @@ -22,9 +20,8 @@ const CACHE_WARNING_MS = 5 * 60 * 1000; // 5 minutes templateUrl: './oidc-webkeys.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class OidcWebKeysComponent implements OnInit { +export class OidcWebKeysComponent { protected readonly refresh = new Subject(); - protected readonly webKeysEnabled$: Observable; protected readonly webKeys$: Observable; protected readonly inactiveWebKeys$: Observable; protected readonly nextWebKeyCandidate$: Observable; @@ -34,17 +31,12 @@ export class OidcWebKeysComponent implements OnInit { constructor( private readonly webKeysService: WebKeysService, - private readonly featureService: NewFeatureService, private readonly toast: ToastService, private readonly timestampToDatePipe: TimestampToDatePipe, private readonly dialog: MatDialog, private readonly destroyRef: DestroyRef, - private readonly router: Router, - private readonly route: ActivatedRoute, ) { - this.webKeysEnabled$ = this.getWebKeysEnabled().pipe(shareReplay({ refCount: true, bufferSize: 1 })); - - const webKeys$ = this.getWebKeys(this.webKeysEnabled$).pipe(shareReplay({ refCount: true, bufferSize: 1 })); + const webKeys$ = this.getWebKeys().pipe(shareReplay({ refCount: true, bufferSize: 1 })); this.webKeys$ = webKeys$.pipe(map((webKeys) => webKeys.filter((webKey) => webKey.state !== State.INACTIVE))); this.inactiveWebKeys$ = webKeys$.pipe(map((webKeys) => webKeys.filter((webKey) => webKey.state === State.INACTIVE))); @@ -52,34 +44,7 @@ export class OidcWebKeysComponent implements OnInit { this.nextWebKeyCandidate$ = this.getNextWebKeyCandidate(this.webKeys$); } - ngOnInit(): void { - // redirect away from this page if web keys are not enabled - // this also preloads the web keys enabled state - this.webKeysEnabled$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(async (webKeysEnabled) => { - if (webKeysEnabled) { - return; - } - await this.router.navigate([], { - relativeTo: this.route, - queryParamsHandling: 'merge', - queryParams: { - id: null, - }, - }); - }); - } - - private getWebKeysEnabled() { - return defer(() => this.featureService.getInstanceFeatures()).pipe( - map((features) => features.webKey?.enabled ?? false), - catchError((err) => { - this.toast.showError(err); - return of(false); - }), - ); - } - - private getWebKeys(webKeysEnabled$: Observable) { + private getWebKeys() { return this.refresh.pipe( startWith(true), switchMap(() => { @@ -87,12 +52,6 @@ export class OidcWebKeysComponent implements OnInit { }), map(({ webKeys }) => webKeys), catchError(async (err) => { - const webKeysEnabled = await firstValueFrom(webKeysEnabled$); - // suppress errors if web keys are not enabled - if (!webKeysEnabled) { - return []; - } - this.toast.showError(err); return []; }), diff --git a/console/src/app/pages/orgs/org-detail/org-detail.component.ts b/console/src/app/pages/orgs/org-detail/org-detail.component.ts index 0de6696ac3..39514d33d3 100644 --- a/console/src/app/pages/orgs/org-detail/org-detail.component.ts +++ b/console/src/app/pages/orgs/org-detail/org-detail.component.ts @@ -1,7 +1,6 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { Router } from '@angular/router'; -import { Buffer } from 'buffer'; import { BehaviorSubject, from, Observable, of, Subject, takeUntil } from 'rxjs'; import { catchError, finalize, map } from 'rxjs/operators'; import { CreationType, MemberCreateDialogComponent } from 'src/app/modules/add-member-dialog/member-create-dialog.component'; @@ -266,10 +265,11 @@ export class OrgDetailComponent implements OnInit, OnDestroy { .listOrgMetadata() .then((resp) => { this.loadingMetadata = false; - this.metadata = resp.resultList.map((md) => { + const decoder = new TextDecoder(); + this.metadata = resp.resultList.map(({ key, value }) => { return { - key: md.key, - value: Buffer.from(md.value as string, 'base64').toString('utf-8'), + key, + value: atob(typeof value === 'string' ? value : decoder.decode(value)), }; }); }) diff --git a/console/src/app/pages/users/user-create/user-create-v2/user-create-v2.component.ts b/console/src/app/pages/users/user-create/user-create-v2/user-create-v2.component.ts index b92c112357..7c5d5a0e7f 100644 --- a/console/src/app/pages/users/user-create/user-create-v2/user-create-v2.component.ts +++ b/console/src/app/pages/users/user-create/user-create-v2/user-create-v2.component.ts @@ -204,7 +204,7 @@ export class UserCreateV2Component implements OnInit { if (authenticationFactor.factor === 'initialPassword') { const { password } = authenticationFactor.form.getRawValue(); - humanReq.passwordType = { + humanReq['passwordType'] = { case: 'password', value: { password, diff --git a/console/src/app/pages/users/user-detail/detail-form/detail-form.component.ts b/console/src/app/pages/users/user-detail/detail-form/detail-form.component.ts index ce72f00271..2a136e7e4a 100644 --- a/console/src/app/pages/users/user-detail/detail-form/detail-form.component.ts +++ b/console/src/app/pages/users/user-detail/detail-form/detail-form.component.ts @@ -8,12 +8,14 @@ import { Gender, HumanProfile, HumanProfileSchema } from '@zitadel/proto/zitadel import { filter, startWith } from 'rxjs/operators'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { Profile } from '@zitadel/proto/zitadel/user_pb'; -import { create } from '@bufbuild/protobuf'; +//@ts-ignore +import { create } from '@zitadel/client'; function toHumanProfile(profile: HumanProfile | Profile): HumanProfile { if (profile.$typeName === 'zitadel.user.v2.HumanProfile') { return profile; } + return create(HumanProfileSchema, { givenName: profile.firstName, familyName: profile.lastName, diff --git a/console/src/app/pages/users/user-detail/user-detail/user-detail.component.ts b/console/src/app/pages/users/user-detail/user-detail/user-detail.component.ts index 9370471ea4..45ed6cd30a 100644 --- a/console/src/app/pages/users/user-detail/user-detail/user-detail.component.ts +++ b/console/src/app/pages/users/user-detail/user-detail/user-detail.component.ts @@ -4,7 +4,6 @@ import { Validators } from '@angular/forms'; import { MatDialog } from '@angular/material/dialog'; import { ActivatedRoute, Params, Router } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; -import { Buffer } from 'buffer'; import { catchError, filter, map, startWith, take } from 'rxjs/operators'; import { ChangeType } from 'src/app/modules/changes/changes.component'; import { phoneValidator, requiredValidator } from 'src/app/modules/form-field/validators/validators'; @@ -413,7 +412,10 @@ export class UserDetailComponent implements OnInit { public sendSetPasswordNotification(user: UserV2): void { this.newMgmtService - .sendHumanResetPasswordNotification(user.userId, SendHumanResetPasswordNotificationRequest_Type.EMAIL) + .sendHumanResetPasswordNotification({ + userId: user.userId, + type: SendHumanResetPasswordNotificationRequest_Type.EMAIL, + }) .then(() => { this.toast.showInfo('USER.TOAST.PASSWORDNOTIFICATIONSENT', true); this.refreshChanges$.emit(); @@ -582,7 +584,7 @@ export class UserDetailComponent implements OnInit { const setFcn = (key: string, value: string) => this.newMgmtService.setUserMetadata({ key, - value: Buffer.from(value), + value: new TextEncoder().encode(value), id: user.userId, }); const removeFcn = (key: string): Promise => this.newMgmtService.removeUserMetadata({ key, id: user.userId }); diff --git a/console/src/app/pages/users/user-list/user-table/user-table.component.ts b/console/src/app/pages/users/user-list/user-table/user-table.component.ts index 6ff0d2cd67..cc5d753f36 100644 --- a/console/src/app/pages/users/user-list/user-table/user-table.component.ts +++ b/console/src/app/pages/users/user-list/user-table/user-table.component.ts @@ -36,10 +36,10 @@ import { AuthenticationService } from 'src/app/services/authentication.service'; import { GrpcAuthService } from 'src/app/services/grpc-auth.service'; import { UserState as UserStateV1 } from 'src/app/proto/generated/zitadel/user_pb'; -type Query = Exclude< - Exclude['queries'], undefined>[number]['query'], - undefined ->; +type ListUsersRequest = MessageInitShape; +type QueriesArray = NonNullable; +type QueryWrapper = QueriesArray extends readonly (infer T)[] ? T : never; +type Query = NonNullable; @Component({ selector: 'cnsl-user-table', diff --git a/console/src/app/services/action.service.ts b/console/src/app/services/action.service.ts index dabe2faf01..a3aec6b92d 100644 --- a/console/src/app/services/action.service.ts +++ b/console/src/app/services/action.service.ts @@ -5,6 +5,7 @@ import { CreateTargetRequestSchema, CreateTargetResponse, DeleteTargetRequestSchema, + DeleteTargetResponse, GetTargetRequestSchema, GetTargetResponse, ListExecutionFunctionsRequestSchema, @@ -37,7 +38,7 @@ export class ActionService { return this.grpcService.actionNew.createTarget(req); } - public deleteTarget(req: MessageInitShape): Promise { + public deleteTarget(req: MessageInitShape): Promise { return this.grpcService.actionNew.deleteTarget(req); } diff --git a/console/src/app/services/grpc.service.ts b/console/src/app/services/grpc.service.ts index d2add12f41..b8be808f8d 100644 --- a/console/src/app/services/grpc.service.ts +++ b/console/src/app/services/grpc.service.ts @@ -16,12 +16,9 @@ import { ExhaustedGrpcInterceptor } from './interceptors/exhausted.grpc.intercep import { I18nInterceptor } from './interceptors/i18n.interceptor'; import { NewConnectWebOrgInterceptor, OrgInterceptor, OrgInterceptorProvider } from './interceptors/org.interceptor'; import { UserServiceClient } from '../proto/generated/zitadel/user/v2/User_serviceServiceClientPb'; -//@ts-ignore import { createFeatureServiceClient, createUserServiceClient, createSessionServiceClient } from '@zitadel/client/v2'; -//@ts-ignore import { createAuthServiceClient, createManagementServiceClient } from '@zitadel/client/v1'; import { createGrpcWebTransport } from '@connectrpc/connect-web'; -// @ts-ignore import { createClientFor } from '@zitadel/client'; import { WebKeyService } from '@zitadel/proto/zitadel/webkey/v2beta/webkey_service_pb'; @@ -77,30 +74,10 @@ export class GrpcService { ], }; - this.auth = new AuthServiceClient( - env.api, - null, - // @ts-ignore - interceptors, - ); - this.mgmt = new ManagementServiceClient( - env.api, - null, - // @ts-ignore - interceptors, - ); - this.admin = new AdminServiceClient( - env.api, - null, - // @ts-ignore - interceptors, - ); - this.user = new UserServiceClient( - env.api, - null, - // @ts-ignore - interceptors, - ); + this.auth = new AuthServiceClient(env.api, null, interceptors); + this.mgmt = new ManagementServiceClient(env.api, null, interceptors); + this.admin = new AdminServiceClient(env.api, null, interceptors); + this.user = new UserServiceClient(env.api, null, interceptors); const transport = createGrpcWebTransport({ baseUrl: env.api, diff --git a/console/src/app/services/new-auth.service.ts b/console/src/app/services/new-auth.service.ts index 4864d8d95f..37f0545459 100644 --- a/console/src/app/services/new-auth.service.ts +++ b/console/src/app/services/new-auth.service.ts @@ -44,27 +44,27 @@ export class NewAuthService { } public listMyMultiFactors(): Promise { - return this.grpcService.authNew.listMyAuthFactors(create(ListMyAuthFactorsRequestSchema), null); + return this.grpcService.authNew.listMyAuthFactors(create(ListMyAuthFactorsRequestSchema)); } public removeMyAuthFactorOTPSMS(): Promise { - return this.grpcService.authNew.removeMyAuthFactorOTPSMS(create(RemoveMyAuthFactorOTPSMSRequestSchema), null); + return this.grpcService.authNew.removeMyAuthFactorOTPSMS(create(RemoveMyAuthFactorOTPSMSRequestSchema)); } public getMyLoginPolicy(): Promise { - return this.grpcService.authNew.getMyLoginPolicy(create(GetMyLoginPolicyRequestSchema), null); + return this.grpcService.authNew.getMyLoginPolicy(create(GetMyLoginPolicyRequestSchema)); } public removeMyMultiFactorOTP(): Promise { - return this.grpcService.authNew.removeMyAuthFactorOTP(create(RemoveMyAuthFactorOTPRequestSchema), null); + return this.grpcService.authNew.removeMyAuthFactorOTP(create(RemoveMyAuthFactorOTPRequestSchema)); } public removeMyMultiFactorU2F(tokenId: string): Promise { - return this.grpcService.authNew.removeMyAuthFactorU2F(create(RemoveMyAuthFactorU2FRequestSchema, { tokenId }), null); + return this.grpcService.authNew.removeMyAuthFactorU2F(create(RemoveMyAuthFactorU2FRequestSchema, { tokenId })); } public removeMyAuthFactorOTPEmail(): Promise { - return this.grpcService.authNew.removeMyAuthFactorOTPEmail(create(RemoveMyAuthFactorOTPEmailRequestSchema), null); + return this.grpcService.authNew.removeMyAuthFactorOTPEmail(create(RemoveMyAuthFactorOTPEmailRequestSchema)); } public getMyPasswordComplexityPolicy(): Promise { diff --git a/console/src/app/services/new-mgmt.service.ts b/console/src/app/services/new-mgmt.service.ts index 6798d25f41..9e0c9dc6e4 100644 --- a/console/src/app/services/new-mgmt.service.ts +++ b/console/src/app/services/new-mgmt.service.ts @@ -64,11 +64,10 @@ export class NewMgmtService { } public sendHumanResetPasswordNotification( - userId: string, - type: SendHumanResetPasswordNotificationRequest_Type, + req: MessageInitShape, ): Promise { return this.grpcService.mgmtNew.sendHumanResetPasswordNotification( - create(SendHumanResetPasswordNotificationRequestSchema, { userId, type }), + create(SendHumanResetPasswordNotificationRequestSchema, req), ); } diff --git a/console/src/app/services/user.service.ts b/console/src/app/services/user.service.ts index a5bbd0aaff..714ffa5fe1 100644 --- a/console/src/app/services/user.service.ts +++ b/console/src/app/services/user.service.ts @@ -229,94 +229,3 @@ export class UserService { return this.grpcService.userNew.setPassword(create(SetPasswordRequestSchema, req)); } } - -function userToV2(user: User): UserV2 { - const details = user.getDetails(); - return create(UserSchema, { - userId: user.getId(), - details: details && detailsToV2(details), - state: user.getState() as number as UserState, - username: user.getUserName(), - loginNames: user.getLoginNamesList(), - preferredLoginName: user.getPreferredLoginName(), - type: typeToV2(user), - }); -} - -function detailsToV2(details: ObjectDetails): Details { - const changeDate = details.getChangeDate(); - return create(DetailsSchema, { - sequence: BigInt(details.getSequence()), - changeDate: changeDate && timestampToV2(changeDate), - resourceOwner: details.getResourceOwner(), - }); -} - -function timestampToV2(timestamp: Timestamp): TimestampV2 { - return create(TimestampSchema, { - seconds: BigInt(timestamp.getSeconds()), - nanos: timestamp.getNanos(), - }); -} - -function typeToV2(user: User): UserV2['type'] { - const human = user.getHuman(); - if (human) { - return { case: 'human', value: humanToV2(user, human) }; - } - - const machine = user.getMachine(); - if (machine) { - return { case: 'machine', value: machineToV2(machine) }; - } - - return { case: undefined }; -} - -function humanToV2(user: User, human: Human): HumanUser { - const profile = human.getProfile(); - const email = human.getEmail()?.getEmail(); - const phone = human.getPhone(); - const passwordChanged = human.getPasswordChanged(); - - return create(HumanUserSchema, { - userId: user.getId(), - state: user.getState() as number as UserState, - username: user.getUserName(), - loginNames: user.getLoginNamesList(), - preferredLoginName: user.getPreferredLoginName(), - profile: profile && humanProfileToV2(profile), - email: { email }, - phone: phone && humanPhoneToV2(phone), - passwordChangeRequired: false, - passwordChanged: passwordChanged && timestampToV2(passwordChanged), - }); -} - -function humanProfileToV2(profile: Profile): HumanProfile { - return create(HumanProfileSchema, { - givenName: profile.getFirstName(), - familyName: profile.getLastName(), - nickName: profile.getNickName(), - displayName: profile.getDisplayName(), - preferredLanguage: profile.getPreferredLanguage(), - gender: profile.getGender() as number as Gender, - avatarUrl: profile.getAvatarUrl(), - }); -} - -function humanPhoneToV2(phone: Phone): HumanPhone { - return create(HumanPhoneSchema, { - phone: phone.getPhone(), - isVerified: phone.getIsPhoneVerified(), - }); -} - -function machineToV2(machine: Machine): MachineUser { - return create(MachineUserSchema, { - name: machine.getName(), - description: machine.getDescription(), - hasSecret: machine.getHasSecret(), - accessTokenType: machine.getAccessTokenType() as number as AccessTokenType, - }); -} diff --git a/console/src/app/services/webkeys.service.ts b/console/src/app/services/webkeys.service.ts index 9a26be4712..b70b60da81 100644 --- a/console/src/app/services/webkeys.service.ts +++ b/console/src/app/services/webkeys.service.ts @@ -16,18 +16,18 @@ export class WebKeysService { constructor(private readonly grpcService: GrpcService) {} public ListWebKeys(): Promise { - return this.grpcService.webKey.listWebKeys({}); + return this.grpcService.webKey['listWebKeys']({}); } public DeleteWebKey(id: string): Promise { - return this.grpcService.webKey.deleteWebKey({ id }); + return this.grpcService.webKey['deleteWebKey']({ id }); } public CreateWebKey(req: MessageInitShape): Promise { - return this.grpcService.webKey.createWebKey(req); + return this.grpcService.webKey['createWebKey'](req); } public ActivateWebKey(id: string): Promise { - return this.grpcService.webKey.activateWebKey({ id }); + return this.grpcService.webKey['activateWebKey']({ id }); } } diff --git a/console/src/app/utils/language.ts b/console/src/app/utils/language.ts index 22eac99b3c..483a2e7e2f 100644 --- a/console/src/app/utils/language.ts +++ b/console/src/app/utils/language.ts @@ -18,6 +18,7 @@ export const supportedLanguages = [ 'hu', 'ko', 'ro', + 'tr', ]; -export const supportedLanguagesRegexp: RegExp = /de|en|es|fr|id|it|ja|pl|zh|bg|pt|mk|cs|ru|nl|sv|hu|ko|ro/; +export const supportedLanguagesRegexp: RegExp = /de|en|es|fr|id|it|ja|pl|zh|bg|pt|mk|cs|ru|nl|sv|hu|ko|ro|tr/; export const fallbackLanguage: string = 'en'; diff --git a/console/src/assets/i18n/bg.json b/console/src/assets/i18n/bg.json index dc3dc04193..ccc4d3a501 100644 --- a/console/src/assets/i18n/bg.json +++ b/console/src/assets/i18n/bg.json @@ -1544,7 +1544,8 @@ "id": "Bahasa Indonesia", "hu": "Magyar", "ko": "한국어", - "ro": "Română" + "ro": "Română", + "tr": "Türkçe" } }, "SMTP": { @@ -1623,12 +1624,8 @@ "FEATURES": { "LOGINDEFAULTORG": "Организация по подразбиране за влизане", "LOGINDEFAULTORG_DESCRIPTION": "Потребителският интерфейс за влизане ще използва настройките на организацията по подразбиране (а не на инстанцията), ако не е зададен контекст на организация.", - "OIDCLEGACYINTROSPECTION": "Наследено осмисляне OIDC", - "OIDCLEGACYINTROSPECTION_DESCRIPTION": "На скоро префакторизирахме крайния пункт за осмисляне заради производителностни причини. Тази функция може да се използва за връщане към наследената реализация, ако възникнат неочаквани грешки.", "OIDCTOKENEXCHANGE": "Обмяна на токени OIDC", "OIDCTOKENEXCHANGE_DESCRIPTION": "Активиране на експерименталния тип на дарение urn:ietf:params:oauth:grant-type:token-exchange за краен пункт на токен OIDC. Обменът на токени може да се използва за заявка на токени с по-малък обхват или за имперсонализиране на други потребители. Вижте политиката за сигурност, за да разрешите имперсонализацията на инстанция.", - "OIDCTRIGGERINTROSPECTIONPROJECTIONS": "Тригери за проекции на осмисляне на OIDC", - "OIDCTRIGGERINTROSPECTIONPROJECTIONS_DESCRIPTION": "Активиране на тригери за проекции по време на заявка за осмисляне. Това може да действа като обходен механизъм, ако има забележими проблеми с консистентността в отговора на осмислянето, но може да окаже влияние върху производителността. Планираме да премахнем тригерите за заявки за осмисляне в бъдеще.", "USERSCHEMA": "Потребителска схема", "USERSCHEMA_DESCRIPTION": "Потребителските схеми позволяват управление на данните за схемите на потребителите. Ако е активиран флагът, ще можете да използвате новото API и неговите функции.", "ACTIONS": "Действия", @@ -1643,8 +1640,6 @@ "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "Back-Channel Logout имплементира OpenID Connect Back-Channel Logout 1.0 и може да се използва за уведомяване на клиентите за прекратяване на сесията при OpenID доставчика.", "PERMISSIONCHECKV2": "Проверка на разрешения V2", "PERMISSIONCHECKV2_DESCRIPTION": "Ако флагът е активиран, ще можете да използвате новия API и неговите функции.", - "WEBKEY": "Уеб ключ", - "WEBKEY_DESCRIPTION": "Ако флагът е активиран, ще можете да използвате новия API и неговите функции.", "STATES": { "INHERITED": "Наследено", "ENABLED": "Активирано", @@ -1799,7 +1794,8 @@ "id": "Bahasa Indonesia", "hu": "Magyar", "ko": "한국어", - "ro": "Română" + "ro": "Română", + "tr": "Türkçe" }, "KEYS": { "emailVerificationDoneText": "Проверката на имейл е извършена", @@ -2749,7 +2745,8 @@ "id": "Bahasa Indonesia", "hu": "Magyar", "ko": "한국어", - "ro": "Română" + "ro": "Română", + "tr": "Türkçe" }, "MEMBER": { "ADD": "Добавяне на мениджър", diff --git a/console/src/assets/i18n/cs.json b/console/src/assets/i18n/cs.json index 6c85389c60..d4679772d6 100644 --- a/console/src/assets/i18n/cs.json +++ b/console/src/assets/i18n/cs.json @@ -1545,7 +1545,8 @@ "id": "Bahasa Indonesia", "hu": "Magyar", "ko": "한국어", - "ro": "Română" + "ro": "Română", + "tr": "Türkçe" } }, "SMTP": { @@ -1624,12 +1625,8 @@ "FEATURES": { "LOGINDEFAULTORG": "Výchozí organizace pro přihlášení", "LOGINDEFAULTORG_DESCRIPTION": "Přihlašovací rozhraní použije nastavení výchozí organizace (a ne z instance), pokud není nastaven žádný kontext organizace.", - "OIDCLEGACYINTROSPECTION": "Dědictví OIDC introspekce", - "OIDCLEGACYINTROSPECTION_DESCRIPTION": "Nedávno jsme přepracovali bod introspekce z výkonnostních důvodů. Tato funkce lze použít k rollbacku na dědickou implementaci, pokud se objeví neočekávané chyby.", "OIDCTOKENEXCHANGE": "Výměna tokenů OIDC", "OIDCTOKENEXCHANGE_DESCRIPTION": "Povolit experimentální typ udělení urn:ietf:params:oauth:grant-type:token-exchange pro bod tokenového bodu OIDC. Výměna tokenů lze použít k žádosti o tokeny s menším rozsahem nebo k impersonaci jiných uživatelů. Podívejte se na bezpečnostní politiku, abyste umožnili impersonaci na instanci.", - "OIDCTRIGGERINTROSPECTIONPROJECTIONS": "Spouštěče projekcí introspekce OIDC", - "OIDCTRIGGERINTROSPECTIONPROJECTIONS_DESCRIPTION": "Povolit spouštěče projekcí během požadavku na introspekci. To může sloužit jako obcházení, pokud existují zjevné problémy s konzistencí v odpovědi na introspekci, ale může to mít vliv na výkon. Plánujeme odstranit spouštěče pro požadavky na introspekci v budoucnosti.", "USERSCHEMA": "Schéma uživatele", "USERSCHEMA_DESCRIPTION": "Schémata uživatelů umožňují spravovat datová schémata uživatelů. Pokud je příznak povolen, budete moci používat nové API a jeho funkce.", "ACTIONS": "Akce", @@ -1644,8 +1641,6 @@ "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "Back-Channel Logout implementuje OpenID Connect Back-Channel Logout 1.0 a může být použit k informování klientů o ukončení relace u poskytovatele OpenID.", "PERMISSIONCHECKV2": "Kontrola oprávnění V2", "PERMISSIONCHECKV2_DESCRIPTION": "Pokud je příznak povolen, budete moci používat nový API a jeho funkce.", - "WEBKEY": "Webový klíč", - "WEBKEY_DESCRIPTION": "Pokud je příznak povolen, budete moci používat nový API a jeho funkce.", "STATES": { "INHERITED": "Děděno", "ENABLED": "Povoleno", @@ -1800,7 +1795,8 @@ "id": "Bahasa Indonesia", "hu": "Magyar", "ko": "한국어", - "ro": "Română" + "ro": "Română", + "tr": "Türkçe" }, "KEYS": { "emailVerificationDoneText": "Ověření e-mailu dokončeno", @@ -2763,7 +2759,8 @@ "id": "Bahasa Indonesia", "hu": "Magyar", "ko": "한국어", - "ro": "Română" + "ro": "Română", + "tr": "Türkçe" }, "MEMBER": { "ADD": "Přidat manažera", diff --git a/console/src/assets/i18n/de.json b/console/src/assets/i18n/de.json index a1f449c1c2..bcce1ec0db 100644 --- a/console/src/assets/i18n/de.json +++ b/console/src/assets/i18n/de.json @@ -1545,7 +1545,8 @@ "id": "Bahasa Indonesia", "hu": "Magyar", "ko": "한국어", - "ro": "Română" + "ro": "Română", + "tr": "Türkçe" } }, "SMTP": { @@ -1624,12 +1625,8 @@ "FEATURES": { "LOGINDEFAULTORG": "Standardorganisation für die Anmeldung", "LOGINDEFAULTORG_DESCRIPTION": "Die Anmelde-Benutzeroberfläche verwendet die Einstellungen der Standardorganisation (und nicht von der Instanz), wenn kein Organisationskontext festgelegt ist.", - "OIDCLEGACYINTROSPECTION": "OIDC Legacy-Introspektion", - "OIDCLEGACYINTROSPECTION_DESCRIPTION": "Wir haben kürzlich den Introspektionsendpunkt aus Leistungsgründen neu strukturiert. Mit diesem Feature können Sie zur alten Implementierung zurückkehren, falls unerwartete Fehler auftreten.", "OIDCTOKENEXCHANGE": "OIDC Token-Austausch", "OIDCTOKENEXCHANGE_DESCRIPTION": "Aktivieren Sie den experimentellen urn:ietf:params:oauth:grant-type:token-exchange-Grant-Typ für den OIDC-Token-Endpunkt. Der Token-Austausch kann verwendet werden, um Token mit einem geringeren Umfang anzufordern oder andere Benutzer zu impersonieren. Siehe die Sicherheitsrichtlinie, um die Impersonation auf einer Instanz zu erlauben.", - "OIDCTRIGGERINTROSPECTIONPROJECTIONS": "OIDC Trigger-Introspektionsprojektionen", - "OIDCTRIGGERINTROSPECTIONPROJECTIONS_DESCRIPTION": "Aktivieren Sie Projektionstrigger während einer Introspektionsanfrage. Dies kann als Workaround fungieren, wenn bemerkbare Konsistenzprobleme in der Introspektionsantwort auftreten, kann sich jedoch auf die Leistung auswirken. Wir planen, Trigger für Introspektionsanfragen in Zukunft zu entfernen.", "USERSCHEMA": "Benutzerschema", "USERSCHEMA_DESCRIPTION": "Benutzerschemata ermöglichen das Verwalten von Datenschemata von Benutzern. Wenn die Flagge aktiviert ist, können Sie die neue API und ihre Funktionen verwenden.", "ACTIONS": "Aktionen", @@ -1644,8 +1641,6 @@ "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "Der Back-Channel-Logout implementiert OpenID Connect Back-Channel Logout 1.0 und kann verwendet werden, um Clients über die Beendigung der Sitzung beim OpenID-Provider zu benachrichtigen.", "PERMISSIONCHECKV2": "Berechtigungsprüfung V2", "PERMISSIONCHECKV2_DESCRIPTION": "Wenn die Flagge aktiviert ist, können Sie die neue API und ihre Funktionen verwenden.", - "WEBKEY": "Web-Schlüssel", - "WEBKEY_DESCRIPTION": "Wenn die Flagge aktiviert ist, können Sie die neue API und ihre Funktionen verwenden.", "STATES": { "INHERITED": "Erben", "ENABLED": "Aktiviert", @@ -1800,7 +1795,8 @@ "id": "Bahasa Indonesia", "hu": "Magyar", "ko": "한국어", - "ro": "Română" + "ro": "Română", + "tr": "Türkçe" }, "KEYS": { "emailVerificationDoneText": "Email Verification erfolgreich", @@ -2754,7 +2750,8 @@ "id": "Bahasa Indonesia", "hu": "Magyar", "ko": "한국어", - "ro": "Română" + "ro": "Română", + "tr": "Türkçe" }, "MEMBER": { "ADD": "Manager hinzufügen", diff --git a/console/src/assets/i18n/en.json b/console/src/assets/i18n/en.json index 9e8dadd416..459dc7165d 100644 --- a/console/src/assets/i18n/en.json +++ b/console/src/assets/i18n/en.json @@ -1548,7 +1548,8 @@ "id": "Bahasa Indonesia", "hu": "Magyar", "ko": "한국어", - "ro": "Română" + "ro": "Română", + "tr": "Türkçe" } }, "SMTP": { @@ -1627,12 +1628,8 @@ "FEATURES": { "LOGINDEFAULTORG": "Login Default Org", "LOGINDEFAULTORG_DESCRIPTION": "The login UI will use the settings of the default org (and not from the instance) if no organization context is set", - "OIDCLEGACYINTROSPECTION": "OIDC Legacy introspection", - "OIDCLEGACYINTROSPECTION_DESCRIPTION": "We have recently refactored the introspection endpoint for performance reasons. This feature can be used to rollback to the legacy implementation if unexpected bugs arise.", "OIDCTOKENEXCHANGE": "OIDC Token Exchange", "OIDCTOKENEXCHANGE_DESCRIPTION": "Enable the experimental urn:ietf:params:oauth:grant-type:token-exchange grant type for the OIDC token endpoint. Token exchange can be used to request tokens with a lesser scope or impersonate other users. See the security policy to allow impersonation on an instance.", - "OIDCTRIGGERINTROSPECTIONPROJECTIONS": "OIDC Trigger introspection Projections", - "OIDCTRIGGERINTROSPECTIONPROJECTIONS_DESCRIPTION": "Enable projection triggers during an introspection request. This can act as workaround if there are noticeable consistency issues in the introspection response but can have an impact on performance. We are planning to remove triggers for introspection requests in the future.", "USERSCHEMA": "User Schema", "USERSCHEMA_DESCRIPTION": "User Schemas allow to manage data schemas of user. If the flag is enabled, you'll be able to use the new API and its features.", "ACTIONS": "Actions", @@ -1647,8 +1644,6 @@ "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "The Back-Channel Logout implements OpenID Connect Back-Channel Logout 1.0 and can be used to notify clients about session termination at the OpenID Provider.", "PERMISSIONCHECKV2": "Permission Check V2", "PERMISSIONCHECKV2_DESCRIPTION": "If the flag is enabled, you'll be able to use the new API and its features.", - "WEBKEY": "Web Key", - "WEBKEY_DESCRIPTION": "If the flag is enabled, you'll be able to use the new API and its features.", "STATES": { "INHERITED": "Inherit", "ENABLED": "Enabled", @@ -1803,7 +1798,8 @@ "id": "Bahasa Indonesia", "hu": "Magyar", "ko": "한국어", - "ro": "Română" + "ro": "Română", + "tr": "Türkçe" }, "KEYS": { "emailVerificationDoneText": "Email verification done", @@ -2782,7 +2778,8 @@ "id": "Bahasa Indonesia", "hu": "Magyar", "ko": "한국어", - "ro": "Română" + "ro": "Română", + "tr": "Türkçe" }, "MEMBER": { "ADD": "Add a Manager", diff --git a/console/src/assets/i18n/es.json b/console/src/assets/i18n/es.json index d5493f5d70..f04f06b5fc 100644 --- a/console/src/assets/i18n/es.json +++ b/console/src/assets/i18n/es.json @@ -1546,7 +1546,8 @@ "id": "Bahasa Indonesia", "hu": "Magyar", "ko": "한국어", - "ro": "Română" + "ro": "Română", + "tr": "Türkçe" } }, "SMTP": { @@ -1625,12 +1626,8 @@ "FEATURES": { "LOGINDEFAULTORG": "Organización predeterminada de inicio de sesión", "LOGINDEFAULTORG_DESCRIPTION": "La interfaz de inicio de sesión utilizará la configuración de la organización predeterminada (y no de la instancia) si no se establece ningún contexto de organización.", - "OIDCLEGACYINTROSPECTION": "Introspección heredada OIDC", - "OIDCLEGACYINTROSPECTION_DESCRIPTION": "Recientemente hemos refactorizado el punto de introspección por razones de rendimiento. Esta función se puede utilizar para volver a la implementación heredada si surgen errores inesperados.", "OIDCTOKENEXCHANGE": "Intercambio de tokens OIDC", "OIDCTOKENEXCHANGE_DESCRIPTION": "Habilita el tipo de concesión experimental urn:ietf:params:oauth:grant-type:token-exchange para el punto de extremo de token OIDC. El intercambio de tokens se puede utilizar para solicitar tokens con un alcance menor o suplantar a otros usuarios. Consulta la política de seguridad para permitir la suplantación en una instancia.", - "OIDCTRIGGERINTROSPECTIONPROJECTIONS": "Desencadenadores de proyecciones de introspección OIDC", - "OIDCTRIGGERINTROSPECTIONPROJECTIONS_DESCRIPTION": "Habilita los desencadenadores de proyección durante una solicitud de introspección. Esto puede actuar como un mecanismo alternativo si hay problemas de coherencia perceptibles en la respuesta a la introspección, pero puede afectar al rendimiento. Estamos planeando eliminar los desencadenadores para las solicitudes de introspección en el futuro.", "USERSCHEMA": "Esquema de usuario", "USERSCHEMA_DESCRIPTION": "Los esquemas de usuario permiten gestionar los esquemas de datos de los usuarios. Si se activa la bandera, podrás utilizar la nueva API y sus funciones.", "ACTIONS": "Acciones", @@ -1645,8 +1642,6 @@ "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "El Back-Channel Logout implementa OpenID Connect Back-Channel Logout 1.0 y se puede usar para notificar a los clientes sobre la terminación de la sesión en el proveedor de OpenID.", "PERMISSIONCHECKV2": "Verificación de permisos V2", "PERMISSIONCHECKV2_DESCRIPTION": "Si la bandera está habilitada, podrá usar la nueva API y sus funciones.", - "WEBKEY": "Clave web", - "WEBKEY_DESCRIPTION": "Si la bandera está habilitada, podrá usar la nueva API y sus funciones.", "STATES": { "INHERITED": "Heredado", "ENABLED": "Habilitado", @@ -1801,7 +1796,8 @@ "id": "Bahasa Indonesia", "hu": "Magyar", "ko": "한국어", - "ro": "Română" + "ro": "Română", + "tr": "Türkçe" }, "KEYS": { "emailVerificationDoneText": "Verificación de email realizada", @@ -2751,7 +2747,8 @@ "id": "Bahasa Indonesia", "hu": "Magyar", "ko": "한국어", - "ro": "Română" + "ro": "Română", + "tr": "Türkçe" }, "MEMBER": { "ADD": "Añadir un Mánager", diff --git a/console/src/assets/i18n/fr.json b/console/src/assets/i18n/fr.json index c0a17ac19d..2c39e34ea0 100644 --- a/console/src/assets/i18n/fr.json +++ b/console/src/assets/i18n/fr.json @@ -1545,7 +1545,8 @@ "id": "Bahasa Indonesia", "hu": "Magyar", "ko": "한국어", - "ro": "Română" + "ro": "Română", + "tr": "Türkçe" } }, "SMTP": { @@ -1624,12 +1625,8 @@ "FEATURES": { "LOGINDEFAULTORG": "Organisation par défaut de connexion", "LOGINDEFAULTORG_DESCRIPTION": "L'interface de connexion utilisera les paramètres de l'organisation par défaut (et non de l'instance) si aucun contexte d'organisation n'est défini.", - "OIDCLEGACYINTROSPECTION": "Introspection héritée OIDC", - "OIDCLEGACYINTROSPECTION_DESCRIPTION": "Nous avons récemment refondu le point d'introspection pour des raisons de performances. Cette fonctionnalité peut être utilisée pour revenir à l'implémentation héritée en cas de bogues inattendus.", "OIDCTOKENEXCHANGE": "Échange de jetons OIDC", "OIDCTOKENEXCHANGE_DESCRIPTION": "Activez le type d'octroi expérimental urn:ietf:params:oauth:grant-type:token-exchange pour le point de terminaison de jeton OIDC. L'échange de jetons peut être utilisé pour demander des jetons avec une portée moindre ou pour usurper d'autres utilisateurs. Consultez la politique de sécurité pour autoriser l'usurpation sur une instance.", - "OIDCTRIGGERINTROSPECTIONPROJECTIONS": "Déclencheurs de projections d'introspection OIDC", - "OIDCTRIGGERINTROSPECTIONPROJECTIONS_DESCRIPTION": "Activez les déclencheurs de projection lors d'une demande d'introspection. Cela peut agir comme un contournement s'il existe des problèmes de cohérence perceptibles dans la réponse à l'introspection, mais cela peut avoir un impact sur les performances. Nous prévoyons de supprimer les déclencheurs pour les demandes d'introspection à l'avenir.", "USERSCHEMA": "Schéma utilisateur", "USERSCHEMA_DESCRIPTION": "Les schémas utilisateur permettent de gérer les schémas de données des utilisateurs. Si le drapeau est activé, vous pourrez utiliser la nouvelle API et ses fonctionnalités.", "ACTIONS": "Actions", @@ -1644,8 +1641,6 @@ "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "Le Back-Channel Logout implémente OpenID Connect Back-Channel Logout 1.0 et peut être utilisé pour notifier les clients de la fin de session chez le fournisseur OpenID.", "PERMISSIONCHECKV2": "Vérification des permissions V2", "PERMISSIONCHECKV2_DESCRIPTION": "Si le drapeau est activé, vous pourrez utiliser la nouvelle API et ses fonctionnalités.", - "WEBKEY": "Clé web", - "WEBKEY_DESCRIPTION": "Si le drapeau est activé, vous pourrez utiliser la nouvelle API et ses fonctionnalités.", "STATES": { "INHERITED": "Hérité", "ENABLED": "Activé", @@ -1800,7 +1795,8 @@ "id": "Bahasa Indonesia", "hu": "Magyar", "ko": "한국어", - "ro": "Română" + "ro": "Română", + "tr": "Türkçe" }, "KEYS": { "emailVerificationDoneText": "Vérification de l'e-mail effectuée", @@ -2755,7 +2751,8 @@ "id": "Bahasa Indonesia", "hu": "Magyar", "ko": "한국어", - "ro": "Română" + "ro": "Română", + "tr": "Türkçe" }, "MEMBER": { "ADD": "Ajouter un responsable", diff --git a/console/src/assets/i18n/hu.json b/console/src/assets/i18n/hu.json index 2f208b82b9..0f49eca901 100644 --- a/console/src/assets/i18n/hu.json +++ b/console/src/assets/i18n/hu.json @@ -1545,7 +1545,8 @@ "id": "Indonéz", "hu": "Magyar", "ko": "한국어", - "ro": "Română" + "ro": "Română", + "tr": "Türkçe" } }, "SMTP": { @@ -1622,12 +1623,8 @@ "FEATURES": { "LOGINDEFAULTORG": "Alapértelmezett Org bejelentkezés", "LOGINDEFAULTORG_DESCRIPTION": "A bejelentkezési felület az alapértelmezett org beállításait fogja használni (és nem az instance-tól), ha nincs megadva szervezeti kontextus", - "OIDCLEGACYINTROSPECTION": "OIDC régi introspekció", - "OIDCLEGACYINTROSPECTION_DESCRIPTION": "Nemrég refaktoráltuk az introspekciós végpontot a teljesítmény javítása érdekében. Ezt a funkciót használhatod a régi implementációra való visszaállításhoz, ha váratlan hibák lépnének fel.", "OIDCTOKENEXCHANGE": "OIDC Token Exchange", "OIDCTOKENEXCHANGE_DESCRIPTION": "Engedélyezd a kísérleti urn:ietf:params:oauth:grant-type:token-exchange támogatását az OIDC token végpont számára. A token csere használható kisebb hatókörű tokenek kérésére vagy más felhasználók megszemélyesítésére. Tekintsd meg a biztonsági irányelvet az impersonáció engedélyezéséhez egy példányon.", - "OIDCTRIGGERINTROSPECTIONPROJECTIONS": "OIDC Introspekciós Projekciók Indítása", - "OIDCTRIGGERINTROSPECTIONPROJECTIONS_DESCRIPTION": "Engedélyezd a projekciós indítókat az introspekciós kérés során. Ez lehet egy megoldás, ha észrevehető konzisztenciaproblémák vannak az introspekciós válaszban, de hatással lehet a teljesítményre. Tervezzük, hogy a jövőben eltávolítjuk a triggereket az introspekciós kérésből.", "USERSCHEMA": "Felhasználói Séma", "USERSCHEMA_DESCRIPTION": "A Felhasználói Sémák lehetővé teszik a felhasználói adat sémák kezelését. Ha az opció engedélyezve van, használhatod az új API-t és annak funkcióit.", "ACTIONS": "Műveletek", @@ -1642,8 +1639,6 @@ "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "A Back-Channel Logout megvalósítja az OpenID Connect Back-Channel Logout 1.0-t, és használható az ügyfelek értesítésére a munkamenet befejezéséről az OpenID szolgáltatónál.", "PERMISSIONCHECKV2": "Engedély ellenőrzés V2", "PERMISSIONCHECKV2_DESCRIPTION": "Ha a zászló engedélyezve van, használhatja az új API-t és annak funkcióit.", - "WEBKEY": "Webkulcs", - "WEBKEY_DESCRIPTION": "Ha a zászló engedélyezve van, használhatja az új API-t és annak funkcióit.", "STATES": { "INHERITED": "Örököl", "ENABLED": "Engedélyezve", @@ -1798,7 +1793,8 @@ "id": "Indonéz", "hu": "Magyar", "ko": "한국어", - "ro": "Română" + "ro": "Română", + "tr": "Türkçe" }, "KEYS": { "emailVerificationDoneText": "E-mail ellenőrzés kész", @@ -2777,7 +2773,8 @@ "id": "Indonéz", "hu": "Magyar", "ko": "한국어", - "ro": "Română" + "ro": "Română", + "tr": "Türkçe" }, "MEMBER": { "ADD": "Hozzáadás egy menedzsert", diff --git a/console/src/assets/i18n/id.json b/console/src/assets/i18n/id.json index d1d4001813..8c772104bb 100644 --- a/console/src/assets/i18n/id.json +++ b/console/src/assets/i18n/id.json @@ -1423,7 +1423,8 @@ "id": "Bahasa Indonesia", "hu": "Magyar", "ko": "한국어", - "ro": "Română" + "ro": "Română", + "tr": "Türkçe" } }, "SMTP": { @@ -1498,12 +1499,8 @@ "FEATURES": { "LOGINDEFAULTORG": "Masuk Organisasi Default", "LOGINDEFAULTORG_DESCRIPTION": "UI login akan menggunakan pengaturan organisasi default (dan bukan dari instance) jika tidak ada konteks organisasi yang ditetapkan", - "OIDCLEGACYINTROSPECTION": "Introspeksi Warisan OIDC", - "OIDCLEGACYINTROSPECTION_DESCRIPTION": "Kami baru-baru ini memfaktorkan ulang titik akhir introspeksi untuk alasan kinerja. Fitur ini dapat digunakan untuk melakukan rollback ke implementasi lama jika muncul bug yang tidak terduga.", "OIDCTOKENEXCHANGE": "Pertukaran Token OIDC", "OIDCTOKENEXCHANGE_DESCRIPTION": "Aktifkan jenis pemberian urn:ietf:params:oauth:grant-type:token-exchange eksperimental untuk titik akhir token OIDC. Pertukaran token dapat digunakan untuk meminta token dengan cakupan yang lebih kecil atau menyamar sebagai pengguna lain. Lihat kebijakan keamanan untuk mengizinkan peniruan identitas pada sebuah instans.", - "OIDCTRIGGERINTROSPECTIONPROJECTIONS": "OIDC Memicu Proyeksi Introspeksi", - "OIDCTRIGGERINTROSPECTIONPROJECTIONS_DESCRIPTION": "Aktifkan pemicu proyeksi selama permintaan introspeksi. Hal ini dapat menjadi solusi jika terdapat masalah konsistensi yang nyata dalam respons introspeksi namun dapat berdampak pada kinerja. Kami berencana untuk menghilangkan pemicu permintaan introspeksi di masa depan.", "USERSCHEMA": "Skema Pengguna", "USERSCHEMA_DESCRIPTION": "Skema Pengguna memungkinkan untuk mengelola skema data pengguna. Jika tanda ini diaktifkan, Anda akan dapat menggunakan API baru dan fitur-fiturnya.", "ACTIONS": "Tindakan", @@ -1515,8 +1512,6 @@ "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "The Back-Channel Logout implements OpenID Connect Back-Channel Logout 1.0 and can be used to notify clients about session termination at the OpenID Provider.", "PERMISSIONCHECKV2": "Permission Check V2", "PERMISSIONCHECKV2_DESCRIPTION": "If the flag is enabled, you'll be able to use the new API and its features.", - "WEBKEY": "Web Key", - "WEBKEY_DESCRIPTION": "If the flag is enabled, you'll be able to use the new API and its features.", "STATES": { "INHERITED": "Mewarisi", "ENABLED": "Diaktifkan", "DISABLED": "Dengan disabilitas" }, "INHERITED_DESCRIPTION": "Ini menetapkan nilai ke nilai default sistem.", "INHERITEDINDICATOR_DESCRIPTION": { @@ -1664,7 +1659,8 @@ "id": "Bahasa Indonesia", "hu": "Magyar", "ko": "한국어", - "ro": "Română" + "ro": "Română", + "tr": "Türkçe" }, "KEYS": { "emailVerificationDoneText": "Verifikasi email selesai", @@ -2462,7 +2458,8 @@ "id": "Bahasa Indonesia", "hu": "Magyar", "ko": "한국어", - "ro": "Română" + "ro": "Română", + "tr": "Türkçe" }, "MEMBER": { "ADD": "Tambahkan Manajer", diff --git a/console/src/assets/i18n/it.json b/console/src/assets/i18n/it.json index 2645afb801..a26653ad9d 100644 --- a/console/src/assets/i18n/it.json +++ b/console/src/assets/i18n/it.json @@ -1545,7 +1545,8 @@ "id": "Bahasa Indonesia", "hu": "Magyar", "ko": "한국어", - "ro": "Română" + "ro": "Română", + "tr": "Türkçe" } }, "SMTP": { @@ -1624,12 +1625,8 @@ "FEATURES": { "LOGINDEFAULTORG": "Organizzazione predefinita per l'accesso", "LOGINDEFAULTORG_DESCRIPTION": "L'interfaccia di accesso utilizzerà le impostazioni dell'organizzazione predefinita (e non dell'istanza) se non è impostato alcun contesto organizzativo.", - "OIDCLEGACYINTROSPECTION": "Introspezione legacy OIDC", - "OIDCLEGACYINTROSPECTION_DESCRIPTION": "Abbiamo recentemente ristrutturato il punto di introspezione per motivi di prestazioni. Questa funzionalità può essere utilizzata per tornare alla vecchia implementazione in caso di bug imprevisti.", "OIDCTOKENEXCHANGE": "Scambio token OIDC", "OIDCTOKENEXCHANGE_DESCRIPTION": "Abilita il tipo di concessione sperimentale urn:ietf:params:oauth:grant-type:token-exchange per il punto finale del token OIDC. Lo scambio di token può essere utilizzato per richiedere token con uno scopo inferiore o impersonare altri utenti. Consultare la policy di sicurezza per consentire l'impersonificazione su un'istanza.", - "OIDCTRIGGERINTROSPECTIONPROJECTIONS": "Proiezioni trigger OIDC per l'introspezione", - "OIDCTRIGGERINTROSPECTIONPROJECTIONS_DESCRIPTION": "Abilita i trigger di proiezione durante una richiesta di introspezione. Questo può agire come soluzione alternativa se ci sono problemi di coerenza evidenti nella risposta all'introspezione, ma può influire sulle prestazioni. Stiamo pianificando di rimuovere i trigger per le richieste di introspezione in futuro.", "USERSCHEMA": "Schema utente", "USERSCHEMA_DESCRIPTION": "Gli schemi utente consentono di gestire gli schemi di dati degli utenti. Se la flag è attivata, sarà possibile utilizzare la nuova API e le sue funzionalità.", "ACTIONS": "Azioni", @@ -1644,8 +1641,6 @@ "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "Il Back-Channel Logout implementa OpenID Connect Back-Channel Logout 1.0 e può essere utilizzato per notificare ai client la terminazione della sessione presso il provider OpenID.", "PERMISSIONCHECKV2": "Controllo permessi V2", "PERMISSIONCHECKV2_DESCRIPTION": "Se il flag è abilitato, potrai utilizzare la nuova API e le sue funzionalità.", - "WEBKEY": "Chiave Web", - "WEBKEY_DESCRIPTION": "Se il flag è abilitato, potrai utilizzare la nuova API e le sue funzionalità.", "STATES": { "INHERITED": "Predefinito", "ENABLED": "Abilitato", @@ -1800,7 +1795,8 @@ "id": "Bahasa Indonesia", "hu": "Magyar", "ko": "한국어", - "ro": "Română" + "ro": "Română", + "tr": "Türkçe" }, "KEYS": { "emailVerificationDoneText": "Verifica dell'e-mail terminata con successo.", @@ -2755,7 +2751,8 @@ "id": "Bahasa Indonesia", "hu": "Magyar", "ko": "한국어", - "ro": "Română" + "ro": "Română", + "tr": "Türkçe" }, "MEMBER": { "ADD": "Aggiungi un manager", diff --git a/console/src/assets/i18n/ja.json b/console/src/assets/i18n/ja.json index 95855ca5fe..3210e33e0b 100644 --- a/console/src/assets/i18n/ja.json +++ b/console/src/assets/i18n/ja.json @@ -1545,7 +1545,8 @@ "id": "Bahasa Indonesia", "hu": "Magyar", "ko": "한국어", - "ro": "Română" + "ro": "Română", + "tr": "Türkçe" } }, "SMTP": { @@ -1624,12 +1625,8 @@ "FEATURES": { "LOGINDEFAULTORG": "ログイン時の既定組織", "LOGINDEFAULTORG_DESCRIPTION": "組織コンテキストが設定されていない場合、ログイン UI は既定の組織の設定を使用します (インスタンスの設定ではなく)", - "OIDCLEGACYINTROSPECTION": "OIDC レガシーイントロスペクション", - "OIDCLEGACYINTROSPECTION_DESCRIPTION": "パフォーマンス上の理由から最近イントロスペクション エンドポイントをリファクタリングしました。この機能は、予期しないバグが発生した場合にレガシー実装にロールバックするために使用できます。", "OIDCTOKENEXCHANGE": "OIDC トークン交換", "OIDCTOKENEXCHANGE_DESCRIPTION": "OIDC トークン エンドポイント用に実験的な urn:ietf:params:oauth:grant-type:token-exchange 付与タイプを有効にします。トークン交換は、より少ないスコープを持つトークンを要求するか、他のユーザーになりすますために使用できます。インスタンスでのなりすましを許可するには、セキュリティポリシーを参照してください。", - "OIDCTRIGGERINTROSPECTIONPROJECTIONS": "OIDC トリガーイントロスペクションプロジェクション", - "OIDCTRIGGERINTROSPECTIONPROJECTIONS_DESCRIPTION": "イントロスペクション要求中にプロジェクショントリガーを有効にします。これは、イントロスペクションレスポンスに顕著な整合性問題がある場合の回避策として機能しますが、パフォーマンスに影響を与える可能性があります。今後、イントロスペクション要求のトリガーを削除する予定です。", "USERSCHEMA": "ユーザー スキーマ", "USERSCHEMA_DESCRIPTION": "ユーザー スキーマを使用すると、ユーザーのデータスキーマを管理できます。フラグが有効になっている場合、新しい APIとその機能を使用できます。", "ACTIONS": "アクション", @@ -1644,8 +1641,6 @@ "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "バックチャネルログアウトは OpenID Connect バックチャネルログアウト 1.0 を実装し、OpenID プロバイダーでのセッション終了についてクライアントに通知するために使用できます。", "PERMISSIONCHECKV2": "権限チェック V2", "PERMISSIONCHECKV2_DESCRIPTION": "フラグが有効になっている場合、新しい API とその機能を使用できます。", - "WEBKEY": "ウェブキー", - "WEBKEY_DESCRIPTION": "フラグが有効になっている場合、新しい API とその機能を使用できます。", "STATES": { "INHERITED": "継承", "ENABLED": "有効", @@ -1800,7 +1795,8 @@ "id": "Bahasa Indonesia", "hu": "Magyar", "ko": "한국어", - "ro": "Română" + "ro": "Română", + "tr": "Türkçe" }, "KEYS": { "emailVerificationDoneText": "メール認証が完了しました", @@ -2779,7 +2775,8 @@ "id": "Bahasa Indonesia", "hu": "Magyar", "ko": "한국어", - "ro": "Română" + "ro": "Română", + "tr": "Türkçe" }, "MEMBER": { "ADD": "マネージャーを追加する", diff --git a/console/src/assets/i18n/ko.json b/console/src/assets/i18n/ko.json index b51f14ff20..1ec00f395e 100644 --- a/console/src/assets/i18n/ko.json +++ b/console/src/assets/i18n/ko.json @@ -1545,7 +1545,8 @@ "id": "Bahasa Indonesia", "hu": "Magyar", "ko": "한국어", - "ro": "Română" + "ro": "Română", + "tr": "Türkçe" } }, "SMTP": { @@ -1624,12 +1625,8 @@ "FEATURES": { "LOGINDEFAULTORG": "로그인 기본 조직", "LOGINDEFAULTORG_DESCRIPTION": "조직 컨텍스트가 설정되지 않은 경우 로그인 UI가 기본 조직의 설정을 사용합니다 (인스턴스에서 설정되지 않음).", - "OIDCLEGACYINTROSPECTION": "OIDC 레거시 내부 조사", - "OIDCLEGACYINTROSPECTION_DESCRIPTION": "최근 내부 조사 엔드포인트를 성능을 위해 리팩토링했습니다. 예상치 못한 버그가 발생하면 이 기능을 사용하여 레거시 구현으로 롤백할 수 있습니다.", "OIDCTOKENEXCHANGE": "OIDC 토큰 교환", "OIDCTOKENEXCHANGE_DESCRIPTION": "OIDC 토큰 엔드포인트의 실험적 urn:ietf:params:oauth:grant-type:token-exchange 허용을 활성화합니다. 토큰 교환을 통해 범위가 좁은 토큰을 요청하거나 다른 사용자를 가장할 수 있습니다. 인스턴스에서 가장을 허용하는 보안 정책을 확인하세요.", - "OIDCTRIGGERINTROSPECTIONPROJECTIONS": "OIDC 트리거 내부 조사 프로젝션", - "OIDCTRIGGERINTROSPECTIONPROJECTIONS_DESCRIPTION": "내부 조사 요청 중 프로젝션 트리거를 활성화합니다. 이는 내부 조사 응답에서 일관성 문제가 있는 경우 임시 해결책으로 작동할 수 있으나 성능에 영향을 미칠 수 있습니다. 향후 내부 조사 요청에 대한 트리거 제거를 계획 중입니다.", "USERSCHEMA": "사용자 스키마", "USERSCHEMA_DESCRIPTION": "사용자 스키마를 통해 사용자의 데이터 스키마를 관리할 수 있습니다. 플래그가 활성화되면 새 API 및 기능을 사용할 수 있습니다.", "ACTIONS": "액션", @@ -1644,8 +1641,6 @@ "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "백채널 로그아웃은 OpenID Connect 백채널 로그아웃 1.0을 구현하며, OpenID 제공자에서 세션 종료에 대해 클라이언트에게 알리는 데 사용할 수 있습니다.", "PERMISSIONCHECKV2": "권한 확인 V2", "PERMISSIONCHECKV2_DESCRIPTION": "플래그가 활성화되면 새로운 API와 그 기능을 사용할 수 있습니다.", - "WEBKEY": "웹 키", - "WEBKEY_DESCRIPTION": "플래그가 활성화되면 새로운 API와 그 기능을 사용할 수 있습니다.", "STATES": { "INHERITED": "상속", "ENABLED": "활성화됨", @@ -1800,7 +1795,8 @@ "id": "Bahasa Indonesia", "hu": "Magyar", "ko": "한국어", - "ro": "Română" + "ro": "Română", + "tr": "Türkçe" }, "KEYS": { "emailVerificationDoneText": "이메일 인증 완료", @@ -2775,7 +2771,8 @@ "id": "Bahasa Indonesia", "hu": "Magyar", "ko": "한국어", - "ro": "Română" + "ro": "Română", + "tr": "Türkçe" }, "MEMBER": { "ADD": "매니저 추가", diff --git a/console/src/assets/i18n/mk.json b/console/src/assets/i18n/mk.json index c89a78238d..b96ebddadf 100644 --- a/console/src/assets/i18n/mk.json +++ b/console/src/assets/i18n/mk.json @@ -1546,7 +1546,8 @@ "id": "Bahasa Indonesia", "hu": "Magyar", "ko": "한국어", - "ro": "Română" + "ro": "Română", + "tr": "Türkçe" } }, "SMTP": { @@ -1625,12 +1626,8 @@ "FEATURES": { "LOGINDEFAULTORG": "Најава Стандардна организација", "LOGINDEFAULTORG_DESCRIPTION": "Интерфејсот за најавување ќе ги користи поставките на стандардната организација (а не од примерот) ако не е поставен контекст на организацијата", - "OIDCLEGACYINTROSPECTION": "Интроспекција на наследството на OIDC", - "OIDCLEGACYINTROSPECTION_DESCRIPTION": "Неодамна ја рефакториравме крајната точка на интроспекција поради перформанси. Оваа функција може да се користи за враќање на наследната имплементација доколку се појават неочекувани грешки.", "OIDCTOKENEXCHANGE": "Размена на токени OIDC", "OIDCTOKENEXCHANGE_DESCRIPTION": "Овозможете го експерименталниот тип на грант urn:ietf:params:oauth:grant-type:token-exchange за крајната точка на токенот OIDC. Размената на токени може да се користи за барање токени со помал опсег или имитирање на други корисници. Погледнете ја безбедносната политика за да дозволите имитирање на пример.", - "OIDCTRIGGERINTROSPECTIONPROJECTIONS": "Проекции за интроспекција на активирањето на OIDC", - "OIDCTRIGGERINTROSPECTIONPROJECTIONS_DESCRIPTION": "Овозможете предизвикувачи за проекција за време на барање за интроспекција. Ова може да дејствува како заобиколување ако има забележителни проблеми со конзистентноста во одговорот на интроспекцијата, но може да има влијание врз перформансите. Планираме да ги отстраниме предизвикувачите за барањата за интроспекција во иднина.", "USERSCHEMA": "Корисничка шема", "USERSCHEMA_DESCRIPTION": "Корисничките шеми овозможуваат управување со податоци шеми на корисникот. Ако знамето е овозможено, ќе можете да го користите новиот API и неговите функции.", "ACTIONS": "Акции", @@ -1645,8 +1642,6 @@ "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "Back-Channel Logout имплементира OpenID Connect Back-Channel Logout 1.0 и може да се користи за известување на клиентите за завршување на сесијата кај OpenID провајдерот.", "PERMISSIONCHECKV2": "Проверка на дозволи V2", "PERMISSIONCHECKV2_DESCRIPTION": "Ако знамето е овозможено, ќе можете да ја користите новата API и нејзините функции.", - "WEBKEY": "Веб клуч", - "WEBKEY_DESCRIPTION": "Ако знамето е овозможено, ќе можете да ја користите новата API и нејзините функции.", "STATES": { "INHERITED": "Наследи", "ENABLED": "Овозможено", @@ -1801,7 +1796,8 @@ "id": "Bahasa Indonesia", "hu": "Magyar", "ko": "한국어", - "ro": "Română" + "ro": "Română", + "tr": "Türkçe" }, "KEYS": { "emailVerificationDoneText": "Е-поштата е верифицирана", @@ -2751,7 +2747,8 @@ "id": "Bahasa Indonesia", "hu": "Magyar", "ko": "한국어", - "ro": "Română" + "ro": "Română", + "tr": "Türkçe" }, "MEMBER": { "ADD": "Додај Менаџер", diff --git a/console/src/assets/i18n/nl.json b/console/src/assets/i18n/nl.json index 17eb2241f7..5f10dc9e39 100644 --- a/console/src/assets/i18n/nl.json +++ b/console/src/assets/i18n/nl.json @@ -1545,7 +1545,8 @@ "id": "Bahasa Indonesia", "hu": "Magyar", "ko": "한국어", - "ro": "Română" + "ro": "Română", + "tr": "Türkçe" } }, "SMTP": { @@ -1624,12 +1625,8 @@ "FEATURES": { "LOGINDEFAULTORG": "Standaard inlogorganisatie", "LOGINDEFAULTORG_DESCRIPTION": "Als er geen organisatiecontext is ingesteld, gebruikt de inlog-UI de instellingen van de standaardorganisatie (en niet van de instantie)", - "OIDCLEGACYINTROSPECTION": "Oude OIDC-introspectie", - "OIDCLEGACYINTROSPECTION_DESCRIPTION": "We hebben onlangs het introspectie-endpoint opnieuw gefactoreerd omwille van de prestaties. Deze functie kan worden gebruikt om terug te keren naar de oude implementatie als er onverwachte bugs optreden.", "OIDCTOKENEXCHANGE": "OIDC-tokenuitwisseling", "OIDCTOKENEXCHANGE_DESCRIPTION": "Schakel het experimentele type verlening urn:ietf:params:oauth:grant-type:token-exchange in voor het OIDC-tokenendpoint. Tokenuitwisseling kan worden gebruikt om tokens met een kleinere scope op te vragen of om zich voor te doen als andere gebruikers. Raadpleeg het beveiligingsbeleid om impersonation op een instantie toe te staan.", - "OIDCTRIGGERINTROSPECTIONPROJECTIONS": "OIDC-triggers voor introspectieprojecties", - "OIDCTRIGGERINTROSPECTIONPROJECTIONS_DESCRIPTION": "Schakel projectietriggers in tijdens een introspectieverzoek. Dit kan dienen als een tijdelijke oplossing als er merkbare consistentieproblemen optreden in het introspectieantwoord, maar het kan wel prestaties beïnvloeden. We zijn van plan om triggers voor introspectieverzoeken in de toekomst te verwijderen.", "USERSCHEMA": "Gebruikerschema", "USERSCHEMA_DESCRIPTION": "Met gebruikerschema's kunt u de dataschema's van gebruikers beheren. Als de vlag is ingeschakeld, kunt u de nieuwe API en zijn functies gebruiken.", "ACTIONS": "Acties", @@ -1644,8 +1641,6 @@ "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "De Back-Channel Logout implementeert OpenID Connect Back-Channel Logout 1.0 en kan worden gebruikt om clients te informeren over het beëindigen van de sessie bij de OpenID-provider.", "PERMISSIONCHECKV2": "Permissiecontrole V2", "PERMISSIONCHECKV2_DESCRIPTION": "Als de vlag is ingeschakeld, kunt u de nieuwe API en de bijbehorende functies gebruiken.", - "WEBKEY": "Websleutel", - "WEBKEY_DESCRIPTION": "Als de vlag is ingeschakeld, kunt u de nieuwe API en de bijbehorende functies gebruiken.", "STATES": { "INHERITED": "Overgenomen", "ENABLED": "Ingeschakeld", @@ -1800,7 +1795,8 @@ "id": "Bahasa Indonesia", "hu": "Magyar", "ko": "한국어", - "ro": "Română" + "ro": "Română", + "tr": "Türkçe" }, "KEYS": { "emailVerificationDoneText": "E-mail verificatie voltooid", @@ -2772,7 +2768,8 @@ "id": "Bahasa Indonesia", "hu": "Magyar", "ko": "한국어", - "ro": "Română" + "ro": "Română", + "tr": "Türkçe" }, "MEMBER": { "ADD": "Voeg een Manager toe", diff --git a/console/src/assets/i18n/pl.json b/console/src/assets/i18n/pl.json index 33e5f5291d..1a0a323242 100644 --- a/console/src/assets/i18n/pl.json +++ b/console/src/assets/i18n/pl.json @@ -1544,7 +1544,8 @@ "id": "Bahasa Indonesia", "hu": "Magyar", "ko": "한국어", - "ro": "Română" + "ro": "Română", + "tr": "Türkçe" } }, "SMTP": { @@ -1623,12 +1624,8 @@ "FEATURES": { "LOGINDEFAULTORG": "Domyślna Organizacja Logowania", "LOGINDEFAULTORG_DESCRIPTION": "Jeśli nie ustawiono kontekstu organizacji, interfejs logowania będzie używać ustawień domyślnej organizacji (a nie instancji)", - "OIDCLEGACYINTROSPECTION": "Starsza Introspekcja OIDC", - "OIDCLEGACYINTROSPECTION_DESCRIPTION": "Ostatnio przeprojektowaliśmy punkt końcowy introspekcji ze względów wydajnościowych. Ta funkcja może być używana do cofnięcia do starszej implementacji, jeśli wystąpią nieoczekiwane błędy.", "OIDCTOKENEXCHANGE": "Wymiana Tokenów OIDC", "OIDCTOKENEXCHANGE_DESCRIPTION": "Włącz eksperymentalny typ grantu urn:ietf:params:oauth:grant-type:token-exchange dla punktu końcowego tokena OIDC. Wymiana tokenów może być używana do żądania tokenów o mniejszym zakresie lub podszywania się za innych użytkowników. Aby zezwolić na podszywanie się na instancji, zapoznaj się z polityką bezpieczeństwa.", - "OIDCTRIGGERINTROSPECTIONPROJECTIONS": "Projekcje Introspekcji Wyzwalane przez OIDC", - "OIDCTRIGGERINTROSPECTIONPROJECTIONS_DESCRIPTION": "Włącz wyzwalacze projekcji podczas żądania introspekcji. Może to stanowić obejście, jeśli w odpowiedzi introspekcji występują zauważalne problemy z spójnością, ale może mieć wpływ na wydajność. Planujemy w przyszłości usunąć wyzwalacze dla żądań introspekcji.", "USERSCHEMA": "Schemat Użytkownika", "USERSCHEMA_DESCRIPTION": "Schematy użytkowników umożliwiają zarządzanie schematami danych użytkowników. Jeśli flaga jest włączona, będziesz mógł korzystać z nowego interfejsu API i jego funkcji.", "ACTIONS": "Akcje", @@ -1643,8 +1640,6 @@ "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "Back-Channel Logout implementuje OpenID Connect Back-Channel Logout 1.0 i może być używany do powiadamiania klientów o zakończeniu sesji u dostawcy OpenID.", "PERMISSIONCHECKV2": "Sprawdzanie uprawnień V2", "PERMISSIONCHECKV2_DESCRIPTION": "Jeśli flaga jest włączona, będziesz mógł korzystać z nowego API i jego funkcji.", - "WEBKEY": "Klucz Web", - "WEBKEY_DESCRIPTION": "Jeśli flaga jest włączona, będziesz mógł korzystać z nowego API i jego funkcji.", "STATES": { "INHERITED": "Dziedziczony", "ENABLED": "Włączony", @@ -1799,7 +1794,8 @@ "id": "Bahasa Indonesia", "hu": "Magyar", "ko": "한국어", - "ro": "Română" + "ro": "Română", + "tr": "Türkçe" }, "KEYS": { "emailVerificationDoneText": "Weryfikacja adresu e-mail zakończona", @@ -2754,7 +2750,8 @@ "id": "Bahasa Indonesia", "hu": "Magyar", "ko": "한국어", - "ro": "Română" + "ro": "Română", + "tr": "Türkçe" }, "MEMBER": { "ADD": "Dodaj managera", diff --git a/console/src/assets/i18n/pt.json b/console/src/assets/i18n/pt.json index ccd17170e4..d5c30f8285 100644 --- a/console/src/assets/i18n/pt.json +++ b/console/src/assets/i18n/pt.json @@ -1546,7 +1546,8 @@ "id": "Bahasa Indonesia", "hu": "Magyar", "ko": "한국어", - "ro": "Română" + "ro": "Română", + "tr": "Türkçe" } }, "SMTP": { @@ -1625,12 +1626,8 @@ "FEATURES": { "LOGINDEFAULTORG": "Organização Padrão de Login", "LOGINDEFAULTORG_DESCRIPTION": "A interface de login utilizará as configurações da organização padrão (e não da instância) se nenhum contexto de organização estiver definido", - "OIDCLEGACYINTROSPECTION": "Introspecção Legada OIDC", - "OIDCLEGACYINTROSPECTION_DESCRIPTION": "Recentemente refatoramos o endpoint de introspecção por motivos de performance. Esse recurso pode ser usado para reverter para a implementação legada caso surjam bugs inesperados.", "OIDCTOKENEXCHANGE": "Troca de Token OIDC", "OIDCTOKENEXCHANGE_DESCRIPTION": "Habilita o tipo de concessão experimental urn:ietf:params:oauth:grant-type:token-exchange para o endpoint de token OIDC. A troca de token pode ser usada para solicitar tokens com escopo menor ou personificar outros usuários. Consulte a política de segurança para permitir a personificação em uma instância.", - "OIDCTRIGGERINTROSPECTIONPROJECTIONS": "Projeções de Introspecção com Gatilho OIDC", - "OIDCTRIGGERINTROSPECTIONPROJECTIONS_DESCRIPTION": "Habilita gatilhos de projeção durante uma solicitação de introspecção. Isso pode funcionar como uma solução alternativa se houver problemas de consistência perceptíveis na resposta de introspecção, mas pode impactar o desempenho. Planejamos remover gatilhos para solicitações de introspecção no futuro.", "USERSCHEMA": "Esquema de Usuário", "USERSCHEMAS_DESCRIPTION": "Esquemas de Usuário permitem gerenciar esquemas de dados do usuário. Se o sinalizador estiver ativado, você poderá usar a nova API e seus recursos.", "ACTIONS": "Ações", @@ -1645,8 +1642,6 @@ "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "O Logout de Back-Channel implementa o OpenID Connect Back-Channel Logout 1.0 e pode ser usado para notificar os clientes sobre a terminação da sessão no Provedor de OpenID.", "PERMISSIONCHECKV2": "Verificação de Permissão V2", "PERMISSIONCHECKV2_DESCRIPTION": "Se a bandeira estiver ativada, você poderá usar a nova API e seus recursos.", - "WEBKEY": "Chave Web", - "WEBKEY_DESCRIPTION": "Se a bandeira estiver ativada, você poderá usar a nova API e seus recursos.", "STATES": { "INHERITED": "Herdade", "ENABLED": "Habilitado", @@ -1801,7 +1796,8 @@ "id": "Bahasa Indonesia", "hu": "Magyar", "ko": "한국어", - "ro": "Română" + "ro": "Română", + "tr": "Türkçe" }, "KEYS": { "emailVerificationDoneText": "Verificação de email concluída", @@ -2750,7 +2746,8 @@ "id": "Bahasa Indonesia", "hu": "Magyar", "ko": "한국어", - "ro": "Română" + "ro": "Română", + "tr": "Türkçe" }, "MEMBER": { "ADD": "Adicionar um Gerente", diff --git a/console/src/assets/i18n/ro.json b/console/src/assets/i18n/ro.json index e0f2e93045..d04bce7207 100644 --- a/console/src/assets/i18n/ro.json +++ b/console/src/assets/i18n/ro.json @@ -1543,7 +1543,8 @@ "id": "Bahasa Indonesia", "hu": "Magyar", "ko": "한국어", - "ro": "Română" + "ro": "Română", + "tr": "Türkçe" } }, "SMTP": { @@ -1622,12 +1623,8 @@ "FEATURES": { "LOGINDEFAULTORG": "Organizație implicită de conectare", "LOGINDEFAULTORG_DESCRIPTION": "UI-ul de conectare va utiliza setările organizației implicite (și nu din instanță) dacă nu este setat niciun context de organizație", - "OIDCLEGACYINTROSPECTION": "Introspecție OIDC Legacy", - "OIDCLEGACYINTROSPECTION_DESCRIPTION": "Am refactorizat recent endpointul de introspecție din motive de performanță. Această caracteristică poate fi utilizată pentru a reveni la implementarea legacy dacă apar erori neașteptate.", "OIDCTOKENEXCHANGE": "Schimb de token OIDC", "OIDCTOKENEXCHANGE_DESCRIPTION": "Activați tipul de grant experimental urn:ietf:params:oauth:grant-type:token-exchange pentru endpointul token OIDC. Schimbul de tokenuri poate fi utilizat pentru a solicita tokenuri cu o rază de acțiune mai mică sau pentru a impersona alți utilizatori. Consultați politica de securitate pentru a permite impersonarea pe o instanță.", - "OIDCTRIGGERINTROSPECTIONPROJECTIONS": "Proiecții de introspecție OIDC Trigger", - "OIDCTRIGGERINTROSPECTIONPROJECTIONS_DESCRIPTION": "Activați declanșatoarele de proiecție în timpul unei solicitări de introspecție. Acest lucru poate acționa ca o soluție dacă există probleme notabile de consistență în răspunsul de introspecție, dar poate avea un impact asupra performanței. Planificăm să eliminăm declanșatoarele pentru solicitările de introspecție în viitor.", "USERSCHEMA": "Schema de utilizator", "USERSCHEMA_DESCRIPTION": "Schemele de utilizator permit gestionarea schemelor de date ale utilizatorului. Dacă indicatorul este activat, veți putea utiliza noul API și caracteristicile sale.", "ACTIONS": "Acțiuni", @@ -1642,8 +1639,6 @@ "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "Logout-ul Back-Channel implementează OpenID Connect Back-Channel Logout 1.0 și poate fi folosit pentru a notifica clienții despre terminarea sesiunii la Producătorul OpenID.", "PERMISSIONCHECKV2": "Verificare Permisiuni V2", "PERMISSIONCHECKV2_DESCRIPTION": "Dacă steagul este activat, veți putea folosi noua API și funcțiile sale.", - "WEBKEY": "Cheie Web", - "WEBKEY_DESCRIPTION": "Dacă steagul este activat, veți putea folosi noua API și funcțiile sale.", "STATES": { "INHERITED": "Moșteniți", "ENABLED": "Activat", @@ -1798,7 +1793,8 @@ "id": "Bahasa Indonesia", "hu": "Magyar", "ko": "한국어", - "ro": "Română" + "ro": "Română", + "tr": "Türkçe" }, "KEYS": { "emailVerificationDoneText": "Verificarea e-mailului efectuată", @@ -2775,7 +2771,8 @@ "id": "Bahasa Indonesia", "hu": "Magyar", "ko": "한국어", - "ro": "Română" + "ro": "Română", + "tr": "Türkçe" }, "MEMBER": { "ADD": "Adăugați un manager", diff --git a/console/src/assets/i18n/ru.json b/console/src/assets/i18n/ru.json index 175e18688b..dbf31ac29e 100644 --- a/console/src/assets/i18n/ru.json +++ b/console/src/assets/i18n/ru.json @@ -1590,7 +1590,8 @@ "id": "Bahasa Indonesia", "hu": "Magyar", "ko": "한국어", - "ro": "Română" + "ro": "Română", + "tr": "Türkçe" } }, "SMTP": { @@ -1677,12 +1678,8 @@ "FEATURES": { "LOGINDEFAULTORG": "Организация по умолчанию для входа", "LOGINDEFAULTORG_DESCRIPTION": "Если контекст организации не установлен, пользовательский интерфейс входа будет использовать настройки организации по умолчанию (а не экземпляра)", - "OIDCLEGACYINTROSPECTION": "Устаревшая интроспекция OIDC", - "OIDCLEGACYINTROSPECTION_DESCRIPTION": "Недавно мы переработали конечную точку интроспекции для повышения производительности. Эта функция может использоваться для отката к устаревшей реализации, если возникнут непредвиденные ошибки.", "OIDCTOKENEXCHANGE": "Обмен токенами OIDC", "OIDCTOKENEXCHANGE_DESCRIPTION": "Включите экспериментальный тип гранта urn:ietf:params:oauth:grant-type:token-exchange для конечной точки токена OIDC. Обмен токенами можно использовать для запроса токенов с меньшей областью действия или для impersonation (выдачи себя за) других пользователей. Информацию о разрешении impersonation на экземпляре см. в политике безопасности.", - "OIDCTRIGGERINTROSPECTIONPROJECTIONS": "Проекции интроспекции с триггером OIDC", - "OIDCTRIGGERINTROSPECTIONPROJECTIONS_DESCRIPTION": "Включите триггеры проекций во время запроса интроспекции. Это может служить обходным путем, если в ответе интроспекции наблюдаются заметные проблемы согласованности, но может повлиять на производительность. В будущем мы планируем удалить триггеры для запросов интроспекции.", "USERSCHEMA": "Схема пользователя", "USERSCHEMA_DESCRIPTION": "Схемы пользователей позволяют управлять схемами данных пользователей. Если флаг включен, вы сможете использовать новый API и его функции.", "ACTIONS": "Действия", @@ -1697,8 +1694,6 @@ "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "Back-Channel Logout реализует OpenID Connect Back-Channel Logout 1.0 и может использоваться для уведомления клиентов о завершении сеанса у поставщика OpenID.", "PERMISSIONCHECKV2": "Проверка Разрешений V2", "PERMISSIONCHECKV2_DESCRIPTION": "Если флаг включен, вы сможете использовать новый API и его функции.", - "WEBKEY": "Веб-ключ", - "WEBKEY_DESCRIPTION": "Если флаг включен, вы сможете использовать новый API и его функции.", "STATES": { "INHERITED": "Наследовать", "ENABLED": "Включено", @@ -1857,7 +1852,8 @@ "id": "Bahasa Indonesia", "hu": "Magyar", "ko": "한국어", - "ro": "Română" + "ro": "Română", + "tr": "Türkçe" }, "LOCALE": "Код языка", "LOCALES": { @@ -2863,7 +2859,8 @@ "id": "Bahasa Indonesia", "hu": "Magyar", "ko": "한국어", - "ro": "Română" + "ro": "Română", + "tr": "Türkçe" }, "MEMBER": { "ADD": "Добавить менеджера", diff --git a/console/src/assets/i18n/sv.json b/console/src/assets/i18n/sv.json index 9de5093353..777e2237aa 100644 --- a/console/src/assets/i18n/sv.json +++ b/console/src/assets/i18n/sv.json @@ -1549,7 +1549,8 @@ "id": "Bahasa Indonesia", "hu": "Magyar", "ko": "한국어", - "ro": "Română" + "ro": "Română", + "tr": "Türkçe" } }, "SMTP": { @@ -1628,12 +1629,8 @@ "FEATURES": { "LOGINDEFAULTORG": "Standardorganisation för inloggning", "LOGINDEFAULTORG_DESCRIPTION": "Inloggningsgränssnittet kommer att använda inställningarna för standardorganisationen (och inte från instansen) om ingen organisationskontext är inställd", - "OIDCLEGACYINTROSPECTION": "OIDC Legacy introspection", - "OIDCLEGACYINTROSPECTION_DESCRIPTION": "Vi har nyligen omarbetat introspektionsändpunkten av prestandaskäl. Denna funktion kan användas för att återgå till den äldre implementationen om oväntade buggar uppstår.", "OIDCTOKENEXCHANGE": "OIDC Token Exchange", "OIDCTOKENEXCHANGE_DESCRIPTION": "Aktivera den experimentella urn:ietf:params:oauth:grant-type:token-exchange grant-typen för OIDC-tokenändpunkten. Tokenutbyte kan användas för att begära tokens med en mindre omfattning eller impersonera andra användare. Se säkerhetspolicyn för att tillåta impersonation på en instans.", - "OIDCTRIGGERINTROSPECTIONPROJECTIONS": "OIDC Trigger introspection Projections", - "OIDCTRIGGERINTROSPECTIONPROJECTIONS_DESCRIPTION": "Aktivera projektionstriggers under en introspektionsbegäran. Detta kan fungera som en lösning om det finns märkbara konsistensproblem i introspektionssvaret men kan påverka prestandan. Vi planerar att ta bort triggers för introspektionsbegäranden i framtiden.", "USERSCHEMA": "Användarschema", "USERSCHEMA_DESCRIPTION": "Användarscheman tillåter att hantera datascheman för användare. Om flaggan är aktiverad kommer du att kunna använda det nya API:et och dess funktioner.", "ACTIONS": "Åtgärder", @@ -1648,8 +1645,6 @@ "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "Back-Channel Logout implementerar OpenID Connect Back-Channel Logout 1.0 och kan användas för att meddela klienter om sessionens avslutning hos OpenID-leverantören.", "PERMISSIONCHECKV2": "Behörighetskontroll V2", "PERMISSIONCHECKV2_DESCRIPTION": "Om flaggan är aktiverad kan du använda den nya API:n och dess funktioner.", - "WEBKEY": "Webbnyckel", - "WEBKEY_DESCRIPTION": "Om flaggan är aktiverad kan du använda den nya API:n och dess funktioner.", "STATES": { "INHERITED": "Ärv", "ENABLED": "Aktiverad", @@ -1804,7 +1799,8 @@ "id": "Bahasa Indonesia", "hu": "Magyar", "ko": "한국어", - "ro": "Română" + "ro": "Română", + "tr": "Türkçe" }, "KEYS": { "emailVerificationDoneText": "E-postverifiering klar", @@ -2783,7 +2779,8 @@ "id": "Bahasa Indonesia", "hu": "Magyar", "ko": "한국어", - "ro": "Română" + "ro": "Română", + "tr": "Türkçe" }, "MEMBER": { "ADD": "Lägg till en administratör", diff --git a/console/src/assets/i18n/tr.json b/console/src/assets/i18n/tr.json new file mode 100644 index 0000000000..58b949f692 --- /dev/null +++ b/console/src/assets/i18n/tr.json @@ -0,0 +1,2859 @@ +{ + "APP_NAME": "ZITADEL", + "DESCRIPTIONS": { + "METADATA_TITLE": "Meta Veri", + "HOME": { + "TITLE": "ZITADEL ile başlayın", + "NEXT": { + "TITLE": "Sonraki adımlarınız", + "DESCRIPTION": "Uygulamanızı güvence altına almak için aşağıdaki adımları tamamlayın.", + "CREATE_PROJECT": { + "TITLE": "Bir proje oluşturun", + "DESCRIPTION": "Bir proje ekleyin ve rollerini ve yetkilendirmelerini tanımlayın." + } + }, + "MORE_SHORTCUTS": { + "GET_STARTED": { + "TITLE": "Başlangıç", + "DESCRIPTION": "Hızlı başlangıç adım adım kılavuzunu takip edin ve hemen oluşturmaya başlayın." + }, + "DOCS": { + "TITLE": "Belgeler", + "DESCRIPTION": "Temel kavram ve fikirlerle tanışmak için ZITADEL bilgi tabanını keşfedin. ZITADEL'in nasıl çalıştığını ve nasıl kullanılacağını öğrenin." + }, + "EXAMPLES": { + "TITLE": "Örnekler ve Yazılım Geliştirme Kitleri", + "DESCRIPTION": "ZITADEL'i favori programlama dilleriniz ve araçlarınızla birlikte kullanmak için örneklerimizi ve SDK'larımızı inceleyin." + } + } + }, + "ORG": { + "TITLE": "Organizasyon", + "DESCRIPTION": "Bir organizasyon kullanıcıları, uygulamalı projeleri, kimlik sağlayıcıları ve şirket markalaması gibi ayarları barındırır. Ayarları birden fazla organizasyon arasında paylaşmak ister misiniz? Varsayılan ayarları yapılandırın.", + "METADATA": "Organizasyona konum veya başka bir sistemdeki tanımlayıcı gibi özel öznitelikler ekleyin. Bu bilgileri eylemlerinizde kullanabilirsiniz." + }, + "PROJECTS": { + "TITLE": "Projeler", + "DESCRIPTION": "Bir proje, kullanıcılarınızı doğrulamak için kullanabileceğiniz bir veya daha fazla uygulamayı barındırır. Ayrıca kullanıcılarınızı projelerle yetkilendirebilirsiniz. Diğer organizasyonlardan kullanıcıların uygulamalarınıza giriş yapmasına izin vermek için onlara projenize erişim izni verin.

Bir proje bulamazsanız, proje sahibiyle veya ilgili haklara sahip biriyle iletişime geçerek erişim sağlayın.", + "OWNED": { + "TITLE": "Sahip Olunan Projeler", + "DESCRIPTION": "Bunlar sahip olduğunuz projelerdir. Bu proje ayarlarını, yetkilendirmelerini ve uygulamalarını yönetebilirsiniz." + }, + "GRANTED": { + "TITLE": "Verilen Projeler", + "DESCRIPTION": "Bunlar diğer organizasyonların size verdiği projelerdir. Verilen projelerle kullanıcılarınıza diğer organizasyonların uygulamalarına erişim sağlayabilirsiniz." + } + }, + "USERS": { + "TITLE": "Kullanıcılar", + "DESCRIPTION": "Kullanıcı, uygulamalarınıza erişebilen bir insan veya makinedir.", + "HUMANS": { + "TITLE": "Kullanıcılar", + "DESCRIPTION": "Kullanıcılar, giriş istemiyle bir tarayıcı oturumunda etkileşimli olarak kimlik doğrulaması yapar.", + "METADATA": "Kullanıcıya departman gibi özel öznitelikler ekleyin. Bu bilgileri eylemlerinizde kullanabilirsiniz." + }, + "MACHINES": { + "TITLE": "Servis Kullanıcıları", + "DESCRIPTION": "Servis Kullanıcıları, özel anahtar ile imzalanmış JWT bearer token kullanarak etkileşimsiz kimlik doğrulaması yapar. Ayrıca kişisel erişim belirteci de kullanabilirler.", + "METADATA": "Kullanıcıya kimlik doğrulayan sistem gibi özel öznitelikler ekleyin. Bu bilgileri eylemlerinizde kullanabilirsiniz." + }, + "SELF": { + "METADATA": "Kullanıcınıza departmanınız gibi özel öznitelikler ekleyin. Bu bilgileri organizasyonunuzun eylemlerinde kullanabilirsiniz." + } + }, + "AUTHORIZATIONS": { + "TITLE": "Yetkilendirmeler", + "DESCRIPTION": "Yetkilendirmeler, bir kullanıcının bir projeye erişim haklarını tanımlar. Bir kullanıcıya projeye erişim verebilir ve kullanıcının o proje içindeki rollerini tanımlayabilirsiniz." + }, + "ACTIONS": { + "TITLE": "Eylemler", + "DESCRIPTION": "Kullanıcılarınız ZITADEL'de kimlik doğrulaması yaparken gerçekleşen olaylarda özel kod çalıştırın. Süreçlerinizi otomatikleştirin, kullanıcılarınızın meta verilerini ve belirteçlerini zenginleştirin veya harici sistemleri bilgilendirin.", + "SCRIPTS": { + "TITLE": "Betikler", + "DESCRIPTION": "JavaScript kodunuzu bir kez yazın ve birden fazla akışta tetikleyin." + }, + "FLOWS": { + "TITLE": "Akışlar", + "DESCRIPTION": "Bir kimlik doğrulama akışı seçin ve eyleminizi bu akış içindeki belirli bir olayda tetikleyin." + }, + "ACTIONSTWO_NOTE": "Actions V2, Actions'ın yeni, geliştirilmiş bir sürümü artık kullanılabilir. Mevcut sürüm hala erişilebilir, ancak gelecekteki geliştirmemiz sonunda mevcut sürümü değiştirecek olan yeni sürüme odaklanacak." + }, + "SETTINGS": { + "INSTANCE": { + "TITLE": "Varsayılan Ayarlar", + "DESCRIPTION": "Tüm organizasyonlar için varsayılan ayarlar. Uygun izinlerle, bunların bazıları organizasyon ayarlarında geçersiz kılınabilir." + }, + "ORG": { + "TITLE": "Organizasyon Ayarları", + "DESCRIPTION": "Organizasyonunuzun ayarlarını özelleştirin." + }, + "FEATURES": { + "TITLE": "Özellik Ayarları", + "DESCRIPTION": "Örneğiniz için özelliklerin kilidini açın." + }, + "IDPS": { + "TITLE": "Kimlik Sağlayıcıları", + "DESCRIPTION": "Harici kimlik sağlayıcıları oluşturun ve etkinleştirin. Tanınmış bir sağlayıcı seçin veya tercih ettiğiniz herhangi bir OIDC, OAuth veya SAML uyumlu sağlayıcıyı yapılandırın. Bir JWT kimlik sağlayıcısı yapılandırarak mevcut JWT belirteçlerinizi federe kimlikler olarak bile kullanabilirsiniz.", + "NEXT": "Şimdi ne yapmalı?", + "SAML": { + "TITLE": "SAML Kimlik Sağlayıcınızı Yapılandırın", + "DESCRIPTION": "ZITADEL yapılandırıldı. Şimdi SAML Kimlik Sağlayıcınızın bazı yapılandırmalara ihtiyacı var. Çoğu sağlayıcı, tüm ZITADEL metadata XML'ini yüklemenize izin verir. Diğer sağlayıcılar yalnızca belirli URL'ler sağlamanızı ister, örneğin entity ID (metadata URL), Assertion Consumer Service (ACS) URL veya Single Logout URL." + }, + "CALLBACK": { + "TITLE": "{{ provider }} Kimlik Sağlayıcınızı Yapılandırın", + "DESCRIPTION": "ZITADEL'i yapılandırabilmeniz için önce bu URL'yi Kimlik Sağlayıcınıza ileterek kimlik doğrulamasından sonra tarayıcının ZITADEL'e geri yönlendirilmesini etkinleştirin." + }, + "JWT": { + "TITLE": "JWT'leri Federe Kimlikler Olarak Kullanın", + "DESCRIPTION": "JWT Kimlik Sağlayıcısı, mevcut JWT belirteçlerinizi federe kimlikler olarak kullanmanızı sağlar. Bu özellik, zaten JWT'ler için bir verici/issuer'ınız varsa kullanışlıdır. Bir JWT IdP ile, bu JWT'leri ZITADEL'de anında kullanıcı oluşturmak ve güncellemek için kullanabilirsiniz." + }, + "LDAP": { + "TITLE": "ZITADEL'i LDAP Kimlik Sağlayıcınıza Bağlanacak Şekilde Yapılandırın", + "DESCRIPTION": "LDAP sunucunuza bağlantı ayrıntılarını sağlayın ve LDAP özniteliklerinizin ZITADEL özniteliklerine eşlenmesini yapılandırın." + }, + "AUTOFILL": { + "TITLE": "Kullanıcı Verilerini Otomatik Doldur", + "DESCRIPTION": "Kullanıcılarınızın deneyimini iyileştirmek için bir eylem kullanın. ZITADEL kayıt formunu kimlik sağlayıcısından gelen değerlerle önceden doldurabilirsiniz." + }, + "ACTIVATE": { + "TITLE": "IdP'yi Etkinleştir", + "DESCRIPTION": "IdP'niz henüz aktif değil. Kullanıcılarınızın giriş yapmasına izin vermek için etkinleştirin." + } + }, + "PW_COMPLEXITY": { + "TITLE": "Şifre Karmaşıklığı", + "DESCRIPTION": "Karmaşıklık kuralları tanımlayarak kullanıcılarınızın güçlü şifreler kullanmasını sağlayın." + }, + "BRANDING": { + "TITLE": "Markalama", + "DESCRIPTION": "Giriş formunuzun görünümünü ve hissini özelleştirin. İşiniz bittiğinde yapılandırmanızı uygulamayı unutmayın." + }, + "PRIVACY_POLICY": { + "TITLE": "Harici Bağlantılar", + "DESCRIPTION": "Kullanıcılarınızı giriş sayfasında gösterilen özel harici kaynaklara yönlendirin. Kullanıcıların kaydolabilmeleri için Hizmet Şartları ve Gizlilik Politikasını kabul etmeleri gerekir. Belgelerinize olan bağlantıyı değiştirin veya belgeler düğmesini konsoldan gizlemek için boş bir dize ayarlayın. Konsolda özel bir harici bağlantı ve o bağlantı için özel bir metin ekleyin, veya bu düğmeyi gizlemek için boş bırakın." + }, + "SMTP_PROVIDER": { + "TITLE": "SMTP Ayarları", + "DESCRIPTION": "Kullanıcılarınızın tanıdığı ve güvendiği bir alan adını gönderen adresi için kullanmak üzere SMTP sunucunuzu yapılandırın." + }, + "SMS_PROVIDER": { + "TITLE": "SMS Ayarları", + "DESCRIPTION": "Tüm ZITADEL özelliklerinin kilidini açmak için kullanıcılarınıza SMS mesajları göndermek üzere Twilio'yu yapılandırın." + }, + "IAM_EVENTS": { + "TITLE": "Olaylar", + "DESCRIPTION": "Bu sayfa, örneğinizin denetim izi sınırına kadar geri giden tüm durum değişikliklerini gösterir. Hata ayıklama amacıyla listeyi zaman aralığına göre filtreleyin veya denetleme amacıyla bir agrega ile filtreleyin." + }, + "IAM_FAILED_EVENTS": { + "TITLE": "Başarısız Olaylar", + "DESCRIPTION": "Bu sayfa, örneğinizdeki tüm başarısız olayları gösterir. ZITADEL beklediğiniz gibi davranmıyorsa, her zaman önce bu listeyi kontrol edin." + }, + "IAM_VIEWS": { + "TITLE": "Görünümler", + "DESCRIPTION": "Bu sayfa tüm veritabanı görünümlerinizi ve en son olaylarını ne zaman işlediklerini gösterir. Bazı verileri kaçırıyorsanız, görünümün güncel olup olmadığını kontrol edin." + }, + "LANGUAGES": { + "TITLE": "Diller", + "DESCRIPTION": "Giriş formunun ve bildirim mesajlarının çevrildiği dilleri kısıtlayın. Bazı dilleri devre dışı bırakmak istiyorsanız, bunları İzin Verilmeyen Diller bölümüne sürükleyin. İzin verilen bir dili varsayılan dil olarak belirleyebilirsiniz. Kullanıcının tercih ettiği dile izin verilmiyorsa, varsayılan dil kullanılır." + }, + "SECRET_GENERATORS": { + "TITLE": "Gizli Üreticiler", + "DESCRIPTION": "Gizlilerinizin karmaşıklığını ve ömrünü tanımlayın. Daha yüksek karmaşıklık ve ömür güvenliği artırır, daha düşük karmaşıklık ve ömür şifre çözme performansını iyileştirir." + }, + "SECURITY": { + "TITLE": "Güvenlik Ayarları", + "DESCRIPTION": "Güvenlik etkisi olabilecek ZITADEL özelliklerini etkinleştirin. Bu ayarları değiştirmeden önce ne yaptığınızı gerçekten bilmelisiniz." + }, + "OIDC": { + "TITLE": "OpenID Connect Ayarları", + "DESCRIPTION": "OIDC belirteç yaşam sürelerinizi yapılandırın. Kullanıcılarınızın güvenliğini artırmak için daha kısa yaşam süreleri, kullanıcılarınızın rahatlığını artırmak için daha uzun yaşam süreleri kullanın.", + "LABEL_HOURS": "Saat Cinsinden Maksimum Yaşam Süresi", + "LABEL_DAYS": "Gün Cinsinden Maksimum Yaşam Süresi", + "ACCESS_TOKEN": { + "TITLE": "Access Token", + "DESCRIPTION": "Access token, bir kullanıcının kimlik doğrulaması için kullanılır. Kullanıcının verilerine erişmek için kullanılan kısa ömürlü bir token'dır. Yetkisiz erişim riskini en aza indirmek için kısa bir yaşam süresi kullanın. Access token'lar, refresh token kullanılarak otomatik olarak yenilenebilir." + }, + "ID_TOKEN": { + "TITLE": "ID Token", + "DESCRIPTION": "ID token, kullanıcı hakkında talepleri içeren bir JSON Web Token (JWT)'dir. ID token yaşam süresi, access token yaşam süresini aşmamalıdır." + }, + "REFRESH_TOKEN": { + "TITLE": "Refresh Token", + "DESCRIPTION": "Refresh token, yeni bir access token elde etmek için kullanılır. Access token'ı yenilemek için kullanılan uzun ömürlü bir token'dır. Refresh token süresi dolduğunda kullanıcının manuel olarak yeniden kimlik doğrulaması yapması gerekir." + }, + "REFRESH_TOKEN_IDLE": { + "TITLE": "Boşta Refresh Token", + "DESCRIPTION": "Boşta refresh token yaşam süresi, bir refresh token'ın kullanılmadan kalabileceği maksimum süredir." + } + }, + "WEB_KEYS": { + "DESCRIPTION": "ZITADEL örneğiniz için token'ları güvenli bir şekilde imzalamak ve doğrulamak için OIDC Web Anahtarlarınızı yönetin.", + "TABLE": { + "TITLE": "Aktif ve Gelecek Web Anahtarları", + "DESCRIPTION": "Aktif ve yaklaşan web anahtarlarınız. Yeni bir anahtar etkinleştirmek mevcut olanı devre dışı bırakacaktır.", + "NOTE": "Not: JWKs OIDC endpoint önbelleğe alınabilir bir yanıt döndürür (varsayılan 5 dk). Henüz önbelleklerde ve istemcilerde mevcut olmayabileceği için bir anahtarı çok erken etkinleştirmekten kaçının.", + "ACTIVATE": "Sonraki Web Anahtarını Etkinleştir", + "ACTIVE": "Şu anda aktif", + "NEXT": "Sırada", + "FUTURE": "Gelecek", + "WARNING": "Web Anahtarı 5 dakikadan daha yeni" + }, + "CREATE": { + "TITLE": "Yeni Web Anahtarı Oluştur", + "DESCRIPTION": "Yeni bir web anahtarı oluşturmak bunu listenize ekler. ZITADEL varsayılan olarak SHA256 hasher ile RSA2048 anahtarları kullanır.", + "KEY_TYPE": "Anahtar Türü", + "BITS": "Bit", + "HASHER": "Hasher", + "CURVE": "Eğri" + }, + "PREVIOUS_TABLE": { + "TITLE": "Önceki Web Anahtarları", + "DESCRIPTION": "Bunlar artık aktif olmayan önceki web anahtarlarınızdır.", + "DEACTIVATED_ON": "Devre dışı bırakıldığı tarih" + } + }, + "MESSAGE_TEXTS": { + "TITLE": "Mesaj Metinleri", + "DESCRIPTION": "Bildirim e-postası veya SMS mesajlarınızın metinlerini özelleştirin. Bazı dilleri devre dışı bırakmak istiyorsanız, bunları örnek dil ayarlarınızda kısıtlayın.", + "TYPE_DESCRIPTIONS": { + "DC": "Organizasyonunuz için bir alan adı talep ettiğinizde, giriş adlarında bu alan adını kullanmayan kullanıcılar, giriş adlarını talep edilen alan adıyla eşleşecek şekilde değiştirmeleri için uyarılacaktır.", + "INIT": "Bir kullanıcı oluşturulduğunda, şifrelerini belirlemek için bir bağlantı içeren bir e-posta alacaklardır.", + "PC": "Bir kullanıcı şifresini değiştirdiğinde, bildirim ayarlarında bunu etkinleştirdiyseniz değişiklik hakkında bir bildirim alacaklardır.", + "PL": "Bir kullanıcı şifresiz kimlik doğrulama yöntemi eklediğinde, bir e-postadaki bağlantıyı tıklayarak etkinleştirmeleri gerekir.", + "PR": "Bir kullanıcı şifresini sıfırladığında, yeni bir şifre belirlemek için bir bağlantı içeren bir e-posta alacaktır.", + "VE": "Bir kullanıcı e-posta adresini değiştirdiğinde, yeni adresi doğrulamak için bir bağlantı içeren bir e-posta alacaktır.", + "VP": "Bir kullanıcı telefon numarasını değiştirdiğinde, yeni numarayı doğrulamak için bir kod içeren bir SMS alacaktır.", + "VEO": "Bir kullanıcı e-posta yoluyla Tek Seferlik Şifre yöntemi eklediğinde, e-posta adreslerine gönderilen bir kodu girerek etkinleştirmeleri gerekir.", + "VSO": "Bir kullanıcı SMS yoluyla Tek Seferlik Şifre yöntemi eklediğinde, telefon numaralarına gönderilen bir kodu girerek etkinleştirmeleri gerekir.", + "IU": "Bir kullanıcı davet kodu oluşturulduğunda, kimlik doğrulama yöntemlerini belirlemek için bir bağlantı içeren bir e-posta alacaklardır." + } + }, + "LOGIN_TEXTS": { + "TITLE": "Giriş Arayüzü Metinleri", + "DESCRIPTION": "Giriş formunuzun metinlerini özelleştirin. Bir metin boşsa, yer tutucu varsayılan değeri gösterir. Bazı dilleri devre dışı bırakmak istiyorsanız, bunları örnek dil ayarlarınızda kısıtlayın." + }, + "DOMAINS": { + "TITLE": "Alan Adı Ayarları", + "DESCRIPTION": "Alan adlarınızda kısıtlamalar tanımlayın ve giriş adı desenlerinizi yapılandırın.", + "REQUIRE_VERIFICATION": { + "TITLE": "Özel alan adlarının doğrulanmasını gerektir", + "DESCRIPTION": "Bu etkinleştirilirse, organizasyon alan adları alan adı keşfi veya kullanıcı adı soneki için kullanılmadan önce doğrulanmalıdır." + }, + "LOGIN_NAME_PATTERN": { + "TITLE": "Giriş Adı Deseni", + "DESCRIPTION": "Kullanıcılarınızın giriş adlarının desenini kontrol edin. ZITADEL, kullanıcılarınız giriş adlarını girer girmez organizasyonlarını seçer. Bu nedenle giriş adlarının tüm organizasyonlar arasında benzersiz olması gerekir. Birden fazla alan adında hesabı olan kullanıcılarınız varsa, giriş adlarınızı organizasyon alan adıyla soneklendirerek benzersizliği sağlayabilirsiniz." + }, + "DOMAIN_VERIFICATION": { + "TITLE": "Alan Adı Doğrulama", + "DESCRIPTION": "Yalnızca organizasyonunuzun gerçekten kontrol ettiği alan adlarını kullanmasına izin verin. Etkinleştirilirse, organizasyon alan adları kullanılmadan önce DNS veya HTTP challenge ile periyodik olarak doğrulanır. Bu, alan adı ele geçirilmesini önlemek için bir güvenlik özelliğidir." + }, + "SMTP_SENDER_ADDRESS": { + "TITLE": "SMTP Gönderen Adresi", + "DESCRIPTION": "Yalnızca örnek alan adlarınızdan biriyle eşleşiyorsa bir SMTP gönderen adresine izin verin." + } + }, + "LOGIN": { + "LIFETIMES": { + "TITLE": "Giriş Yaşam Süreleri", + "DESCRIPTION": "Bazı giriş ile ilgili maksimum yaşam sürelerini azaltarak güvenliğinizi sağlamlaştırın.", + "LABEL": "Saat Cinsinden Maksimum Yaşam Süresi", + "PW_CHECK": { + "TITLE": "Şifre Kontrolü", + "DESCRIPTION": "Kullanıcılar bu süre sonra şifreleriyle yeniden kimlik doğrulaması yapmak zorunda kalacaklardır." + }, + "EXT_LOGIN_CHECK": { + "TITLE": "Harici Giriş Kontrolü", + "DESCRIPTION": "Kullanıcılarınız bu süre sonra harici kimlik sağlayıcılarına yönlendirilir." + }, + "MULTI_FACTOR_INIT": { + "TITLE": "Çoklu Faktör Başlatma Kontrolü", + "DESCRIPTION": "Kullanıcılarınız henüz yapmadılarsa bu süre sonra ikinci bir faktör veya Çoklu faktör kurmaları için uyarılacaktır. 0 yaşam süresi bu uyarıyı devre dışı bırakır." + }, + "SECOND_FACTOR_CHECK": { + "TITLE": "İkinci Faktör Kontrolü", + "DESCRIPTION": "Kullanıcılarınız bu sürelerde ikinci faktörlerini yeniden doğrulamak zorundadır." + }, + "MULTI_FACTOR_CHECK": { + "TITLE": "Çoklu Faktör Kontrolü", + "DESCRIPTION": "Kullanıcılarınız bu sürelerde Çoklu faktörlerini yeniden doğrulamak zorundadır." + } + }, + "FORM": { + "TITLE": "Giriş Formu", + "DESCRIPTION": "Giriş formunu özelleştirin.", + "USERNAME_PASSWORD_ALLOWED": { + "TITLE": "Kullanıcı adı ve Şifreye izin verildi", + "DESCRIPTION": "Kullanıcılarınızın kullanıcı adı ve şifreleriyle giriş yapmalarına izin verin. Bu devre dışı bırakılırsa, kullanıcılarınız yalnızca şifresiz kimlik doğrulama veya harici kimlik sağlayıcısı kullanarak giriş yapabilir." + }, + "USER_REGISTRATION_ALLOWED": { + "TITLE": "Kullanıcı Kaydına izin verildi", + "DESCRIPTION": "Anonim kullanıcıların hesap oluşturmasına izin verin." + }, + "ORG_REGISTRATION_ALLOWED": { + "TITLE": "Organizasyon Kaydına izin verildi", + "DESCRIPTION": "Anonim kullanıcıların organizasyon oluşturmasına izin verin." + }, + "EXTERNAL_LOGIN_ALLOWED": { + "TITLE": "Harici Girişe izin verildi", + "DESCRIPTION": "Kullanıcılarınızın giriş yapmak için ZITADEL kullanıcısını kullanmak yerine harici kimlik sağlayıcısıyla giriş yapmalarına izin verin." + }, + "HIDE_PASSWORD_RESET": { + "TITLE": "Şifre Sıfırlama gizlendi", + "DESCRIPTION": "Kullanıcılarınızın şifrelerini sıfırlamalarına izin vermeyin." + }, + "DOMAIN_DISCOVERY_ALLOWED": { + "TITLE": "Alan Adı Keşfine izin verildi", + "DESCRIPTION": "Kullanıcılarınızın giriş adlarının alan adına bağlı olarak organizasyonlarını bulun, örneğin e-posta adresleri." + }, + "IGNORE_UNKNOWN_USERNAMES": { + "TITLE": "Bilinmeyen Kullanıcı Adlarını yoksay", + "DESCRIPTION": "Bu etkinleştirilirse, giriş formu kullanıcı adı bilinmiyorsa hata mesajı göstermez. Bu, kullanıcı adı tahmin etmeyi önlemeye yardımcı olur." + }, + "DISABLE_EMAIL_LOGIN": { + "TITLE": "E-posta Girişini Devre Dışı Bırak", + "DESCRIPTION": "Bu etkinleştirilirse, kullanıcılarınız giriş yapmak için e-posta adreslerini kullanamaz. Bunu devre dışı bırakırsanız, giriş yapabilmek için kullanıcılarınızın e-posta adreslerinin tüm organizasyonlar arasında benzersiz olması gerektiğini unutmayın." + }, + "DISABLE_PHONE_LOGIN": { + "TITLE": "Telefon Girişini Devre Dışı Bırak", + "DESCRIPTION": "Bu etkinleştirilirse, kullanıcılarınız giriş yapmak için telefon numaralarını kullanamaz. Bunu devre dışı bırakırsanız, giriş yapabilmek için kullanıcılarınızın telefon numaralarının tüm organizasyonlar arasında benzersiz olması gerektiğini unutmayın." + } + } + } + } + }, + "PAGINATOR": { + "PREVIOUS": "Önceki", + "NEXT": "Sonraki", + "COUNT": "Toplam Sonuç", + "MORE": "Daha Fazla" + }, + "FOOTER": { + "LINKS": { + "CONTACT": "İletişim", + "TOS": "Hizmet Şartları", + "PP": "Gizlilik Politikası" + }, + "THEME": { + "DARK": "Koyu", + "LIGHT": "Açık" + } + }, + "HOME": { + "WELCOME": "ZITADEL ile başlayın", + "DISCLAIMER": "ZITADEL verilerinizi gizli ve güvenli bir şekilde işler.", + "DISCLAIMERLINK": "Daha fazla bilgi", + "DOCUMENTATION": { + "DESCRIPTION": "ZITADEL ile hızlıca başlayın." + }, + "GETSTARTED": { + "DESCRIPTION": "ZITADEL ile hızlıca başlayın." + }, + "QUICKSTARTS": { + "LABEL": "İlk Adımlar", + "DESCRIPTION": "ZITADEL ile hızlıca başlayın." + }, + "SHORTCUTS": { + "SHORTCUTS": "Kısayollar", + "SETTINGS": "Mevcut kısayollar", + "PROJECTS": "Projeler", + "REORDER": "Taşımak için kareyi basılı tutun ve sürükleyin", + "ADD": "Eklemek için bir kareyi basılı tutun ve sürükleyin" + } + }, + "ONBOARDING": { + "DESCRIPTION": "Sonraki adımlarınız", + "MOREDESCRIPTION": "daha fazla kısayol", + "COMPLETED": "tamamlandı", + "DISMISS": "Hayır teşekkürler, ben uzmanım.", + "CARD": { + "TITLE": "ZITADEL'inizi çalıştırın", + "DESCRIPTION": "Bu kontrol listesi örneğinizi kurmaya yardımcı olur ve en temel adımlarda size rehberlik eder" + }, + "MILESTONES": { + "instance.policy.label.added": { + "title": "Markanızı kurun", + "description": "Girişinizin renklendirmesini ve şeklini tanımlayın ve logonuzu ve simgelerinizi yükleyin.", + "action": "Markalamayı kur" + }, + "instance.smtp.config.added": { + "title": "SMTP ayarlarınızı kurun", + "description": "Kendi posta sunucusu ayarlarınızı belirleyin.", + "action": "SMTP'yi kur" + }, + "PROJECT_CREATED": { + "title": "Bir proje oluşturun", + "description": "Bir proje ekleyin ve rollerini ve yetkilendirmelerini tanımlayın.", + "action": "Proje oluştur" + }, + "APPLICATION_CREATED": { + "title": "Uygulamanızı kaydedin", + "description": "Web, native, api veya saml uygulamanızı kaydedin ve bir kimlik doğrulama akışı kurun.", + "action": "Uygulamayı kaydet" + }, + "AUTHENTICATION_SUCCEEDED_ON_APPLICATION": { + "title": "Uygulamanızda oturum açın", + "description": "Kimlik doğrulama için uygulamanızı ZITADEL ile entegre edin ve admin kullanıcınızla oturum açarak test edin.", + "action": "Oturum aç" + }, + "user.human.added": { + "title": "Kullanıcı ekleyin", + "description": "Uygulama kullanıcılarınızı ekleyin", + "action": "Kullanıcı ekle" + }, + "user.grant.added": { + "title": "Kullanıcılara yetki verin", + "description": "Kullanıcıların uygulamanıza erişmesine izin verin ve rollerini kurun.", + "action": "Kullanıcıya yetki ver" + } + } + }, + "MENU": { + "INSTANCE": "Varsayılan ayarlar", + "DASHBOARD": "Ana Sayfa", + "PERSONAL_INFO": "Kişisel Bilgiler", + "DOCUMENTATION": "Belgeler", + "INSTANCEOVERVIEW": "Örnek", + "ORGS": "Organizasyonlar", + "VIEWS": "Görünümler", + "EVENTS": "Olaylar", + "FAILEDEVENTS": "Başarısız Olaylar", + "ORGANIZATION": "Organizasyon", + "PROJECT": "Projeler", + "PROJECTOVERVIEW": "Genel Bakış", + "PROJECTGRANTS": "Yetkilendirmeler", + "ROLES": "Roller", + "GRANTEDPROJECT": "Verilen Projeler", + "HUMANUSERS": "Kullanıcılar", + "MACHINEUSERS": "Servis Kullanıcıları", + "LOGOUT": "Tüm Kullanıcıları Çıkış Yap", + "NEWORG": "Yeni Organizasyon", + "IAMADMIN": "IAM Yöneticisisiniz. Genişletilmiş izinleriniz olduğunu unutmayın.", + "SHOWORGS": "Tüm Organizasyonları Göster", + "GRANTS": "Yetkilendirmeler", + "ACTIONS": "Eylemler", + "PRIVACY": "Gizlilik", + "TOS": "Hizmet Şartları", + "OPENSHORTCUTSTOOLTIP": "Klavye kısayollarını göstermek için ? yazın", + "SETTINGS": "Ayarlar", + "CUSTOMERPORTAL": "Müşteri Portalı" + }, + "QUICKSTART": { + "TITLE": "ZITADEL'i uygulamanıza entegre edin", + "DESCRIPTION": "ZITADEL'i uygulamanıza entegre edin veya dakikalar içinde başlamak için örneklerimizden birini kullanın.", + "BTN_START": "Uygulama Oluştur", + "BTN_LEARNMORE": "Daha Fazla Öğren", + "CREATEPROJECTFORAPP": "Proje Oluştur {{value}}", + "SELECT_FRAMEWORK": "Framework Seç", + "FRAMEWORK": "Framework", + "FRAMEWORK_OTHER": "Diğer (OIDC, SAML, API)", + "ALMOSTDONE": "Neredeyse bitti.", + "REVIEWCONFIGURATION": "Yapılandırmayı Gözden Geçir", + "REVIEWCONFIGURATION_DESCRIPTION": "{{value}} uygulamaları için temel bir yapılandırma oluşturduk. Bu yapılandırmayı oluşturduktan sonra ihtiyaçlarınıza göre uyarlayabilirsiniz.", + "REDIRECTS": "Yönlendirmeleri yapılandır", + "DEVMODEWARN": "Dev Mode varsayılan olarak etkindir. Değerleri daha sonra üretim için güncelleyebilirsiniz.", + "GUIDE": "Kılavuz", + "BROWSEEXAMPLES": "Örnekleri ve SDK'ları Gözat", + "DUPLICATEAPPRENAME": "Aynı ada sahip bir uygulama zaten mevcut. Lütfen farklı bir ad seçin.", + "DIALOG": { + "CHANGE": { + "TITLE": "Framework Değiştir", + "DESCRIPTION": "Uygulamanızın hızlı kurulumu için mevcut frameworklerden birini seçin." + } + } + }, + "ACTIONS": { + "ACTIONS": "Eylemler", + "FILTER": "Filtrele", + "RENAME": "Yeniden Adlandır", + "SET": "Ayarla", + "COPY": "Panoya Kopyala", + "COPIED": "Panoya kopyalandı.", + "RESET": "Sıfırla", + "RESETDEFAULT": "Varsayılana Sıfırla", + "RESETTO": "Şuna sıfırla: ", + "RESETCURRENT": "Geçerli duruma sıfırla", + "SHOW": "Göster", + "HIDE": "Gizle", + "SAVE": "Kaydet", + "SAVENOW": "Şimdi kaydet", + "NEW": "Yeni", + "ADD": "Ekle", + "CREATE": "Oluştur", + "CONTINUE": "Devam Et", + "CONTINUEWITH": "{{value}} ile devam et", + "BACK": "Geri", + "CLOSE": "Kapat", + "CLEAR": "Temizle", + "CANCEL": "İptal", + "INFO": "Bilgi", + "OK": "Tamam", + "SELECT": "Seç", + "VIEW": "Göster", + "SELECTIONDELETE": "Seçimi sil", + "DELETE": "Sil", + "REMOVE": "Kaldır", + "VERIFY": "Doğrula", + "FINISH": "Bitir", + "FINISHED": "Kapat", + "CHANGE": "Değiştir", + "REACTIVATE": "Yeniden Etkinleştir", + "ACTIVATE": "Etkinleştir", + "DEACTIVATE": "Devre Dışı Bırak", + "REFRESH": "Yenile", + "LOGIN": "Giriş", + "EDIT": "Düzenle", + "PIN": "Sabitle / Sabitlemeyi Kaldır", + "CONFIGURE": "Yapılandır", + "SEND": "Gönder", + "NEWVALUE": "Yeni Değer", + "RESTORE": "Geri Yükle", + "CONTINUEWITHOUTSAVE": "Kaydetmeden devam et", + "OF": "kadarının", + "PREVIOUS": "Önceki", + "NEXT": "Sonraki", + "MORE": "daha fazla", + "STEP": "Adım", + "SETUP": "Kurulum", + "TEST": "Test", + "UNSAVEDCHANGES": "Kaydedilmemiş değişiklikler", + "UNSAVED": { + "DIALOG": { + "DESCRIPTION": "Bu yeni eylemi atmak istediğinizden emin misiniz? Eyleminiz kaybolacak", + "CANCEL": "İptal", + "DISCARD": "At" + } + }, + "TABLE": { + "SHOWUSER": "Kullanıcı {{value}}'ı göster" + }, + "DOWNLOAD": "İndir", + "APPLY": "Uygula" + }, + "ACTIONSTWO": { + "BETA_NOTE": "Şu anda beta aşamasında olan yeni Actions V2'yi kullanıyorsunuz. Önceki Sürüm 1 hala mevcut ancak gelecekte kullanımdan kaldırılacaktır. Lütfen sorunları veya geri bildirimleri bildirin.", + "EXECUTION": { + "TITLE": "Eylemler", + "DESCRIPTION": "Eylemler, API isteklerine, olaylara veya belirli işlevlere yanıt olarak özel kod çalıştırmanıza olanak tanır. Zitadel'i genişletmek, iş akışlarını otomatikleştirmek ve diğer sistemlerle entegre olmak için bunları kullanın.", + "TYPES": { + "request": "İstek", + "response": "Yanıt", + "event": "Olaylar", + "function": "İşlev" + }, + "DIALOG": { + "CREATE_TITLE": "Bir Eylem Oluştur", + "UPDATE_TITLE": "Bir Eylemi Güncelle", + "TYPE": { + "DESCRIPTION": "Bu Eylemin ne zaman çalışmasını istediğinizi seçin", + "REQUEST": { + "TITLE": "İstek", + "DESCRIPTION": "Zitadel içinde gerçekleşen istekler. Bu, giriş isteği çağrısı gibi bir şey olabilir." + }, + "RESPONSE": { + "TITLE": "Yanıt", + "DESCRIPTION": "Zitadel içindeki bir istekten gelen yanıt. Bir kullanıcı getirirken aldığınız yanıtı düşünün." + }, + "EVENTS": { + "TITLE": "Olaylar", + "DESCRIPTION": "Zitadel içinde gerçekleşen olaylar. Bu, bir kullanıcının hesap oluşturması, başarılı bir giriş vb. gibi herhangi bir şey olabilir." + }, + "FUNCTIONS": { + "TITLE": "İşlevler", + "DESCRIPTION": "Zitadel içinde çağırabileceğiniz işlevler. Bu, e-posta gönderme ile kullanıcı oluşturma arasında herhangi bir şey olabilir." + } + }, + "CONDITION": { + "REQ_RESP_DESCRIPTION": "Bu eylemin tüm istekler, belirli bir hizmet (örn. kullanıcı yönetimi) veya tek bir istek (örn. kullanıcı oluştur) için geçerli olup olmadığını seçin.", + "ALL": { + "TITLE": "Tümü", + "DESCRIPTION": "Eyleminizi her istekte çalıştırmak istiyorsanız bunu seçin" + }, + "ALL_EVENTS": "Eyleminizi her olayda çalıştırmak istiyorsanız bunu seçin", + "SELECT_SERVICE": { + "TITLE": "Hizmet Seç", + "DESCRIPTION": "Eyleminiz için bir Zitadel Hizmeti seçin." + }, + "SELECT_METHOD": { + "TITLE": "Yöntem Seç", + "DESCRIPTION": "Yalnızca belirli bir istekte yürütmek istiyorsanız, burada seçin", + "NOTE": "Bir yöntem seçmezseniz, eyleminiz seçilen hizmetinizde her istekte çalışacaktır." + }, + "FUNCTIONNAME": { + "TITLE": "İşlev Adı", + "DESCRIPTION": "Yürütmek istediğiniz işlevi seçin" + }, + "SELECT_GROUP": { + "TITLE": "Grup Ayarla", + "DESCRIPTION": "Yalnızca bir olay grubunda yürütmek istiyorsanız, grubu buraya ayarlayın" + }, + "SELECT_EVENT": { + "TITLE": "Olay Seç", + "DESCRIPTION": "Yalnızca belirli bir olayda yürütmek istiyorsanız, burada belirtin" + } + }, + "TARGET": { + "DESCRIPTION": "Bir hedef yürütmeyi seçebilir veya diğer hedeflerle aynı koşullarda çalıştırabilirsiniz.", + "TARGET": { + "DESCRIPTION": "Bu eylem için yürütmek istediğiniz hedef" + }, + "CONDITIONS": { + "DESCRIPTION": "Yürütme Koşulları" + } + } + }, + "TABLE": { + "CONDITION": "Koşul", + "TYPE": "Tür", + "TARGET": "Hedef", + "CREATIONDATE": "Oluşturma Tarihi" + } + }, + "TARGET": { + "TITLE": "Hedefler", + "DESCRIPTION": "Hedef, bir eylemden yürütmek istediğiniz kodun varış noktasıdır. Burada bir hedef oluşturun ve eylemlerinize ekleyin.", + "CREATE": { + "TITLE": "Hedefinizi Oluşturun", + "DESCRIPTION": "Zitadel dışında kendi hedefinizi oluşturun", + "NAME": "Ad", + "NAME_DESCRIPTION": "Daha sonra kolayca tanımlanabilmesi için hedefinize açık, açıklayıcı bir ad verin", + "TYPE": "Tür", + "TYPES": { + "restWebhook": "REST Webhook", + "restCall": "REST Call", + "restAsync": "REST Async" + }, + "TYPES_DESCRIPTION": "Webhook, çağrı durum kodunu işler ancak yanıt önemsizdir\nCall, çağrı durum kodunu ve yanıtını işler\nAsync, çağrı ne durum kodunu ne de yanıtını işler, ancak diğer Hedeflerle paralel olarak çağrılabilir", + "ENDPOINT": "Endpoint", + "ENDPOINT_DESCRIPTION": "Kodunuzun barındırıldığı endpoint'i girin. Bize erişilebilir olduğundan emin olun!", + "TIMEOUT": "Zaman Aşımı", + "TIMEOUT_DESCRIPTION": "Hedefinizin yanıt vermesi için maksimum süreyi ayarlayın. Daha uzun sürerse, isteği durduracağız.", + "INTERRUPT_ON_ERROR": "Hata Durumunda Kes", + "INTERRUPT_ON_ERROR_DESCRIPTION": "Hedefler hatayla döndüğünde tüm yürütmeleri durdur", + "INTERRUPT_ON_ERROR_WARNING": "Caution: “Interrupt on Error” halts operations on failure, risking lockout. Test with it disabled to prevent blocking login/creation.", + "AWAIT_RESPONSE": "Yanıt Bekle", + "AWAIT_RESPONSE_DESCRIPTION": "Başka bir şey yapmadan önce yanıt bekleyeceğiz. Tek bir eylem için birden fazla hedef kullanmayı planlıyorsanız kullanışlıdır" + }, + "TABLE": { + "NAME": "Ad", + "ENDPOINT": "Endpoint", + "CREATIONDATE": "Oluşturma Tarihi", + "REORDER": "Yeniden Sırala" + } + } + }, + "MEMBERROLES": { + "IAM_OWNER": "Tüm organizasyonlar dahil olmak üzere tüm örnek üzerinde kontrole sahiptir", + "IAM_OWNER_VIEWER": "Tüm organizasyonlar dahil olmak üzere tüm örneği gözden geçirme iznine sahiptir", + "IAM_ORG_MANAGER": "Organizasyonları oluşturma ve yönetme iznine sahiptir", + "IAM_USER_MANAGER": "Kullanıcıları oluşturma ve yönetme iznine sahiptir", + "IAM_ADMIN_IMPERSONATOR": "Tüm organizasyonlardan admin ve son kullanıcıların kimliğine bürünme iznine sahiptir", + "IAM_END_USER_IMPERSONATOR": "Tüm organizasyonlardan son kullanıcıların kimliğine bürünme iznine sahiptir", + "IAM_LOGIN_CLIENT": "Giriş istemcilerini yönetme iznine sahiptir", + "ORG_OWNER": "Tüm organizasyon üzerinde izne sahiptir", + "ORG_USER_MANAGER": "Organizasyonun kullanıcılarını oluşturma ve yönetme iznine sahiptir", + "ORG_OWNER_VIEWER": "Tüm organizasyonu gözden geçirme iznine sahiptir", + "ORG_SETTINGS_MANAGER": "Organizasyon ayarlarını yönetme iznine sahiptir", + "ORG_USER_PERMISSION_EDITOR": "Kullanıcı yetkilerini yönetme iznine sahiptir", + "ORG_PROJECT_PERMISSION_EDITOR": "Proje yetkilerini yönetme iznine sahiptir", + "ORG_PROJECT_CREATOR": "Kendi projelerini ve altyapı ayarlarını oluşturma iznine sahiptir", + "ORG_ADMIN_IMPERSONATOR": "Organizasyondan admin ve son kullanıcıların kimliğine bürünme iznine sahiptir", + "ORG_END_USER_IMPERSONATOR": "Organizasyondan son kullanıcıların kimliğine bürünme iznine sahiptir", + "ORG_USER_SELF_MANAGER": "Kendi kullanıcısını yönetme iznine sahiptir", + "PROJECT_OWNER": "Tüm proje üzerinde izne sahiptir", + "PROJECT_OWNER_VIEWER": "Tüm projeyi gözden geçirme iznine sahiptir", + "PROJECT_OWNER_GLOBAL": "Tüm proje üzerinde izne sahiptir", + "PROJECT_OWNER_VIEWER_GLOBAL": "Tüm projeyi gözden geçirme iznine sahiptir", + "PROJECT_GRANT_OWNER": "Proje yetkisini yönetme iznine sahiptir", + "PROJECT_GRANT_OWNER_VIEWER": "Proje yetkisini gözden geçirme iznine sahiptir" + }, + "OVERLAYS": { + "ORGSWITCHER": { + "TEXT": "Konsoldaki tüm organizasyon ayarları ve tabloları seçilen bir organizasyona dayanır. Organizasyon değiştirmek veya yeni bir tane oluşturmak için bu düğmeye tıklayın." + }, + "INSTANCE": { + "TEXT": "Varsayılan ayarlara ulaşmak için buraya tıklayın. Bu düğmeye yalnızca gelişmiş izinleriniz varsa erişebildiğinizi unutmayın." + }, + "PROFILE": { + "TEXT": "Burada kullanıcı hesaplarınız arasında geçiş yapabilir ve oturumlarınızı ve profilinizi yönetebilirsiniz." + }, + "NAV": { + "TEXT": "Bu navigasyon yukarıda seçili organizasyonunuza veya örneğinize göre değişir" + }, + "CONTEXTCHANGED": { + "TEXT": "Organizasyon bağlamı değişti." + }, + "SWITCHEDTOINSTANCE": { + "TEXT": "Görünüm az önce örneğe değişti!" + } + }, + "FILTER": { + "TITLE": "Filtre", + "ORGNAME": "Organizasyon Adı", + "ORGID": "Organizasyon ID", + "STATE": "Durum", + "PRIMARYDOMAIN": "Birincil Alan Adı", + "DISPLAYNAME": "Kullanıcı Görünen Adı", + "EMAIL": "E-posta", + "USERNAME": "Kullanıcı Adı", + "PROJECTNAME": "Proje Adı", + "RESOURCEOWNER": "Kaynak Sahibi", + "METHODS": { + "5": "içerir", + "7": "ile biter", + "1": "eşittir" + } + }, + "KEYBOARDSHORTCUTS": { + "TITLE": "Klavye Kısayolları", + "UNDERORGCONTEXT": "Organizasyon sayfaları içinde", + "SIDEWIDE": "Site geneli kısayollar", + "SHORTCUTS": { + "HOME": "Ana Sayfaya git", + "INSTANCE": "Örneğe git", + "ORG": "Organizasyona git", + "ORGSETTINGS": "Organizasyon Ayarlarına git", + "ORGSWITCHER": "Organizasyon Değiştir", + "ME": "Kendi profiline git", + "PROJECTS": "Projelere git", + "USERS": "Kullanıcılara git", + "USERGRANTS": "Yetkilendirmelere git", + "ACTIONS": "Eylemler ve Akışlara git", + "DOMAINS": "Alan Adlarına git" + } + }, + "RESOURCEID": "Kaynak ID", + "NAME": "Ad", + "VERSION": "Sürüm", + "TABLE": { + "NOROWS": "Veri yok" + }, + "ERRORS": { + "REQUIRED": "Lütfen bu alanı doldurun.", + "ATLEASTONE": "En az bir değer sağlayın.", + "TOKENINVALID": { + "TITLE": "Yetkilendirme token'ınızın süresi doldu.", + "DESCRIPTION": "Tekrar giriş yapmak için aşağıdaki düğmeye tıklayın." + }, + "EXHAUSTED": { + "TITLE": "Örneğiniz engellendi.", + "DESCRIPTION": "ZITADEL örnek yöneticinizden aboneliği güncellemesini isteyin." + }, + "INVALID_FORMAT": "Biçimlendirme geçersiz.", + "NOTANEMAIL": "Verilen değer bir e-posta adresi değil.", + "MINLENGTH": "En az {{requiredLength}} karakter uzunluğunda olmalıdır.", + "MAXLENGTH": "{{requiredLength}} karakterden az olmalıdır.", + "UPPERCASEMISSING": "Büyük harf içermelidir.", + "LOWERCASEMISSING": "Küçük harf içermelidir.", + "SYMBOLERROR": "Bir sembol veya noktalama işareti içermelidir.", + "NUMBERERROR": "Bir rakam içermelidir.", + "PWNOTEQUAL": "Sağlanan şifreler eşleşmiyor.", + "PHONE": "Telefon numarası + ile başlamalıdır." + }, + "USER": { + "SETTINGS": { + "TITLE": "Ayarlar", + "GENERAL": "Genel", + "IDP": "Kimlik Sağlayıcıları", + "SECURITY": "Şifre ve Güvenlik", + "KEYS": "Anahtarlar", + "PAT": "Kişisel Erişim Token'ları", + "USERGRANTS": "Yetkilendirmeler", + "MEMBERSHIPS": "Üyelikler", + "METADATA": "Meta Veri" + }, + "TITLE": "Kişisel Bilgiler", + "DESCRIPTION": "Bilgilerinizi ve güvenlik ayarlarınızı yönetin.", + "PAGES": { + "TITLE": "Kullanıcı", + "DETAIL": "Detay", + "CREATE": "Oluştur", + "MY": "Bilgilerim", + "LOGINNAMES": "Giriş adları", + "LOGINMETHODS": "Giriş yöntemleri", + "LOGINNAMESDESC": "Bunlar giriş adlarınızdır:", + "NOUSER": "İlişkili kullanıcı yok.", + "REACTIVATE": "Yeniden Etkinleştir", + "DEACTIVATE": "Devre Dışı Bırak", + "FILTER": "Filtre", + "STATE": "Durum", + "DELETE": "Kullanıcıyı Sil", + "UNLOCK": "Kullanıcı Kilidini Aç", + "GENERATESECRET": "İstemci Gizli Anahtarı Oluştur", + "REMOVESECRET": "İstemci Gizli Anahtarını Kaldır", + "LOCKEDDESCRIPTION": "Bu kullanıcı maksimum giriş denemelerini aştığı için kilitlendi ve tekrar kullanılmak için kilidinin açılması gerekiyor.", + "DELETEACCOUNT": "Hesabı Sil", + "DELETEACCOUNT_DESC": "Bu eylemi gerçekleştirirseniz, oturumunuz kapatılacak ve artık hesabınıza erişeminiz olmayacaktır. Bu eylem geri alınamaz, bu nedenle lütfen dikkatli devam edin.", + "DELETEACCOUNT_BTN": "Hesabı Sil", + "DELETEACCOUNT_SUCCESS": "Hesap başarıyla silindi!" + }, + "DETAILS": { + "DATECREATED": "Oluşturuldu", + "DATECHANGED": "Değiştirildi" + }, + "DIALOG": { + "DELETE_TITLE": "Kullanıcıyı Sil", + "DELETE_SELF_TITLE": "Hesabı Sil", + "DELETE_DESCRIPTION": "Bir kullanıcıyı kalıcı olarak silmek üzeresiniz. Emin misiniz?", + "DELETE_SELF_DESCRIPTION": "Kişisel hesabınızı kalıcı olarak silmek üzeresiniz. Bu, oturumunuzu kapatacak ve kullanıcınızı silecektir. Bu eylem geri alınamaz!", + "DELETE_AUTH_DESCRIPTION": "Kişisel hesabınızı kalıcı olarak silmek üzeresiniz. Emin misiniz?", + "TYPEUSERNAME": "Kullanıcıyı onaylamak ve silmek için '{{value}}' yazın.", + "USERNAME": "Giriş adı", + "DELETE_BTN": "Kalıcı olarak sil" + }, + "SENDEMAILDIALOG": { + "TITLE": "E-posta Bildirimi Gönder", + "DESCRIPTION": "Mevcut e-posta adresine bir bildirim göndermek için aşağıdaki düğmeye tıklayın veya alandaki e-posta adresini değiştirin.", + "NEWEMAIL": "Yeni e-posta adresi" + }, + "SECRETDIALOG": { + "CLIENTSECRET": "İstemci Gizli Anahtarı", + "CLIENTSECRET_DESCRIPTION": "İstemci gizli anahtarınızı güvenli bir yerde saklayın çünkü dialog kapatıldığında kaybolacaktır." + }, + "TABLE": { + "DEACTIVATE": "Devre Dışı Bırak", + "ACTIVATE": "Etkinleştir", + "CHANGEDATE": "Son Değiştirilme", + "CREATIONDATE": "Oluşturulma Zamanı", + "FILTER": { + "0": "Görünen Ad için filtrele", + "1": "Kullanıcı Adı için filtrele", + "2": "Görünen Ad için filtrele", + "3": "Kullanıcı Adı için filtrele", + "4": "E-posta için filtrele", + "5": "Görünen Ad için filtrele", + "10": "organizasyon adı için filtrele", + "12": "proje adı için filtrele" + }, + "EMPTY": "Girdi yok" + }, + "PASSWORDLESS": { + "SEND": "Kayıt bağlantısı gönder", + "TABLETYPE": "Tür", + "TABLESTATE": "Durum", + "NAME": "Ad", + "EMPTY": "Cihaz ayarlanmamış", + "TITLE": "Şifresiz Kimlik Doğrulama", + "DESCRIPTION": "ZITADEL'e şifresiz giriş yapmak için WebAuthn tabanlı Kimlik Doğrulama Yöntemleri ekleyin.", + "MANAGE_DESCRIPTION": "Kullanıcılarınızın ikinci faktör yöntemlerini yönetin.", + "U2F": "Yöntem ekle", + "U2F_DIALOG_TITLE": "Kimlik doğrulayıcıyı doğrula", + "U2F_DIALOG_DESCRIPTION": "Kullandığınız şifresiz Giriş için bir ad girin", + "U2F_SUCCESS": "Şifresiz Kimlik Doğrulama başarıyla oluşturuldu!", + "U2F_ERROR": "Kurulum sırasında bir hata oluştu!", + "U2F_NAME": "Kimlik Doğrulayıcı Adı", + "TYPE": { + "0": "MFA tanımlanmamış", + "1": "Tek Seferlik Şifre (OTP)", + "2": "Parmak izi, Güvenlik Anahtarları, Face ID ve diğer" + }, + "STATE": { + "0": "Durum Yok", + "1": "Hazır Değil", + "2": "Hazır", + "3": "Silindi" + }, + "DIALOG": { + "DELETE_TITLE": "Şifresiz Kimlik Doğrulama Yöntemini Kaldır", + "DELETE_DESCRIPTION": "Bir şifresiz Kimlik Doğrulama yöntemini silmek üzeresiniz. Emin misiniz?", + "ADD_TITLE": "Şifresiz Kimlik Doğrulama", + "ADD_DESCRIPTION": "Şifresiz kimlik doğrulama yöntemi oluşturmak için mevcut seçeneklerden birini seçin.", + "SEND_DESCRIPTION": "E-posta adresinize bir kayıt bağlantısı gönderin.", + "SEND": "Kayıt bağlantısı gönder", + "SENT": "E-posta başarıyla teslim edildi. Kuruluma devam etmek için posta kutunuzu kontrol edin.", + "QRCODE_DESCRIPTION": "Başka bir cihazla tarama için QR kodu oluşturun.", + "QRCODE": "QR kodu oluştur", + "QRCODE_SCAN": "Cihazınızda kuruluma devam etmek için bu QR kodunu tarayın.", + "NEW_DESCRIPTION": "Şifresiz kurmak için bu cihazı kullanın.", + "NEW": "Yeni Ekle" + } + }, + "MFA": { + "TABLETYPE": "Tür", + "TABLESTATE": "Durum", + "NAME": "Ad", + "EMPTY": "Ek faktör yok", + "TITLE": "Çok Faktörlü Kimlik Doğrulama", + "DESCRIPTION": "Hesabınız için optimal güvenliği sağlamak için ikinci bir faktör ekleyin.", + "MANAGE_DESCRIPTION": "Kullanıcılarınızın ikinci faktör yöntemlerini yönetin.", + "ADD": "Faktör Ekle", + "OTP": "TOTP için Kimlik Doğrulayıcı Uygulaması (Zamana Dayalı Tek Seferlik Şifre)", + "OTP_DIALOG_TITLE": "OTP Ekle", + "OTP_DIALOG_DESCRIPTION": "QR kodunu bir kimlik doğrulayıcı uygulamayla tarayın ve OTP yöntemini doğrulamak ve etkinleştirmek için aşağıya kodu girin.", + "U2F": "Parmak izi, Güvenlik Anahtarları, Face ID ve diğer", + "U2F_DIALOG_TITLE": "Faktörü Doğrula", + "U2F_DIALOG_DESCRIPTION": "Kullandığınız evrensel Çok faktör için bir ad girin.", + "U2F_SUCCESS": "Faktör başarıyla eklendi!", + "U2F_ERROR": "Kurulum sırasında bir hata oluştu!", + "U2F_NAME": "Kimlik Doğrulayıcı Adı", + "OTPSMS": "SMS ile OTP (Tek Seferlik Şifre)", + "OTPEMAIL": "E-posta ile OTP (Tek Seferlik Şifre)", + "SETUPOTPSMSDESCRIPTION": "Bu telefon numarasını OTP (Tek seferlik şifre) ikinci faktörü olarak kurmak istiyor musunuz?", + "OTPSMSSUCCESS": "OTP faktörü başarıyla kuruldu.", + "OTPSMSPHONEMUSTBEVERIFIED": "Bu yöntemi kullanabilmek için telefonunuzun doğrulanması gerekir.", + "OTPEMAILSUCCESS": "OTP faktörü başarıyla kuruldu.", + "TYPE": { + "0": "MFA tanımlanmamış", + "1": "Tek Seferlik Şifre (OTP)", + "2": "Parmak izi, Güvenlik Anahtarları, Face ID ve diğer" + }, + "STATE": { + "0": "Durum Yok", + "1": "Hazır Değil", + "2": "Hazır", + "3": "Silindi" + }, + "DIALOG": { + "MFA_DELETE_TITLE": "İkinci faktörü kaldır", + "MFA_DELETE_DESCRIPTION": "Bir ikinci faktörü silmek üzeresiniz. Emin misiniz?", + "ADD_MFA_TITLE": "İkinci Faktör Ekle", + "ADD_MFA_DESCRIPTION": "Aşağıdaki seçeneklerden birini seçin." + } + }, + "EXTERNALIDP": { + "TITLE": "Harici Kimlik Sağlayıcıları", + "DESC": "", + "IDPCONFIGID": "IDP Yapılandırma ID", + "IDPNAME": "IDP Adı", + "USERDISPLAYNAME": "Harici Ad", + "EXTERNALUSERID": "Harici Kullanıcı ID", + "EMPTY": "Harici IdP bulunamadı", + "DIALOG": { + "DELETE_TITLE": "IdP'yi Kaldır", + "DELETE_DESCRIPTION": "Bir kullanıcıdan Kimlik Sağlayıcısını silmek üzeresiniz. Gerçekten devam etmek istiyor musunuz?" + } + }, + "CREATE": { + "TITLE": "Yeni Bir Kullanıcı Oluşturun", + "DESCRIPTION": "Lütfen gerekli bilgileri sağlayın.", + "NAMEANDEMAILSECTION": "Ad ve E-posta", + "GENDERLANGSECTION": "Cinsiyet ve Dil", + "PHONESECTION": "Telefon numaraları", + "PASSWORDSECTION": "Başlangıç Şifresi", + "ADDRESSANDPHONESECTION": "Telefon numarası", + "INITMAILDESCRIPTION": "Her iki seçenek de seçilirse, başlatma için e-posta gönderilmez. Seçeneklerden yalnızca biri seçilirse, veri sağlamak / doğrulamak için bir posta gönderilir.", + "SETUPAUTHENTICATIONLATER": "Bu Kullanıcı için kimlik doğrulamayı daha sonra kur.", + "INVITATION": "Kimlik doğrulama kurulumu ve E-posta doğrulaması için davet E-postası gönder.", + "INITIALPASSWORD": "Kullanıcı için başlangıç şifresi belirle." + }, + "CODEDIALOG": { + "TITLE": "Telefon Numarasını Doğrula", + "DESCRIPTION": "Telefon numaranızı doğrulamak için kısa mesajla aldığınız kodu girin.", + "CODE": "Kod" + }, + "DATA": { + "STATE": "Durum", + "STATE0": "Bilinmeyen", + "STATE1": "Aktif", + "STATE2": "Pasif", + "STATE3": "Silindi", + "STATE4": "Kilitli", + "STATE5": "Askıya Alındı", + "STATE6": "Başlangıç" + }, + "PROFILE": { + "TITLE": "Profil", + "EMAIL": "E-posta", + "PHONE": "Telefon numarası", + "PHONE_HINT": "+ sembolünü ve ardından ülke arama kodunu kullanın veya açılır menüden ülkeyi seçin ve son olarak telefon numarasını girin", + "PHONE_VERIFIED": "Telefon Numarası Doğrulandı", + "SEND_SMS": "Doğrulama SMS'i Gönder", + "SEND_EMAIL": "E-posta Gönder", + "USERNAME": "Kullanıcı Adı", + "CHANGEUSERNAME": "değiştir", + "CHANGEUSERNAME_TITLE": "Kullanıcı adını değiştir", + "CHANGEUSERNAME_DESC": "Aşağıdaki alana yeni adı girin.", + "FIRSTNAME": "Ad", + "LASTNAME": "Soyad", + "NICKNAME": "Takma Ad", + "DISPLAYNAME": "Görünen Ad", + "PREFERREDLOGINNAME": "Tercih edilen giriş adı", + "PREFERRED_LANGUAGE": "Dil", + "GENDER": "Cinsiyet", + "PASSWORD": "Şifre", + "AVATAR": { + "UPLOADTITLE": "Profil Resminizi Yükleyin", + "UPLOADBTN": "Dosya seç", + "UPLOAD": "Yükle", + "CURRENT": "Mevcut Resim", + "PREVIEW": "Önizleme", + "DELETESUCCESS": "Başarıyla silindi!", + "CROPPERERROR": "Dosyanızı yüklerken bir hata oluştu. Gerekirse farklı bir format ve boyut deneyin." + }, + "COUNTRY": "Ülke" + }, + "MACHINE": { + "TITLE": "Servis Kullanıcı Detayları", + "USERNAME": "Kullanıcı Adı", + "NAME": "Ad", + "DESCRIPTION": "Açıklama", + "KEYSTITLE": "Anahtarlar", + "KEYSDESC": "Anahtarlarınızı tanımlayın ve isteğe bağlı bir son kullanma tarihi ekleyin.", + "TOKENSTITLE": "Kişisel Erişim Token'ları", + "TOKENSDESC": "Kişisel erişim token'ları sıradan OAuth erişim token'ları gibi çalışır.", + "ID": "Anahtar ID", + "TYPE": "Tür", + "EXPIRATIONDATE": "Son kullanma tarihi", + "CHOOSEDATEAFTER": "Sonrasında geçerli bir son kullanma tarihi girin", + "CHOOSEEXPIRY": "Bir son kullanma tarihi seçin", + "CREATIONDATE": "Oluşturma Tarihi", + "KEYDETAILS": "Anahtar Detayları", + "ACCESSTOKENTYPE": "Access Token Türü", + "ACCESSTOKENTYPES": { + "0": "Bearer", + "1": "JWT" + }, + "ADD": { + "TITLE": "Anahtar Ekle", + "DESCRIPTION": "Anahtar türünüzü seçin ve isteğe bağlı bir son kullanma tarihi seçin." + }, + "ADDED": { + "TITLE": "Anahtar oluşturuldu", + "DESCRIPTION": "Anahtarı indirin çünkü bu dialog kapatıldıktan sonra görünmeyecektir!" + }, + "KEYTYPES": { + "1": "JSON" + }, + "DIALOG": { + "DELETE_KEY": { + "TITLE": "Anahtarı Sil", + "DESCRIPTION": "Seçilen anahtarı silmek istiyor musunuz? Bu geri alınamaz." + } + } + }, + "PASSWORD": { + "TITLE": "Şifre", + "LABEL": "Güvenli bir şifre hesabı korumaya yardımcı olur", + "DESCRIPTION": "Aşağıdaki politikaya göre yeni şifreyi girin.", + "OLD": "Mevcut Şifre", + "NEW": "Yeni Şifre", + "CONFIRM": "Yeni Şifreyi Onayla", + "NEWINITIAL": "Şifre", + "CONFIRMINITIAL": "Şifreyi Onayla", + "RESET": "Mevcut Şifreyi Sıfırla", + "SET": "Yeni Şifre Belirle", + "RESENDNOTIFICATION": "Şifre Sıfırlama Bağlantısı Gönder", + "REQUIRED": "Bazı gerekli alanlar eksik.", + "MINLENGTHERROR": "En az {{value}} karakter uzunluğunda olmalıdır.", + "MAXLENGTHERROR": "{{value}} karakterden az olmalıdır." + }, + "ID": "ID", + "EMAIL": "E-posta", + "PHONE": "Telefon numarası", + "PHONEEMPTY": "Telefon numarası tanımlanmamış", + "PHONEVERIFIED": "Telefon numarası doğrulandı.", + "EMAILVERIFIED": "E-posta doğrulandı", + "NOTVERIFIED": "doğrulanmamış", + "PREFERRED_LOGINNAME": "Tercih edilen Giriş adı", + "ISINITIAL": "Kullanıcı henüz aktif değil.", + "LOGINMETHODS": { + "TITLE": "İletişim Bilgileri", + "DESCRIPTION": "Sağlanan bilgiler, şifre sıfırlama e-postaları gibi önemli bilgileri size göndermek için kullanılır.", + "EMAIL": { + "TITLE": "E-mail", + "VALID": "doğrulandı", + "ISVERIFIED": "E-posta Doğrulandı", + "ISVERIFIEDDESC": "E-posta doğrulanmış olarak işaretlenmişse, e-posta doğrulama talebi gönderilmeyecektir.", + "RESEND": "Doğrulama E-postasını Tekrar Gönder", + "EDITTITLE": "E-postayı Değiştir", + "EDITDESC": "Aşağıdaki alana yeni e-postayı girin." + }, + "PHONE": { + "TITLE": "Telefon", + "VALID": "doğrulandı", + "RESEND": "Doğrulama SMS'ini Tekrar Gönder", + "EDITTITLE": "Numarayı değiştir", + "EDITVALUE": "Telefon numarası", + "EDITDESC": "Aşağıdaki alana yeni telefon numarasını girin.", + "DELETETITLE": "Telefon numarasını sil", + "DELETEDESC": "Telefon numarasını gerçekten silmek istiyor musunuz", + "OTPSMSREMOVALWARNING": "Bu hesap bu telefon numarasını ikinci faktör olarak kullanıyor. Devam ettikten sonra bunu kullanamayacaksınız." + }, + "RESENDCODE": "Kodu Tekrar Gönder", + "ENTERCODE": "Doğrula", + "ENTERCODE_DESC": "Kodu Doğrula" + }, + "GRANTS": { + "TITLE": "Kullanıcı Yetkileri", + "DESCRIPTION": "Bu kullanıcıya belirli projelere erişim yetkisi verin", + "CREATE": { + "TITLE": "Kullanıcı Yetkisi Oluştur", + "DESCRIPTION": "Organizasyon, proje ve ilgili proje rollerini arayın." + }, + "PROJECTNAME": "Proje Adı", + "PROJECT-OWNED": "Proje", + "PROJECT-GRANTED": "Yetkilendirilmiş proje", + "FILTER": { + "0": "kullanıcı için filtrele", + "1": "domain için filtrele", + "2": "proje adı için filtrele", + "3": "rol adı için filtrele" + } + }, + "STATE": { + "0": "Bilinmeyen", + "1": "Aktif", + "2": "Pasif", + "3": "Silinmiş", + "4": "Kilitli", + "5": "Askıya Alınmış", + "6": "Başlangıç" + }, + "STATEV2": { + "0": "Bilinmeyen", + "1": "Aktif", + "2": "Pasif", + "3": "Silinmiş", + "4": "Kilitli", + "5": "Başlangıç" + }, + "SEARCH": { + "ADDITIONAL": "Kullanıcı adı (mevcut organizasyon)", + "ADDITIONAL-EXTERNAL": "Kullanıcı adı (harici organizasyon)" + }, + "TARGET": { + "SELF": "Başka bir organizasyondan bir kullanıcıya yetki vermek istiyorsanız", + "EXTERNAL": "Organizasyonunuzun bir kullanıcısına yetki vermek için", + "CLICKHERE": "buraya tıklayın" + }, + "SIGNEDOUT": "Oturumunuz kapatılmıştır. Tekrar oturum açmak için \"Oturum Aç\" düğmesine tıklayın.", + "SIGNEDOUT_BTN": "Oturum Aç", + "EDITACCOUNT": "Hesabı Düzenle", + "ADDACCOUNT": "Başka Bir Hesapla Giriş Yap", + "RESENDINITIALEMAIL": "Aktivasyon mailini tekrar gönder", + "RESENDEMAILNOTIFICATION": "E-posta bildirimini tekrar gönder", + "TOAST": { + "CREATED": "Kullanıcı başarıyla oluşturuldu.", + "SAVED": "Profil başarıyla kaydedildi.", + "USERNAMECHANGED": "Kullanıcı adı değiştirildi.", + "EMAILSAVED": "E-posta başarıyla kaydedildi.", + "INITEMAILSENT": "Başlatma maili gönderildi.", + "PHONESAVED": "Telefon başarıyla kaydedildi.", + "PHONEREMOVED": "Telefon kaldırıldı.", + "PHONEVERIFIED": "Telefon başarıyla doğrulandı.", + "PHONEVERIFICATIONSENT": "Telefon doğrulama kodu gönderildi.", + "EMAILVERIFICATIONSENT": "E-posta doğrulama kodu gönderildi.", + "OTPREMOVED": "OTP kaldırıldı.", + "U2FREMOVED": "Faktör kaldırıldı.", + "PASSWORDLESSREMOVED": "Şifresiz kimlik doğrulama kaldırıldı.", + "INITIALPASSWORDSET": "İlk şifre ayarlandı.", + "PASSWORDNOTIFICATIONSENT": "Şifre değişikliği bildirimi gönderildi.", + "PASSWORDCHANGED": "Şifre başarıyla değiştirildi.", + "REACTIVATED": "Kullanıcı yeniden etkinleştirildi.", + "DEACTIVATED": "Kullanıcı devre dışı bırakıldı.", + "SELECTEDREACTIVATED": "Seçilen kullanıcılar yeniden etkinleştirildi.", + "SELECTEDDEACTIVATED": "Seçilen kullanıcılar devre dışı bırakıldı.", + "SELECTEDKEYSDELETED": "Seçilen anahtarlar silindi.", + "KEYADDED": "Anahtar eklendi!", + "MACHINEADDED": "Servis Kullanıcısı oluşturuldu!", + "DELETED": "Kullanıcı başarıyla silindi!", + "UNLOCKED": "Kullanıcı başarıyla kilidi açıldı!", + "PASSWORDLESSREGISTRATIONSENT": "Kayıt bağlantısı başarıyla gönderildi.", + "SECRETGENERATED": "Gizli anahtar başarıyla oluşturuldu!", + "SECRETREMOVED": "Gizli anahtar başarıyla kaldırıldı!" + }, + "MEMBERSHIPS": { + "TITLE": "ZITADEL Yönetici Rolleri", + "DESCRIPTION": "Bunlar kullanıcının tüm üyelik yetkileridir. Bunları organizasyon, proje veya IAM detay sayfalarında da değiştirebilirsiniz.", + "ORGCONTEXT": "Şu anda seçili organizasyonla ilişkili tüm organizasyonları ve projeleri görüyorsunuz.", + "USERCONTEXT": "Yetkiniz olan tüm organizasyonları ve projeleri görüyorsunuz. Diğer organizasyonlar dahil.", + "CREATIONDATE": "Oluşturma Tarihi", + "CHANGEDATE": "Son Değişiklik", + "DISPLAYNAME": "Görünen Ad", + "REMOVE": "Kaldır", + "TYPE": "Tür", + "ORGID": "Organizasyon ID", + "UPDATED": "Üyelik güncellendi.", + "NOPERMISSIONTOEDIT": "Rolleri düzenlemek için gerekli izinlere sahip değilsiniz!", + "TYPES": { + "UNKNOWN": "Bilinmeyen", + "ORG": "Organizasyon", + "PROJECT": "Proje", + "GRANTEDPROJECT": "Yetkilendirilmiş Proje" + } + }, + "PERSONALACCESSTOKEN": { + "ID": "ID", + "TOKEN": "Token", + "ADD": { + "TITLE": "Yeni Kişisel Erişim Token'ı Oluştur", + "DESCRIPTION": "Token için özel bir son kullanma tarihi tanımlayın.", + "CHOOSEEXPIRY": "Bir son kullanma tarihi seçin", + "CHOOSEDATEAFTER": "Şu tarihten sonra geçerli bir son kullanma tarihi girin" + }, + "ADDED": { + "TITLE": "Kişisel Erişim Token'ı", + "DESCRIPTION": "Kişisel erişim token'ınızı kopyalamayı unutmayın. Bir daha göremeyeceksiniz!" + }, + "DELETE": { + "TITLE": "Token'ı Sil", + "DESCRIPTION": "Kişisel erişim token'ını silmek üzeresiniz. Emin misiniz?" + }, + "DELETED": "Token başarıyla silindi." + } + }, + "METADATA": { + "TITLE": "Metadata", + "KEY": "Anahtar", + "VALUE": "Değer", + "ADD": "Yeni Girdi", + "SAVE": "Kaydet", + "EMPTY": "Metadata yok", + "SETSUCCESS": "Öğe başarıyla kaydedildi", + "REMOVESUCCESS": "Öğe başarıyla silindi" + }, + "FLOWS": { + "ID": "ID", + "NAME": "Ad", + "STATE": "Durum", + "STATES": { + "0": "durum yok", + "1": "pasif", + "2": "aktif" + }, + "ADDTRIGGER": "Tetikleyici ekle", + "FLOWCHANGED": "Akış başarıyla değiştirildi", + "FLOWCLEARED": "Akış başarıyla sıfırlandı", + "TIMEOUT": "Zaman Aşımı", + "TIMEOUTINSEC": "Saniye cinsinden zaman aşımı", + "ALLOWEDTOFAIL": "Başarısız Olmasına İzin Ver", + "ALLOWEDTOFAILWARN": { + "TITLE": "Uyarı", + "DESCRIPTION": "Bu ayarı devre dışı bırakırsanız, organizasyonunuzdaki kullanıcıların oturum açamamasına neden olabilir. Ayrıca, eylemi devre dışı bırakmak için konsola erişemeyeceksiniz. Ayrı bir organizasyonda yönetici kullanıcı oluşturmanızı veya önce geliştirme ortamında veya geliştirme organizasyonunda betikleri test etmenizi öneririz." + }, + "SCRIPT": "Betik", + "FLOWTYPE": "Akış Türü", + "TRIGGERTYPE": "Tetikleyici Türü", + "ACTIONS": "Eylemler", + "ACTIONSMAX": "Seviyenize göre, sınırlı sayıda Eylem ({{value}}) kullanabilirsiniz. İhtiyacınız olmayanları devre dışı bırakmayı veya seviyenizi yükseltmeyi düşünün.", + "DIALOG": { + "ADD": { + "TITLE": "Bir Eylem Oluştur" + }, + "UPDATE": { + "TITLE": "Eylemi Güncelle" + }, + "DELETEACTION": { + "TITLE": "Eylem Silinsin mi?", + "DESCRIPTION": "Bir eylemi silmek üzeresiniz. Bu geri alınamaz. Emin misiniz?", + "DELETE_SUCCESS": "Eylem başarıyla silindi." + }, + "CLEAR": { + "TITLE": "Akış temizlensin mi?", + "DESCRIPTION": "Akışı tetikleyicileri ve eylemleriyle birlikte sıfırlamak üzeresiniz. Bu değişiklik geri alınamaz. Emin misiniz?" + }, + "REMOVEACTIONSLIST": { + "TITLE": "Seçilen Eylemler silinsin mi?", + "DESCRIPTION": "Seçilen eylemleri akıştan silmek istediğinizden emin misiniz?" + }, + "ABOUTNAME": "Eylemin adı ve javascript'teki fonksiyonun adı aynı olmalıdır" + }, + "TOAST": { + "ACTIONSSET": "Eylemler ayarlandı", + "ACTIONREACTIVATED": "Eylemler başarıyla yeniden etkinleştirildi", + "ACTIONDEACTIVATED": "Eylemler başarıyla devre dışı bırakıldı" + } + }, + "IAM": { + "POLICIES": { + "TITLE": "Sistem Politikaları ve Erişim Ayarları", + "DESCRIPTION": "Küresel Politikalarınızı ve Yönetim Erişim Ayarlarınızı yönetin." + }, + "EVENTSTORE": { + "TITLE": "IAM Depolama Yönetimi", + "DESCRIPTION": "ZITADEL görünümlerinizi ve başarısız olaylarınızı yönetin." + }, + "MEMBER": { + "TITLE": "Yöneticiler", + "DESCRIPTION": "Bu Yöneticilerin örneğinizde değişiklik yapma izni vardır." + }, + "PAGES": { + "STATE": "Durum", + "DOMAINLIST": "Özel Domain'ler" + }, + "STATE": { + "0": "Belirtilmemiş", + "1": "Oluşturuluyor", + "2": "Çalışıyor", + "3": "Durduruluyor", + "4": "Durduruldu" + }, + "VIEWS": { + "VIEWNAME": "Ad", + "DATABASE": "Veritabanı", + "SEQUENCE": "Sıra", + "EVENTTIMESTAMP": "Zaman Damgası", + "LASTSPOOL": "Başarılı spool", + "ACTIONS": "Eylemler", + "CLEAR": "Temizle", + "CLEARED": "Görünüm başarıyla temizlendi!", + "DIALOG": { + "VIEW_CLEAR_TITLE": "Görünümü Temizle", + "VIEW_CLEAR_DESCRIPTION": "Bir görünümü temizlemek üzeresiniz. Görünüm temizleme, verilerin son kullanıcılar için geçici olarak kullanılamayabileceği bir süreç oluşturur. Gerçekten emin misiniz?" + } + }, + "FAILEDEVENTS": { + "VIEWNAME": "Ad", + "DATABASE": "Veritabanı", + "FAILEDSEQUENCE": "Başarısız Sıra", + "FAILURECOUNT": "Başarısızlık Sayısı", + "LASTFAILED": "Son başarısızlık zamanı", + "ERRORMESSAGE": "Hata Mesajı", + "ACTIONS": "Eylemler", + "DELETE": "Kaldır", + "DELETESUCCESS": "Başarısız olaylar kaldırıldı." + }, + "EVENTS": { + "EDITOR": "Editör", + "EDITORID": "Editör ID", + "AGGREGATE": "Toplam", + "AGGREGATEID": "Toplam ID", + "AGGREGATETYPE": "Toplam Türü", + "RESOURCEOWNER": "Kaynak Sahibi", + "SEQUENCE": "Sıra", + "CREATIONDATE": "Oluşturulma Zamanı", + "TYPE": "Tür", + "PAYLOAD": "Payload", + "FILTERS": { + "BTN": "Filtrele", + "USER": { + "IDLABEL": "ID", + "CHECKBOX": "Editöre göre filtrele" + }, + "AGGREGATE": { + "TYPELABEL": "Toplam Türü", + "IDLABEL": "ID", + "CHECKBOX": "Toplama göre filtrele" + }, + "TYPE": { + "TYPELABEL": "Tür", + "CHECKBOX": "Türe göre filtrele" + }, + "RESOURCEOWNER": { + "LABEL": "ID", + "CHECKBOX": "Kaynak Sahibine göre filtrele" + }, + "SEQUENCE": { + "LABEL": "Sıra", + "CHECKBOX": "Sıraya göre filtrele" + }, + "SORT": "Sırala", + "ASC": "Artan", + "DESC": "Azalan", + "CREATIONDATE": { + "RADIO_FROM": "Başlangıç", + "RADIO_RANGE": "Aralık", + "LABEL_SINCE": "Bu tarihten itibaren", + "LABEL_UNTIL": "Bu tarihe kadar" + }, + "OTHER": "diğer", + "OTHERS": "diğerleri" + }, + "DIALOG": { + "TITLE": "Olay Detayı" + } + }, + "TOAST": { + "MEMBERREMOVED": "Yönetici kaldırıldı.", + "MEMBERSADDED": "Yöneticiler eklendi.", + "MEMBERADDED": "Yönetici eklendi.", + "MEMBERCHANGED": "Yönetici değiştirildi.", + "ROLEREMOVED": "Rol kaldırıldı.", + "ROLECHANGED": "Rol değiştirildi.", + "REACTIVATED": "Yeniden etkinleştirildi", + "DEACTIVATED": "Devre dışı bırakıldı" + } + }, + "ORG": { + "PAGES": { + "NAME": "Ad", + "ID": "ID", + "CREATIONDATE": "Oluşturma Tarihi", + "DATECHANGED": "Değiştirildi", + "FILTER": "Filtre", + "FILTERPLACEHOLDER": "İsme göre filtrele", + "LIST": "Organizasyonlar", + "LISTDESCRIPTION": "Bir organizasyon seçin.", + "ACTIVE": "Aktif", + "CREATE": "Organizasyon Oluştur", + "DEACTIVATE": "Organizasyonu Devre Dışı Bırak", + "REACTIVATE": "Organizasyonu Yeniden Etkinleştir", + "NOPERMISSION": "Organizasyon ayarlarına erişim izniniz yok.", + "USERSELFACCOUNT": "Organizasyon sahibi olarak kişisel hesabınızı kullanın", + "ORGDETAIL_TITLE": "Yeni organizasyonunuzun adını ve domain'ini girin.", + "ORGDETAIL_TITLE_WITHOUT_DOMAIN": "Yeni organizasyonunuzun adını girin.", + "ORGDETAILUSER_TITLE": "Organizasyon Sahibini Yapılandır", + "DELETE": "Organizasyonu sil", + "DEFAULTLABEL": "Varsayılan", + "SETASDEFAULT": "Varsayılan organizasyon olarak ayarla", + "DEFAULTORGSET": "Varsayılan organizasyon başarıyla değiştirildi", + "RENAME": { + "ACTION": "Yeniden adlandır", + "TITLE": "Organizasyonu Yeniden Adlandır", + "DESCRIPTION": "Organizasyonunuz için yeni adı girin", + "BTN": "Yeniden adlandır" + }, + "ORGDOMAIN": { + "TITLE": "{{value}} sahipliğini doğrula", + "VERIFICATION": "Domain'inizi manuel olarak doğrulamak için iki yöntem sunuyoruz:", + "VERIFICATION_HTML": "- HTTP. Web sitenizde geçici bir doğrulama dosyası barındırın", + "VERIFICATION_DNS": "- DNS. Bir TXT Record DNS girişi oluşturun", + "VERIFICATION_DNS_DESC": "{{ value }} yönetiyorsanız ve DNS kayıtlarınıza erişiminiz varsa, aşağıdaki değerlerle yeni bir TXT kaydı oluşturabilirsiniz:", + "VERIFICATION_DNS_HOST_LABEL": "Host:", + "VERIFICATION_DNS_CHALLENGE_LABEL": "TXT kaydının değeri için bu kodu kullanın:", + "VERIFICATION_HTTP_DESC": "Web sitesi barındırma hizmetinize erişiminiz varsa, doğrulama dosyasını indirin ve belirtilen URL'ye yükleyin", + "VERIFICATION_HTTP_URL_LABEL": "Beklenen URL:", + "VERIFICATION_HTTP_FILE_LABEL": "Doğrulama dosyası:", + "VERIFICATION_SKIP": "Şimdilik doğrulamayı atlayabilir ve organizasyonunuzu oluşturmaya devam edebilirsiniz, ancak domain'inizi kullanmak için bu adımın tamamlanması gerekir!", + "VERIFICATION_VALIDATION_DESC": "ZITADEL domain'inizin sahipliğini zaman zaman yeniden kontrol edeceği için doğrulama kodunu silmeyin.", + "VERIFICATION_NEWTOKEN_TITLE": "Yeni Token İste", + "VERIFICATION_VALIDATION_ONGOING": "Domain'inizi doğrulamak için {{ value }} yöntemi seçildi. Doğrulama kontrolü tetiklemek veya doğrulama sürecini sıfırlamak için düğmeye tıklayın.", + "VERIFICATION_SUCCESSFUL": "Domain başarıyla doğrulandı!", + "RESETMETHOD": "Doğrulama yöntemini sıfırla" + }, + "DOWNLOAD_FILE": "Dosyayı İndir", + "SELECTORGTOOLTIP": "Bu organizasyonu seç.", + "PRIMARYDOMAIN": "Birincil Domain", + "STATE": "Durum", + "USEPASSWORD": "İlk Şifreyi Ayarla", + "USEPASSWORDDESC": "Kullanıcının başlatma sırasında şifre ayarlaması gerekmez." + }, + "LIST": { + "TITLE": "Organizasyonlar", + "DESCRIPTION": "Bunlar örneğinizdeki organizasyonlardır" + }, + "DOMAINS": { + "NEW": "Domain Ekle", + "TITLE": "Doğrulanmış domain'ler", + "DESCRIPTION": "Organizasyon domain'lerinizi yapılandırın. Bu domain, domain keşfi ve kullanıcı adı soneki için kullanılabilir.", + "SETPRIMARY": "Birincil Olarak Ayarla", + "DELETE": { + "TITLE": "Domain'i Sil", + "DESCRIPTION": "Domain'lerinizden birini silmek üzeresiniz." + }, + "ADD": { + "TITLE": "Domain Ekle", + "DESCRIPTION": "Organizasyonunuz için bir domain eklemek üzeresiniz. Başarılı süreçten sonra, domain domain keşfi için ve kullanıcılarınız için sonek olarak kullanılabilir." + } + }, + "STATE": { + "0": "Tanımlı değil", + "1": "Aktif", + "2": "Devre dışı" + }, + "MEMBER": { + "TITLE": "Organizasyon Yöneticileri", + "DESCRIPTION": "Organizasyonunuzun tercihlerini değiştirebilecek kullanıcıları tanımlayın." + }, + "TOAST": { + "UPDATED": "Organizasyon başarıyla güncellendi.", + "DEACTIVATED": "Organizasyon devre dışı bırakıldı.", + "REACTIVATED": "Organizasyon yeniden etkinleştirildi.", + "DOMAINADDED": "Domain eklendi.", + "DOMAINREMOVED": "Domain kaldırıldı.", + "MEMBERADDED": "Yönetici eklendi.", + "MEMBERREMOVED": "Yönetici kaldırıldı.", + "MEMBERCHANGED": "Yönetici değiştirildi.", + "SETPRIMARY": "Birincil domain ayarlandı.", + "DELETED": "Organizasyon başarıyla silindi", + "DEFAULTORGNOTFOUND": "Varsayılan organizasyon bulunamadı", + "ORG_WAS_DELETED": "Organizasyon silindi." + }, + "DIALOG": { + "DEACTIVATE": { + "TITLE": "Organizasyonu devre dışı bırak", + "DESCRIPTION": "Organizasyonunuzu devre dışı bırakmak üzeresiniz. Kullanıcılar bundan sonra oturum açamayacaklar. Devam etmek istediğinizden emin misiniz?" + }, + "REACTIVATE": { + "TITLE": "Organizasyonu yeniden etkinleştir", + "DESCRIPTION": "Organizasyonunuzu yeniden etkinleştirmek üzeresiniz. Kullanıcılar tekrar oturum açabilecekler. Devam etmek istediğinizden emin misiniz?" + }, + "DELETE": { + "TITLE": "Organizasyonu sil", + "DESCRIPTION": "Organizasyonunuzu silmek üzeresiniz. Bu, organizasyonla ilgili tüm verilerin silineceği bir süreç başlatır. Şu an için bu eylemi geri alamazsınız.", + "TYPENAME": "Organizasyonunuzu silmek için '{{value}}' yazın.", + "ORGNAME": "Ad", + "BTN": "Sil" + } + } + }, + "SETTINGS": { + "LIST": { + "ORGS": "Organizasyonlar", + "FEATURESETTINGS": "Özellikler", + "LANGUAGES": "Diller", + "LOGIN": "Giriş Davranışı ve Güvenlik", + "LOCKOUT": "Kilitleme", + "AGE": "Şifre süresi", + "COMPLEXITY": "Şifre karmaşıklığı", + "NOTIFICATIONS": "Bildirimler", + "SMTP_PROVIDER": "SMTP Sağlayıcısı", + "SMS_PROVIDER": "SMS/Telefon Sağlayıcısı", + "NOTIFICATIONS_DESC": "SMTP ve SMS Ayarları", + "MESSAGETEXTS": "Mesaj Metinleri", + "IDP": "Kimlik Sağlayıcıları", + "VERIFIED_DOMAINS": "Doğrulanmış domain'ler", + "DOMAIN": "Domain ayarları", + "LOGINTEXTS": "Giriş Arayüzü Metinleri", + "BRANDING": "Marka", + "PRIVACYPOLICY": "Harici bağlantılar", + "OIDC": "OIDC Token yaşam süresi ve sona erme", + "WEB_KEYS": "OIDC Web Anahtarları", + "SECRETS": "Gizli Anahtar Üretici", + "SECURITY": "Güvenlik ayarları", + "EVENTS": "Olaylar", + "FAILEDEVENTS": "Başarısız Olaylar", + "VIEWS": "Görünümler", + "ACTIONS": "Eylemler", + "TARGETS": "Hedefler" + }, + "GROUPS": { + "GENERAL": "Genel Bilgiler", + "NOTIFICATIONS": "Bildirimler", + "LOGIN": "Giriş ve Erişim", + "DOMAIN": "Domain", + "TEXTS": "Metinler ve Diller", + "APPEARANCE": "Görünüm", + "OTHER": "Diğer", + "STORAGE": "Depolama", + "ACTIONS": "Eylemler" + }, + "BETA": "BETA" + }, + "SETTING": { + "LANGUAGES": { + "DEFAULT": "Varsayılan Dil", + "ALLOWED": "İzin Verilen Diller", + "NOT_ALLOWED": "İzin Verilmeyen Diller", + "ALLOW_ALL": "Tümüne İzin Ver", + "DISALLOW_ALL": "Tümünü Yasakla", + "SETASDEFAULT": "Varsayılan Dil Olarak Ayarla", + "DEFAULT_SAVED": "Varsayılan Dil kaydedildi", + "ALLOWED_SAVED": "İzin Verilen Diller kaydedildi", + "OPTIONS": { + "de": "Deutsch", + "en": "English", + "es": "Español", + "fr": "Français", + "it": "Italiano", + "ja": "日本語", + "pl": "Polski", + "zh": "简体中文", + "bg": "Български", + "pt": "Portuguese", + "mk": "Македонски", + "cs": "Čeština", + "ru": "Русский", + "nl": "Nederlands", + "sv": "Svenska", + "id": "Bahasa Indonesia", + "hu": "Magyar", + "ko": "한국어", + "ro": "Română", + "tr": "Türkçe" + } + }, + "SMTP": { + "TITLE": "SMTP Sağlayıcısı", + "DESCRIPTION": "Açıklama", + "SENDERADDRESS": "Gönderen E-posta Adresi", + "SENDERNAME": "Gönderen Adı", + "REPLYTOADDRESS": "Yanıtlanacak Adres", + "HOSTANDPORT": "Host ve Port", + "USER": "Kullanıcı", + "PASSWORD": "Şifre", + "SETPASSWORD": "SMTP Şifresi Ayarla", + "PASSWORDSET": "SMTP Şifresi başarıyla ayarlandı.", + "TLS": "Taşıma Katmanı Güvenliği (TLS)", + "SAVED": "Başarıyla kaydedildi!", + "NOCHANGES": "Değişiklik yok!", + "REQUIREDWARN": "Domain'inizden bildirim göndermek için SMTP verilerinizi girmeniz gerekiyor." + }, + "SMS": { + "PROVIDERS": "Sağlayıcılar", + "PROVIDER": "SMS Sağlayıcısı", + "ADDPROVIDER": "SMS Sağlayıcısı Ekle", + "ADDPROVIDERDESCRIPTION": "Mevcut sağlayıcılardan birini seçin ve gerekli verileri girin.", + "REMOVEPROVIDER": "Sağlayıcıyı Kaldır", + "REMOVEPROVIDER_DESC": "Bir sağlayıcı yapılandırmasını silmek üzeresiniz. Devam etmek istiyor musunuz?", + "SMSPROVIDERSTATE": { + "0": "Belirtilmemiş", + "1": "Aktif", + "2": "Pasif" + }, + "ACTIVATED": "Sağlayıcı etkinleştirildi.", + "DEACTIVATED": "Sağlayıcı devre dışı bırakıldı.", + "TWILIO": { + "SID": "Sid", + "TOKEN": "Token", + "SENDERNUMBER": "Gönderen Numarası", + "VERIFYSERVICESID": "Doğrulama Servis Sid", + "VERIFYSERVICESID_DESCRIPTION": "Doğrulama Servis Sid ayarlamak, telefon numaralarının ve OTP SMS'lerinin doğrulanması için Mesaj Servisi yerine Twilio Doğrulama Servisini kullanmaya izin verir", + "ADDED": "Twilio başarıyla eklendi.", + "UPDATED": "Twilio başarıyla güncellendi.", + "REMOVED": "Twilio kaldırıldı", + "CHANGETOKEN": "Token'ı Değiştir", + "SETTOKEN": "Token'ı Ayarla", + "TOKENSET": "Token başarıyla ayarlandı." + } + }, + "SECRETS": { + "TYPES": "Gizli Anahtar Türleri", + "TYPE": { + "1": "Başlatma Maili", + "2": "E-posta doğrulama", + "3": "Telefon doğrulama", + "4": "Şifre Sıfırlama", + "5": "Şifresiz Başlatma", + "6": "Uygulama Gizli Anahtarı", + "7": "Tek Kullanımlık Şifre (OTP) - SMS", + "8": "Tek Kullanımlık Şifre (OTP) - E-posta" + }, + "EXPIRY": "Son kullanma (dakika cinsinden)", + "INCLUDEDIGITS": "Sayıları Dahil Et", + "INCLUDESYMBOLS": "Sembolleri Dahil Et", + "INCLUDELOWERLETTERS": "Küçük harfleri dahil et", + "INCLUDEUPPERLETTERS": "Büyük harfleri dahil et", + "LENGTH": "Uzunluk", + "UPDATED": "Ayarlar güncellendi." + }, + "SECURITY": { + "IFRAMETITLE": "iFrame", + "IFRAMEDESCRIPTION": "Bu ayar, CSP'yi belirli izin verilen domain'lerden çerçevelemeye izin verecek şekilde ayarlar. iFrame kullanımını etkinleştirerek, clickjacking riskine maruz kalabileceğinizi unutmayın.", + "IFRAMEENABLED": "iFrame'e İzin Ver", + "ALLOWEDORIGINS": "İzin Verilen URL'ler", + "IMPERSONATIONTITLE": "Kimliğe Bürünme", + "IMPERSONATIONENABLED": "Kimliğe Bürünmeye İzin Ver", + "IMPERSONATIONDESCRIPTION": "Bu ayar prensipte kimliğe bürünme kullanımına izin verir. Kimliğe bürünen kişinin ayrıca uygun `*_IMPERSONATOR` rollerinin atanması gerektiğini unutmayın." + }, + "FEATURES": { + "LOGINDEFAULTORG": "Giriş Varsayılan Org", + "LOGINDEFAULTORG_DESCRIPTION": "Organizasyon bağlamı ayarlanmadıysa, giriş UI'ı varsayılan org'un ayarlarını (örnektekini değil) kullanacaktır", + "OIDCTOKENEXCHANGE": "OIDC Token Exchange", + "OIDCTOKENEXCHANGE_DESCRIPTION": "OIDC token uç noktası için deneysel urn:ietf:params:oauth:grant-type:token-exchange grant türünü etkinleştirin. Token değişimi, daha az kapsamlı token'lar talep etmek veya diğer kullanıcıların kimliğine bürünmek için kullanılabilir. Bir örnekte kimliğe bürünmeye izin vermek için güvenlik politikasına bakın.", + "USERSCHEMA": "Kullanıcı Şeması", + "USERSCHEMA_DESCRIPTION": "Kullanıcı Şemaları, kullanıcının veri şemalarını yönetmeye izin verir. Bayrak etkinleştirilirse, yeni API'yi ve özelliklerini kullanabileceksiniz.", + "ACTIONS": "Eylemler", + "ACTIONS_DESCRIPTION": "Eylemler v2, veri yürütmelerini ve hedefleri yönetmeye izin verir. Bayrak etkinleştirilirse, yeni API'yi ve özelliklerini kullanabileceksiniz.", + "OIDCSINGLEV1SESSIONTERMINATION": "OIDC Tek V1 Oturum Sonlandırma", + "OIDCSINGLEV1SESSIONTERMINATION_DESCRIPTION": "Bayrak etkinleştirilirse, end_session uç noktasında id_token_hint olarak `sid` iddiası olan bir id_token sağlayarak giriş UI'ından tek bir oturumu sonlandırabileceksiniz. Şu anda giriş UI'ında aynı kullanıcı aracısından (tarayıcı) tüm oturumların sonlandırıldığını unutmayın. Oturum API'si aracılığıyla yönetilen oturumlar zaten tek oturumların sonlandırılmasına izin veriyor.", + "DEBUGOIDCPARENTERROR": "OIDC Üst Hatayı Hata Ayıkla", + "DEBUGOIDCPARENTERROR_DESCRIPTION": "Bayrak etkinleştirilirse, OIDC üst hatası konsolda günlüğe kaydedilecektir.", + "DISABLEUSERTOKENEVENT": "Kullanıcı Token Olayını Devre Dışı Bırak", + "DISABLEUSERTOKENEVENT_DESCRIPTION": "", + "ENABLEBACKCHANNELLOGOUT": "Arka Kanal Çıkışını Etkinleştir", + "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "Arka Kanal Çıkışı OpenID Connect Arka Kanal Çıkışı 1.0'ı uygular ve OpenID Sağlayıcısında oturum sonlandırma hakkında istemcileri bilgilendirmek için kullanılabilir.", + "PERMISSIONCHECKV2": "İzin Kontrolü V2", + "PERMISSIONCHECKV2_DESCRIPTION": "Bayrak etkinleştirilirse, yeni API'yi ve özelliklerini kullanabileceksiniz.", + "STATES": { + "INHERITED": "Miras Al", + "ENABLED": "Etkin", + "DISABLED": "Devre Dışı" + }, + "INHERITED_DESCRIPTION": "Bu değeri sistemin varsayılan değerine ayarlar.", + "INHERITEDINDICATOR_DESCRIPTION": { + "ENABLED": "\"Etkin\" miras alınır", + "DISABLED": "\"Devre Dışı\" miras alınır" + }, + "RESET": "Tümünü miras almaya ayarla", + "CONSOLEUSEV2USERAPI": "Kullanıcı oluşturma için Konsolda V2 Api'yi kullan", + "CONSOLEUSEV2USERAPI_DESCRIPTION": "Bu bayrak etkinleştirildiğinde, konsol yeni kullanıcılar oluşturmak için V2 Kullanıcı API'sini kullanır. V2 API ile yeni oluşturulan kullanıcılar başlangıç durumu olmadan başlar.", + "LOGINV2": "Giriş V2", + "LOGINV2_DESCRIPTION": "Bunu etkinleştirmek, gelişmiş güvenlik, performans ve özelleştirme ile yeni TypeScript tabanlı giriş UI'ını etkinleştirir.", + "LOGINV2_BASEURI": "Temel URI" + }, + "DIALOG": { + "RESET": { + "DEFAULTTITLE": "Ayarı Sıfırla", + "DEFAULTDESCRIPTION": "Ayarlarınızı örneğinizin varsayılan yapılandırmasına sıfırlamak üzeresiniz. Devam etmek istediğinizden emin misiniz?", + "LOGINPOLICY_DESCRIPTION": "Uyarı: Devam ederseniz, Kimlik Sağlayıcısı ayarları da örnek ayarına sıfırlanacaktır." + } + } + }, + "POLICY": { + "APPLIEDTO": "Uygulandığı yer", + "PWD_COMPLEXITY": { + "TITLE": "Şifre Karmaşıklığı", + "DESCRIPTION": "Ayarlanan tüm şifrelerin belirli bir desene uygun olmasını sağlar", + "SYMBOLANDNUMBERERROR": "Bir rakam ve bir sembol/noktalama işaretinden oluşmalıdır.", + "SYMBOLERROR": "Bir sembol/noktalama işareti içermelidir.", + "NUMBERERROR": "Bir rakam içermelidir.", + "PATTERNERROR": "Şifre gerekli deseni karşılamıyor." + }, + "NOTIFICATION": { + "TITLE": "Bildirim", + "DESCRIPTION": "Hangi değişikliklerde bildirimlerin gönderileceğini belirler.", + "PASSWORDCHANGE": "Şifre değişikliği" + }, + "PRIVATELABELING": { + "DESCRIPTION": "Girişe kişiselleştirilmiş tarzınızı verin ve davranışını değiştirin.", + "PREVIEW_DESCRIPTION": "Politika değişiklikleri otomatik olarak önizleme ortamına dağıtılacaktır.", + "BTN": "Dosya Seç", + "ACTIVATEPREVIEW": "Yapılandırmayı uygula", + "DARK": "Koyu Mod", + "LIGHT": "Açık Mod", + "CHANGEVIEW": "Görünümü Değiştir", + "ACTIVATED": "Politika değişiklikleri artık CANLI", + "THEME": "Tema", + "COLORS": "Renkler", + "FONT": "Yazı Tipi", + "ADVANCEDBEHAVIOR": "Gelişmiş Davranış", + "DROP": "Resmi buraya bırakın veya", + "RELEASE": "Bırak", + "DROPFONT": "Yazı tipi dosyasını buraya bırakın", + "RELEASEFONT": "Bırak", + "USEOFLOGO": "Logonuz Giriş'te ve e-postalarda kullanılacaktır, ikon ise konsoldaki organizasyon değiştirici gibi daha küçük UI öğeleri için kullanılır", + "MAXSIZE": "Maksimum boyut 524kB ile sınırlıdır", + "EMAILNOSVG": "SVG dosya formatı e-postalarda desteklenmez. Bu nedenle logonuzu PNG veya diğer desteklenen formatlarda yükleyin.", + "MAXSIZEEXCEEDED": "524kB maksimum boyutu aşıldı.", + "NOSVGSUPPORTED": "SVG desteklenmiyor!", + "FONTINLOGINONLY": "Yazı tipi şu anda yalnızca giriş arayüzünde görüntüleniyor.", + "BACKGROUNDCOLOR": "Arka plan rengi", + "PRIMARYCOLOR": "Birincil renk", + "WARNCOLOR": "Uyarı rengi", + "FONTCOLOR": "Yazı tipi rengi", + "VIEWS": { + "PREVIEW": "Önizleme", + "CURRENT": "Mevcut Yapılandırma" + }, + "PREVIEW": { + "TITLE": "Giriş", + "SECOND": "ZITADEL Hesabınızla giriş yapın.", + "ERROR": "Kullanıcı bulunamadı!", + "PRIMARYBUTTON": "ileri", + "SECONDARYBUTTON": "kayıt ol" + }, + "THEMEMODE": { + "THEME_MODE_AUTO": "Otomatik Mod", + "THEME_MODE_LIGHT": "Yalnızca Açık Mod", + "THEME_MODE_DARK": "Yalnızca Koyu Mod" + } + }, + "PWD_AGE": { + "TITLE": "Şifre Süresi", + "DESCRIPTION": "Şifrelerin süresi için bir politika belirleyebilirsiniz. Bu politika, kullanıcının süre dolduktan sonraki ilk girişinde şifreyi değiştirmesini zorlayacaktır. Otomatik uyarılar ve bildirimler yoktur." + }, + "PWD_LOCKOUT": { + "TITLE": "Kilitleme Politikası", + "DESCRIPTION": "Hesapların bloke edileceği maksimum şifre deneme sayısını ayarlayın." + }, + "PRIVATELABELING_POLICY": { + "TITLE": "Marka", + "BTN": "Dosya Seç", + "DESCRIPTION": "Giriş görünümünü özelleştir", + "ACTIVATEPREVIEW": "Yapılandırmayı Etkinleştir" + }, + "LOGIN_POLICY": { + "TITLE": "Giriş Ayarları", + "DESCRIPTION": "Kullanıcıların nasıl kimlik doğrulaması yapabileceğini tanımlayın ve Kimlik Sağlayıcılarını yapılandırın", + "DESCRIPTIONCREATEADMIN": "Kullanıcılar aşağıdaki mevcut kimlik sağlayıcıları arasından seçim yapabilir.", + "DESCRIPTIONCREATEMGMT": "Kullanıcılar aşağıdaki mevcut kimlik sağlayıcıları arasından seçim yapabilir. Not: Sistem tarafından ayarlanan sağlayıcıları ve sadece organizasyonunuz için ayarlanan sağlayıcıları kullanabilirsiniz.", + "LIFETIME_INVALID": "Form geçersiz değer(ler) içeriyor.", + "SAVED": "Başarıyla kaydedildi!", + "PROVIDER_ADDED": "Kimlik sağlayıcısı etkinleştirildi." + }, + "PRIVACY_POLICY": { + "DESCRIPTION": "Gizlilik Politikası ve Hizmet Şartları Bağlantılarınızı ayarlayın", + "TOSLINK": "Hizmet Şartları bağlantısı", + "POLICYLINK": "Gizlilik Politikası bağlantısı", + "HELPLINK": "Yardım bağlantısı", + "SUPPORTEMAIL": "Destek E-postası", + "DOCSLINK": "Dokümanlar Bağlantısı (Konsol)", + "CUSTOMLINK": "Özel Bağlantı (Konsol)", + "CUSTOMLINKTEXT": "Özel Bağlantı Metni (Konsol)", + "SAVED": "Başarıyla kaydedildi!", + "RESET_TITLE": "Varsayılan Değerleri Geri Yükle", + "RESET_DESCRIPTION": "Hizmet Şartları ve Gizlilik Politikası için varsayılan Bağlantıları geri yüklemek üzeresiniz. Gerçekten devam etmek istiyor musunuz?" + }, + "LOGIN_TEXTS": { + "TITLE": "Giriş Arayüzü Metinleri", + "DESCRIPTION": "Giriş arayüzleri için metinlerinizi tanımlayın. Metinler boşsa, yer tutucu olarak gösterilen varsayılan değer kullanılacaktır.", + "DESCRIPTION_SHORT": "Giriş arayüzleri için metinlerinizi tanımlayın.", + "NEWERVERSIONEXISTS": "Daha yeni sürüm mevcut", + "CURRENTDATE": "Mevcut yapılandırma", + "CHANGEDATE": "Daha yeni sürüm tarih:", + "KEYNAME": "Giriş Ekranı / Arayüzü", + "RESET_TITLE": "Varsayılan Değerleri Geri Yükle", + "RESET_DESCRIPTION": "Tüm varsayılan değerleri geri yüklemek üzeresiniz. Yaptığınız tüm değişiklikler kalıcı olarak silinecektir. Gerçekten devam etmek istiyor musunuz?", + "UNSAVED_TITLE": "Kaydetmeden devam et?", + "UNSAVED_DESCRIPTION": "Kaydetmeden değişiklikler yaptınız. Şimdi kaydetmek istiyor musunuz?", + "ACTIVE_LANGUAGE_NOT_ALLOWED": "İzin verilmeyen bir dil seçtiniz. Metinleri değiştirmeye devam edebilirsiniz. Ancak kullanıcılarınızın bu dili gerçekten kullanabilmesini istiyorsanız, örneğinizin kısıtlamalarını değiştirin.", + "LANGUAGES_NOT_ALLOWED": "İzin verilmeyen:", + "LANGUAGE": "Dil", + "LANGUAGES": { + "de": "Deutsch", + "en": "English", + "es": "Español", + "fr": "Français", + "it": "Italiano", + "ja": "日本語", + "pl": "Polski", + "zh": "简体中文", + "bg": "Български", + "pt": "Portuguese", + "mk": "Македонски", + "cs": "Čeština", + "ru": "Русский", + "nl": "Nederlands", + "sv": "Svenska", + "id": "Bahasa Indonesia", + "hu": "Magyar", + "ko": "한국어", + "ro": "Română", + "tr": "Türkçe" + }, + "KEYS": { + "emailVerificationDoneText": "E-posta doğrulama tamamlandı", + "emailVerificationText": "E-posta doğrulama", + "externalUserNotFoundText": "Harici kullanıcı bulunamadı", + "footerText": "Alt bilgi", + "initMfaDoneText": "MFA başlatma tamamlandı", + "initMfaOtpText": "MFA başlat", + "initMfaPromptText": "MFA Başlatma İstemi", + "initMfaU2fText": "Evrensel İkinci Faktörü Başlat", + "initPasswordDoneText": "Şifre başlatma tamamlandı", + "initPasswordText": "Şifre başlat", + "initializeDoneText": "Kullanıcı başlatma tamamlandı", + "initializeUserText": "Kullanıcıyı başlat", + "linkingUserDoneText": "Kullanıcı bağlama tamamlandı", + "loginText": "Giriş", + "logoutText": "Çıkış", + "mfaProvidersText": "MFA Sağlayıcıları", + "passwordChangeDoneText": "Şifre değişikliği tamamlandı", + "passwordChangeText": "Şifre değişikliği", + "passwordResetDoneText": "Şifre sıfırlama tamamlandı", + "passwordText": "Şifre", + "registrationOptionText": "Kayıt Seçenekleri", + "registrationOrgText": "Org Kaydet", + "registrationUserText": "Kullanıcı Kaydet", + "selectAccountText": "Hesap Seç", + "successLoginText": "Başarılı giriş", + "usernameChangeDoneText": "Kullanıcı adı değişikliği tamamlandı", + "usernameChangeText": "Kullanıcı adı değişikliği", + "verifyMfaOtpText": "OTP Doğrula", + "verifyMfaU2fText": "Evrensel İkinci Faktörü Doğrula", + "passwordlessPromptText": "Şifresiz İstem", + "passwordlessRegistrationDoneText": "Şifresiz Kayıt Tamamlandı", + "passwordlessRegistrationText": "Şifresiz Kayıt", + "passwordlessText": "Şifresiz", + "externalRegistrationUserOverviewText": "Harici Kayıt Kullanıcı Özeti" + } + }, + "MESSAGE_TEXTS": { + "TYPE": "Bildirim", + "TYPES": { + "INIT": "Başlatma", + "VE": "E-postayı Doğrula", + "VP": "Telefonu Doğrula", + "VSO": "SMS OTP Doğrula", + "VEO": "E-posta OTP Doğrula", + "PR": "Şifre Sıfırlama", + "DC": "Domain Talebi", + "PL": "Şifresiz", + "PC": "Şifre Değişikliği", + "IU": "Kullanıcı Davet Et" + }, + "CHIPS": { + "firstname": "Ad", + "lastname": "Soyad", + "code": "Kod", + "preferredLoginName": "Tercih Edilen Giriş Adı", + "displayName": "Görünen ad", + "nickName": "Takma ad", + "loginnames": "Giriş adları", + "domain": "Domain", + "lastEmail": "Son e-posta", + "lastPhone": "Son telefon", + "verifiedEmail": "Doğrulanmış e-posta", + "verifiedPhone": "Doğrulanmış telefon", + "changedate": "Değişiklik tarihi", + "username": "Kullanıcı adı", + "tempUsername": "Geçici kullanıcı adı", + "otp": "Tek kullanımlık şifre", + "verifyUrl": "Tek kullanımlık şifre URL'sini doğrula", + "expiry": "Son kullanma", + "applicationName": "Uygulama adı" + }, + "TOAST": { + "UPDATED": "Özel Metinler kaydedildi." + } + }, + "DEFAULTLABEL": "Mevcut ayarlar Örneğinizin standardına karşılık gelir.", + "BTN_INSTALL": "Kurulum", + "BTN_EDIT": "Değiştir", + "DATA": { + "DESCRIPTION": "Açıklama", + "MINLENGTH": "minimum uzunluğa sahip olmalı", + "HASNUMBER": "bir sayı içermeli", + "HASSYMBOL": "bir sembol içermeli", + "HASLOWERCASE": "küçük harf içermeli", + "HASUPPERCASE": "büyük harf içermeli", + "SHOWLOCKOUTFAILURES": "kilitleme başarısızlıklarını göster", + "MAXPASSWORDATTEMPTS": "Şifre maksimum deneme sayısı", + "MAXOTPATTEMPTS": "OTP maksimum deneme sayısı", + "EXPIREWARNDAYS": "Gün sonra son kullanma uyarısı", + "MAXAGEDAYS": "Gün cinsinden maksimum geçerlilik", + "USERLOGINMUSTBEDOMAIN": "Giriş adlarına organizasyon domain'ini sonek olarak ekle", + "USERLOGINMUSTBEDOMAIN_DESCRIPTION": "Bu ayarı etkinleştirirseniz, tüm giriş adları organizasyon domain'i ile soneklenecektir. Bu ayar devre dışıysa, kullanıcı adlarının tüm organizasyonlarda benzersiz olmasını sağlamanız gerekir.", + "VALIDATEORGDOMAINS": "Organizasyon domain doğrulaması gerekli (DNS veya HTTP zorluğu)", + "SMTPSENDERADDRESSMATCHESINSTANCEDOMAIN": "SMTP Gönderen Adresi Örnek Domain'i ile Eşleşir", + "ALLOWUSERNAMEPASSWORD_DESC": "Kullanıcı adı ve şifre ile geleneksel giriş yapılmasına izin verilir.", + "ALLOWEXTERNALIDP_DESC": "Altta yatan kimlik sağlayıcıları için giriş yapılmasına izin verilir", + "ALLOWREGISTER_DESC": "Seçenek işaretlenmişse, girişte kullanıcı kaydetme için ek bir adım görünür.", + "FORCEMFA": "Tüm kullanıcılar için MFA'yı zorla", + "FORCEMFALOCALONLY": "Yalnızca yerel kimlik doğrulamalı kullanıcılar için MFA'yı zorla", + "FORCEMFALOCALONLY_DESC": "Seçenek işaretlenmişse, yerel kimlik doğrulamalı kullanıcılar giriş için ikinci faktör yapılandırmak zorundadır.", + "HIDEPASSWORDRESET_DESC": "Seçenek işaretlenmişse, kullanıcı giriş sürecinde şifresini sıfırlayamaz.", + "HIDELOGINNAMESUFFIX": "Giriş adı sonekini gizle", + "HIDELOGINNAMESUFFIX_DESC": "Giriş arayüzünde giriş adı sonekini gizler", + "IGNOREUNKNOWNUSERNAMES_DESC": "Seçenek işaretlenmişse, kullanıcı bulunamasa bile giriş sürecinde şifre ekranı gösterilecektir. Şifre kontrolündeki hata, kullanıcı adının mı yoksa şifrenin mi yanlış olduğunu açığa çıkarmayacaktır.", + "ALLOWDOMAINDISCOVERY_DESC": "Seçenek işaretlenmişse, giriş ekranındaki bilinmeyen kullanıcı adı girişinin soneki (@domain.com) organizasyon domain'leri ile eşleştirilecek ve başarı durumunda o organizasyonun kaydına yönlendirecektir.", + "DEFAULTREDIRECTURI": "Varsayılan Yönlendirme URI", + "DEFAULTREDIRECTURI_DESC": "Giriş bir uygulama bağlamı olmadan başlatılmışsa kullanıcının nereye yönlendirileceğini tanımlar (örn. e-postadan)", + "ERRORMSGPOPUP": "Hatayı Dialog'ta Göster", + "DISABLEWATERMARK": "Filigranı Gizle", + "DISABLEWATERMARK_DESC": "Giriş arayüzünde Powered by ZITADEL filigranını gizle" + }, + "RESET": "Örnek varsayılanına sıfırla", + "CREATECUSTOM": "Özel Politika Oluştur", + "TOAST": { + "SET": "Politika başarıyla ayarlandı!", + "RESETSUCCESS": "Politika başarıyla sıfırlandı!", + "UPLOADSUCCESS": "Başarıyla yüklendi!", + "DELETESUCCESS": "Başarıyla silindi!", + "UPLOADFAILED": "Yükleme başarısız!" + } + }, + "ORG_DETAIL": { + "TITLE": "Organizasyon", + "DESCRIPTION": "Burada organizasyonunuzun yapılandırmasını düzenleyebilir ve üyeleri yönetebilirsiniz.", + "DETAIL": { + "TITLE": "Detay", + "NAME": "Ad", + "DOMAIN": "Domain", + "STATE": { + "0": "Tanımlı değil", + "1": "Aktif", + "2": "Pasif" + } + }, + "MEMBER": { + "TITLE": "Üyeler", + "USERNAME": "Kullanıcı Adı", + "DISPLAYNAME": "Görünen Ad", + "LOGINNAME": "Giriş Adı", + "EMAIL": "E-posta", + "ROLES": "Roller", + "ADD": "Üye Ekle", + "ADDDESCRIPTION": "Eklenecek kullanıcıların adlarını girin." + }, + "TABLE": { + "TOTAL": "Toplam girdi", + "SELECTION": "Seçilen Öğeler", + "DEACTIVATE": "Kullanıcıyı Devre Dışı Bırak", + "ACTIVATE": "Kullanıcıyı Etkinleştir", + "DELETE": "Kullanıcıyı Sil", + "CLEAR": "Seçimi temizle" + } + }, + "PROJECT": { + "PAGES": { + "TITLE": "Proje", + "DESCRIPTION": "Burada uygulamaları tanımlayabilir, rolleri yönetebilir ve diğer organizasyonlara projenizi kullanma yetkisi verebilirsiniz.", + "DELETE": "Projeyi Sil", + "DETAIL": "Detay", + "CREATE": "Proje Oluştur", + "CREATE_DESC": "Projenizin adını girin.", + "ROLE": "Rol", + "NOITEMS": "Proje yok", + "ZITADELPROJECT": "Bu ZITADEL projesine aittir. Dikkat: Değişiklik yaparsanız ZITADEL amaçlandığı gibi davranmayabilir.", + "TYPE": { + "OWNED": "Sahip Olunan Projeler", + "OWNED_SINGULAR": "Sahip Olunan Proje", + "GRANTED_SINGULAR": "{{name}} tarafından Yetkilendirilmiş Proje" + }, + "PRIVATELABEL": { + "TITLE": "Marka Ayarı", + "0": { + "TITLE": "Belirtilmemiş", + "DESC": "Kullanıcı tanımlandığında, sistem varsayılanı gösterilmeden önce tanımlanan kullanıcının organizasyonunun markası gösterilecektir." + }, + "1": { + "TITLE": "Proje ayarını kullan", + "DESC": "Projenin sahibi olan organizasyonun markası gösterilecektir" + }, + "2": { + "TITLE": "Kullanıcı Organizasyonu ayarını kullan", + "DESC": "Projenin organizasyonunun markası gösterilecektir, ancak kullanıcı tanımlandığında, tanımlanan kullanıcının organizasyonunun ayarı gösterilecektir." + }, + "DIALOG": { + "TITLE": "Marka Ayarı", + "DESCRIPTION": "Projeyi kullanırken giriş davranışını seçin." + } + }, + "PINNED": "Sabitlenmiş", + "ALL": "Tümü", + "CREATEDON": "Oluşturulma tarihi", + "LASTMODIFIED": "Son değişiklik tarihi", + "ADDNEW": "Yeni Proje Oluştur", + "DIALOG": { + "REACTIVATE": { + "TITLE": "Projeyi Yeniden Etkinleştir", + "DESCRIPTION": "Projenizi gerçekten yeniden etkinleştirmek istiyor musunuz?" + }, + "DEACTIVATE": { + "TITLE": "Projeyi Devre Dışı Bırak", + "DESCRIPTION": "Projenizi gerçekten devre dışı bırakmak istiyor musunuz?" + }, + "DELETE": { + "TITLE": "Projeyi Sil", + "DESCRIPTION": "Projenizi gerçekten silmek istiyor musunuz?", + "TYPENAME": "Kalıcı olarak silmek için projenin adını yazın." + } + } + }, + "SETTINGS": { + "TITLE": "Ayarlar", + "DESCRIPTION": "" + }, + "STATE": { + "TITLE": "Durum", + "0": "Tanımlı değil", + "1": "Aktif", + "2": "Pasif" + }, + "TYPE": { + "TITLE": "Tür", + "0": "Bilinmeyen tür", + "1": "Sahip olunan", + "2": "Yetkilendirilmiş" + }, + "NAME": "Ad", + "NAMEDIALOG": { + "TITLE": "Projeyi Yeniden Adlandır", + "DESCRIPTION": "Projeniz için yeni adı girin", + "NAME": "Yeni Ad" + }, + "MEMBER": { + "TITLE": "Yöneticiler", + "TITLEDESC": "Yöneticiler rollerine göre bu projede değişiklik yapabilirler.", + "DESCRIPTION": "Bu yöneticiler projenizi düzenleyebilir.", + "USERNAME": "Kullanıcı Adı", + "DISPLAYNAME": "Görünen Ad", + "LOGINNAME": "Giriş adı", + "EMAIL": "E-posta", + "ROLES": "Roller", + "USERID": "Kullanıcı ID" + }, + "GRANT": { + "EMPTY": "Yetkilendirilmiş organizasyon yok.", + "TITLE": "Proje Yetkileri", + "DESCRIPTION": "Başka bir organizasyonun projenizi kullanmasına izin verin.", + "EDITTITLE": "Rolleri düzenle", + "CREATE": { + "TITLE": "Organizasyon Yetkisi Oluştur", + "SEL_USERS": "Erişim vermek istediğiniz kullanıcıları seçin", + "SEL_PROJECT": "Bir proje arayın", + "SEL_ROLES": "Yetkiye eklenmesini istediğiniz rolleri seçin", + "SEL_USER": "Kullanıcıları seçin", + "SEL_ORG": "Bir organizasyon arayın", + "SEL_ORG_DESC": "Yetkilendirilecek organizasyonu arayın.", + "ORG_DESCRIPTION": "{{name}} organizasyonu için bir kullanıcıyı yetkilendirmek üzeresiniz.", + "ORG_DESCRIPTION_DESC": "Başka bir organizasyon için kullanıcı yetkilendirmek için yukarıdaki başlıktaki bağlamı değiştirin.", + "SEL_ORG_FORMFIELD": "Organizasyon", + "FOR_ORG": "Yetki şunun için oluşturulur:" + }, + "DETAIL": { + "TITLE": "Proje Yetkisi", + "DESC": "Belirtilen organizasyon tarafından hangi rollerin kullanılabileceğini seçebilir ve yöneticileri seçebilirsiniz", + "MEMBERTITLE": "Yöneticiler", + "MEMBERDESC": "Bunlar yetkilendirilmiş organizasyonun yöneticileridir. Projenin verilerini düzenleme erişimi kazanması gereken kullanıcıları buraya ekleyin.", + "PROJECTNAME": "Proje Adı", + "GRANTEDORG": "Yetkilendirilmiş Organizasyon", + "RESOURCEOWNER": "Kaynak Sahibi" + }, + "STATE": "Durum", + "STATES": { + "1": "Aktif", + "2": "Pasif" + }, + "ALL": "Tümü", + "SHOWDETAIL": "Detayları Göster", + "USER": "Kullanıcı", + "MEMBERS": "Yöneticiler", + "ORG": "Organizasyon", + "PROJECTNAME": "Proje Adı", + "GRANTEDORG": "Yetkilendirilmiş Organizasyon", + "GRANTEDORGDOMAIN": "Domain", + "RESOURCEOWNER": "Kaynak Sahibi", + "GRANTEDORGNAME": "Organizasyon Adı", + "GRANTID": "Yetki Id", + "CREATIONDATE": "Oluşturma Tarihi", + "CHANGEDATE": "Son değişiklik", + "DATES": "Tarihler", + "ROLENAMESLIST": "Roller", + "NOROLES": "Rol yok", + "TYPE": "Tür", + "TOAST": { + "PROJECTGRANTUSERGRANTADDED": "Proje yetkisi oluşturuldu.", + "PROJECTGRANTADDED": "Proje yetkisi oluşturuldu.", + "PROJECTGRANTCHANGED": "Proje yetkisi değiştirildi.", + "PROJECTGRANTMEMBERADDED": "Yetki yöneticisi eklendi.", + "PROJECTGRANTMEMBERCHANGED": "Yetki yöneticisi değiştirildi.", + "PROJECTGRANTMEMBERREMOVED": "Yetki yöneticisi kaldırıldı.", + "PROJECTGRANTUPDATED": "Proje Yetkisi güncellendi" + }, + "DIALOG": { + "DELETE_TITLE": "Proje yetkisini sil", + "DELETE_DESCRIPTION": "Bir proje yetkisini silmek üzeresiniz. Emin misiniz?" + }, + "ROLES": "Proje Rolleri" + }, + "APP": { + "TITLE": "Uygulamalar", + "NAME": "Ad", + "NAMEREQUIRED": "Bir ad gereklidir." + }, + "ROLE": { + "EMPTY": "Henüz hiç rol oluşturulmamış.", + "ADDNEWLINE": "Ek rol ekle", + "KEY": "Anahtar", + "TITLE": "Roller", + "DESCRIPTION": "Proje yetkileri oluşturmak için kullanılabilecek bazı roller tanımlayın.", + "NAME": "Ad", + "DISPLAY_NAME": "Görünen Ad", + "GROUP": "Grup", + "ACTIONS": "Eylemler", + "ADDTITLE": "Rol Oluştur", + "ADDDESCRIPTION": "Yeni rol için verileri girin.", + "EDITTITLE": "Rolü Düzenle", + "EDITDESCRIPTION": "Rol için yeni verileri girin.", + "DELETE": "Rolü Sil", + "CREATIONDATE": "Oluşturuldu", + "CHANGEDATE": "Son Değişiklik", + "SELECTGROUPTOOLTIP": "{{group}} grubunun tüm Rollerini seç.", + "OPTIONS": "Seçenekler", + "ASSERTION": "Kimlik Doğrulamada Rolleri Doğrula", + "ASSERTION_DESCRIPTION": "Rol bilgisi Userinfo uç noktasından gönderilir ve uygulama ayarlarınıza bağlı olarak token'larda ve diğer türlerde yer alır.", + "CHECK": "Kimlik Doğrulamada yetkilendirmeyi kontrol et", + "CHECK_DESCRIPTION": "Ayarlanırsa, kullanıcılar yalnızca hesaplarına herhangi bir rol atanmışsa kimlik doğrulaması yapabilirler.", + "DIALOG": { + "DELETE_TITLE": "Rolü sil", + "DELETE_DESCRIPTION": "Bir proje rolünü silmek üzeresiniz. Emin misiniz?" + } + }, + "HAS_PROJECT": "Kimlik Doğrulamada Proje Kontrolü", + "HAS_PROJECT_DESCRIPTION": "Kullanıcının organizasyonunun bu projeye sahip olup olmadığı kontrol edilir. Değilse, kullanıcı kimlik doğrulaması yapamaz.", + "TABLE": { + "TOTAL": "Toplam girdi:", + "SELECTION": "Seçilen Öğeler", + "DEACTIVATE": "Projeyi Devre Dışı Bırak", + "ACTIVATE": "Projeyi Etkinleştir", + "DELETE": "Projeyi Sil", + "ORGNAME": "Organizasyon Adı", + "ORGID": "Organizasyon ID", + "ORGDOMAIN": "Organizasyon Domain'i", + "STATE": "Durum", + "TYPE": "Tür", + "CREATIONDATE": "Oluşturulma zamanı", + "CHANGEDATE": "Son değişiklik", + "RESOURCEOWNER": "Sahibi", + "SHOWTABLE": "Tabloyu göster", + "SHOWGRID": "Izgarayı göster", + "EMPTY": "Proje bulunamadı" + }, + "TOAST": { + "MEMBERREMOVED": "Yönetici kaldırıldı.", + "MEMBERSADDED": "Yöneticiler eklendi.", + "MEMBERADDED": "Yönetici eklendi.", + "MEMBERCHANGED": "Yönetici değiştirildi.", + "ROLESCREATED": "Roller oluşturuldu.", + "ROLEREMOVED": "Rol kaldırıldı.", + "ROLECHANGED": "Rol değiştirildi.", + "REACTIVATED": "Yeniden etkinleştirildi.", + "DEACTIVATED": "Devre dışı bırakıldı.", + "CREATED": "Proje oluşturuldu.", + "UPDATED": "Proje değiştirildi.", + "GRANTUPDATED": "Yetki değiştirildi.", + "DELETED": "Proje silindi." + } + }, + "ROLES": { + "DIALOG": { + "DELETE_TITLE": "Rolü sil", + "DELETE_DESCRIPTION": "Bir rolü silmek üzeresiniz. Emin misiniz?" + } + }, + "NEXTSTEPS": { + "TITLE": "Sonraki Adımlar" + }, + "IDP": { + "LIST": { + "ACTIVETITLE": "Aktif Kimlik Sağlayıcıları" + }, + "CREATE": { + "TITLE": "Sağlayıcı ekle", + "DESCRIPTION": "Aşağıdaki sağlayıcılardan birini veya daha fazlasını seçin.", + "STEPPERTITLE": "Sağlayıcı Oluştur", + "OIDC": { + "TITLE": "OIDC Sağlayıcısı", + "DESCRIPTION": "OIDC sağlayıcınız için gerekli verileri girin." + }, + "OAUTH": { + "TITLE": "OAuth Sağlayıcısı", + "DESCRIPTION": "OAuth sağlayıcınız için gerekli verileri girin." + }, + "JWT": { + "TITLE": "JWT Sağlayıcısı", + "DESCRIPTION": "JWT sağlayıcınız için gerekli verileri girin." + }, + "GOOGLE": { + "TITLE": "Google Sağlayıcısı", + "DESCRIPTION": "Google Kimlik Sağlayıcınız için kimlik bilgilerini girin" + }, + "GITLAB": { + "TITLE": "Gitlab Sağlayıcısı", + "DESCRIPTION": "Gitlab Kimlik Sağlayıcınız için kimlik bilgilerini girin" + }, + "GITLABSELFHOSTED": { + "TITLE": "Gitlab Kendi Sunucu Sağlayıcısı", + "DESCRIPTION": "Gitlab Kendi Sunucu Kimlik Sağlayıcınız için kimlik bilgilerini girin" + }, + "GITHUBES": { + "TITLE": "GitHub Enterprise Server Sağlayıcısı", + "DESCRIPTION": "GitHub Enterprise Server Kimlik Sağlayıcınız için kimlik bilgilerini girin" + }, + "GITHUB": { + "TITLE": "Github Sağlayıcısı", + "DESCRIPTION": "Github Kimlik Sağlayıcınız için kimlik bilgilerini girin" + }, + "AZUREAD": { + "TITLE": "Microsoft Sağlayıcısı", + "DESCRIPTION": "Microsoft Kimlik Sağlayıcınız için kimlik bilgilerini girin" + }, + "LDAP": { + "TITLE": "Active Directory / LDAP", + "DESCRIPTION": "LDAP Sağlayıcınız için kimlik bilgilerini girin" + }, + "APPLE": { + "TITLE": "Apple ile Giriş", + "DESCRIPTION": "Apple Sağlayıcınız için kimlik bilgilerini girin" + }, + "SAML": { + "TITLE": "SAML ile Giriş", + "DESCRIPTION": "SAML Sağlayıcınız için kimlik bilgilerini girin" + } + }, + "DETAIL": { + "TITLE": "Kimlik Sağlayıcısı", + "DESCRIPTION": "Sağlayıcı yapılandırmanızı güncelleyin", + "DATECREATED": "Oluşturuldu", + "DATECHANGED": "Değiştirildi" + }, + "OPTIONS": { + "ISAUTOCREATION": "Otomatik oluşturma", + "ISAUTOCREATION_DESC": "Seçilirse, henüz mevcut değilse bir hesap oluşturulacaktır.", + "ISAUTOUPDATE": "Otomatik güncelleme", + "ISAUTOUPDATE_DESC": "Seçilirse, hesaplar yeniden kimlik doğrulamada güncellenir.", + "ISCREATIONALLOWED": "Hesap oluşturmaya izin verildi (manuel)", + "ISCREATIONALLOWED_DESC": "Hesapların harici hesap kullanılarak oluşturulup oluşturulamayacağını belirler. auto_creation etkinleştirildiğinde kullanıcıların hesap bilgilerini düzenleyememesi gerekiyorsa devre dışı bırakın.", + "ISLINKINGALLOWED": "Hesap bağlamaya izin verildi (manuel)", + "ISLINKINGALLOWED_DESC": "Bir kimliğin mevcut bir hesaba manuel olarak bağlanıp bağlanamayacağını belirler. Kullanıcıların yalnızca aktif auto_linking durumunda önerilen hesabı bağlamasına izin verilmesi gerekiyorsa devre dışı bırakın.", + "AUTOLINKING_DESC": "Bir kimliğin mevcut bir hesaba bağlanması için isteme çıkıp çıkmayacağını belirler.", + "AUTOLINKINGTYPE": { + "0": "Devre dışı", + "1": "Mevcut Kullanıcı Adını kontrol et", + "2": "Mevcut E-postayı kontrol et" + } + }, + "OWNERTYPES": { + "0": "bilinmeyen", + "1": "Örnek", + "2": "Organizasyon" + }, + "STATES": { + "1": "aktif", + "2": "pasif" + }, + "AZUREADTENANTTYPES": { + "3": "Kiracı ID", + "0": "Ortak", + "1": "Organizasyonlar", + "2": "Tüketiciler" + }, + "AZUREADTENANTTYPE": "Kiracı Türü", + "AZUREADTENANTID": "Kiracı ID", + "EMAILVERIFIED": "E-posta doğrulandı", + "NAMEHINT": "Belirtilirse giriş arayüzünde gösterilecektir.", + "OPTIONAL": "isteğe bağlı", + "LDAPATTRIBUTES": "LDAP Öznitelikleri", + "UPDATEBINDPASSWORD": "Bağlama Şifresini güncelle", + "UPDATECLIENTSECRET": "istemci gizli anahtarını güncelle", + "ADD": "Kimlik Sağlayıcısı Ekle", + "TYPE": "Tür", + "OWNER": "Sahibi", + "ID": "ID", + "NAME": "Ad", + "AUTHORIZATIONENDPOINT": "Yetkilendirme Uç Noktası", + "TOKENENDPOINT": "Token Uç Noktası", + "USERENDPOINT": "Kullanıcı Uç Noktası", + "IDATTRIBUTE": "ID Özniteliği", + "AVAILABILITY": "Kullanılabilirlik", + "AVAILABLE": "kullanılabilir", + "AVAILABLEBUTINACTIVE": "kullanılabilir ancak pasif", + "SETAVAILABLE": "kullanılabilir olarak ayarla", + "SETUNAVAILABLE": "kullanılamaz olarak ayarla", + "CONFIG": "Yapılandırma", + "STATE": "Durum", + "ISSUER": "Yayıncı", + "SCOPESLIST": "Kapsam Listesi", + "CLIENTID": "İstemci ID", + "CLIENTSECRET": "İstemci Gizli Anahtarı", + "LDAPCONNECTION": "Bağlantı", + "LDAPUSERBINDING": "Kullanıcı bağlama", + "BASEDN": "BaseDn", + "BINDDN": "BindDn", + "BINDPASSWORD": "Bağlama Şifresi", + "SERVERS": "Sunucular", + "STARTTLS": "TLS Başlat", + "TIMEOUT": "Saniye cinsinden zaman aşımı", + "USERBASE": "Kullanıcı tabanı", + "USERFILTERS": "Kullanıcı filtreleri", + "USEROBJECTCLASSES": "Kullanıcı Nesne Sınıfları", + "REQUIRED": "gerekli", + "LDAPIDATTRIBUTE": "ID özniteliği", + "AVATARURLATTRIBUTE": "Avatar Url özniteliği", + "DISPLAYNAMEATTRIBUTE": "Görünen ad özniteliği", + "EMAILATTRIBUTEATTRIBUTE": "E-posta özniteliği", + "EMAILVERIFIEDATTRIBUTE": "E-posta doğrulandı özniteliği", + "FIRSTNAMEATTRIBUTE": "Ad özniteliği", + "LASTNAMEATTRIBUTE": "Soyad özniteliği", + "NICKNAMEATTRIBUTE": "Takma ad özniteliği", + "PHONEATTRIBUTE": "Telefon özniteliği", + "PHONEVERIFIEDATTRIBUTE": "Telefon doğrulandı özniteliği", + "PREFERREDLANGUAGEATTRIBUTE": "Tercih edilen dil özniteliği", + "PREFERREDUSERNAMEATTRIBUTE": "Tercih edilen kullanıcı adı özniteliği", + "PROFILEATTRIBUTE": "Profil özniteliği", + "IDPDISPLAYNAMMAPPING": "IdP Görünen Ad Eşlemesi", + "USERNAMEMAPPING": "Kullanıcı Adı Eşlemesi", + "DATES": "Tarihler", + "CREATIONDATE": "Oluşturulma Zamanı", + "CHANGEDATE": "Son Değişiklik", + "DEACTIVATE": "Devre Dışı Bırak", + "ACTIVATE": "Etkinleştir", + "DELETE": "Sil", + "DELETE_TITLE": "IdP'yi Sil", + "DELETE_DESCRIPTION": "Bir kimlik sağlayıcısını silmek üzeresiniz. Sonuçta ortaya çıkan değişiklikler geri alınamaz. Gerçekten bunu yapmak istiyor musunuz?", + "REMOVE_WARN_TITLE": "IdP'yi Kaldır", + "REMOVE_WARN_DESCRIPTION": "Bir kimlik sağlayıcısını kaldırmak üzeresiniz. Bu, kullanıcılarınız için mevcut IdP seçimini kaldıracak ve zaten kayıtlı kullanıcılar tekrar giriş yapamayacaklar. Devam etmek istediğinizden emin misiniz?", + "DELETE_SELECTION_TITLE": "IdP'yi Sil", + "DELETE_SELECTION_DESCRIPTION": "Bir kimlik sağlayıcısını silmek üzeresiniz. Sonuçta ortaya çıkan değişiklikler geri alınamaz. Gerçekten bunu yapmak istiyor musunuz?", + "FEDERATEDLOGOUTENABLED": "Federe Çıkış Etkin", + "FEDERATEDLOGOUTENABLED_DESC": "Etkinleştirilirse, kullanıcı ZITADEL'de oturumu sonlandırdığında IdP'den de çıkış yapılacaktır.", + "EMPTY": "Kullanılabilir IdP yok", + "OIDC": { + "GENERAL": "Genel Bilgiler", + "TITLE": "OIDC Yapılandırması", + "DESCRIPTION": "OIDC Kimlik Sağlayıcısı için verileri girin." + }, + "JWT": { + "TITLE": "JWT Yapılandırması", + "DESCRIPTION": "JWT Kimlik Sağlayıcısı için verileri girin.", + "HEADERNAME": "Başlık Adı", + "JWTENDPOINT": "JWT Uç Noktası", + "JWTKEYSENDPOINT": "JWT Anahtarları Uç Noktası" + }, + "APPLE": { + "TEAMID": "Takım ID", + "KEYID": "Anahtar ID", + "PRIVATEKEY": "Özel Anahtar", + "UPDATEPRIVATEKEY": "Özel Anahtarı Güncelle", + "UPLOADPRIVATEKEY": "Özel Anahtar Yükle", + "KEYMAXSIZEEXCEEDED": "Maksimum 5kB boyutu aşıldı." + }, + "SAML": { + "METADATAXML": "Metadata Xml", + "METADATAURL": "Metadata URL", + "BINDING": "Binding", + "SIGNEDREQUEST": "İmzalı İstek", + "NAMEIDFORMAT": "NameID Formatı", + "TRANSIENTMAPPINGATTRIBUTENAME": "Özel Eşleme Özellik Adı", + "TRANSIENTMAPPINGATTRIBUTENAME_DESC": "`nameid-format` `transient` döndürülmesi durumunda kullanıcıyı eşlemek için alternatif özellik adı, örn. `http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress`" + }, + "TOAST": { + "SAVED": "Başarıyla kaydedildi.", + "REACTIVATED": "IdP yeniden etkinleştirildi.", + "DEACTIVATED": "IdP devre dışı bırakıldı.", + "SELECTEDREACTIVATED": "Seçilen IdP'ler yeniden etkinleştirildi.", + "SELECTEDDEACTIVATED": "Seçilen IdP'ler devre dışı bırakıldı.", + "SELECTEDKEYSDELETED": "Seçilen IdP'ler silindi.", + "DELETED": "IdP başarıyla kaldırıldı!", + "ADDED": "Başarıyla eklendi.", + "REMOVED": "Başarıyla kaldırıldı." + }, + "ISIDTOKENMAPPING": "ID token'den eşle", + "ISIDTOKENMAPPING_DESC": "Seçilirse, sağlayıcı bilgileri userinfo uç noktasından değil ID token'den eşlenir.", + "USEPKCE": "PKCE Kullan", + "USEPKCE_DESC": "Auth isteğinde code_challenge ve code_challenge_method parametrelerinin dahil edilip edilmeyeceğini belirler" + }, + "MFA": { + "LIST": { + "MULTIFACTORTITLE": "Şifresiz", + "MULTIFACTORDESCRIPTION": "Şifresiz kimlik doğrulama için çok faktörlü kimlik doğrulama yöntemlerinizi burada tanımlayın.", + "SECONDFACTORTITLE": "Çok Faktörlü Kimlik Doğrulama", + "SECONDFACTORDESCRIPTION": "Şifre kimlik doğrulamanızı güvence altına almak için kullanabileceğiniz ek faktörleri tanımlayın." + }, + "CREATE": { + "TITLE": "Yeni Faktör", + "DESCRIPTION": "Yeni faktör türünüzü seçin." + }, + "DELETE": { + "TITLE": "Faktörü Sil", + "DESCRIPTION": "Giriş Ayarlarından bir faktörü silmek üzeresiniz. Emin misiniz?" + }, + "TOAST": { + "ADDED": "Başarıyla eklendi.", + "SAVED": "Başarıyla kaydedildi.", + "DELETED": "Başarıyla kaldırıldı" + }, + "TYPE": "Tür", + "MULTIFACTORTYPES": { + "0": "Bilinmeyen", + "1": "Parmak izi, Güvenlik Anahtarları, Face ID ve diğerleri" + }, + "SECONDFACTORTYPES": { + "0": "Bilinmeyen", + "1": "Kimlik Doğrulayıcı Uygulama ile Tek Kullanımlık Şifre (TOTP)", + "2": "Parmak izi, Güvenlik Anahtarları, Face ID ve diğerleri", + "3": "E-posta ile Tek Kullanımlık Şifre (Email OTP)", + "4": "SMS ile Tek Kullanımlık Şifre (SMS OTP)" + } + }, + "LOGINPOLICY": { + "CREATE": { + "TITLE": "Giriş Ayarları", + "DESCRIPTION": "Kullanıcılarınızın organizasyonunuzda nasıl kimlik doğrulaması yapabileceğini tanımlayın." + }, + "IDPS": "Kimlik Sağlayıcıları", + "ADDIDP": { + "TITLE": "Kimlik Sağlayıcısı Ekle", + "DESCRIPTION": "Kimlik doğrulama için önceden tanımlanmış veya kendi oluşturduğunuz sağlayıcıları seçebilirsiniz.", + "SELECTIDPS": "Kimlik sağlayıcıları" + }, + "PASSWORDLESS": "Şifresiz Giriş", + "PASSWORDLESSTYPE": { + "0": "İzin verilmez", + "1": "İzin verilir" + } + }, + "SMTP": { + "LIST": { + "TITLE": "SMTP Sağlayıcısı", + "DESCRIPTION": "Bunlar ZITADEL örneğiniz için SMTP sağlayıcılarıdır. Kullanıcılarınıza bildirim göndermek için kullanmak istediğinizi etkinleştirin.", + "EMPTY": "Kullanılabilir SMTP Sağlayıcısı yok", + "ACTIVATED": "Etkinleştirildi", + "ACTIVATE": "Sağlayıcıyı etkinleştir", + "DEACTIVATE": "Sağlayıcıyı devre dışı bırak", + "TEST": "Sağlayıcınızı test edin", + "TYPE": "Tür", + "DIALOG": { + "ACTIVATED": "SMTP yapılandırması etkinleştirildi", + "ACTIVATE_WARN_TITLE": "SMTP yapılandırmasını etkinleştir", + "ACTIVATE_WARN_DESCRIPTION": "Bir SMTP yapılandırmasını etkinleştirmek üzeresiniz. Önce mevcut aktif sağlayıcıyı devre dışı bırakacağız, sonra bu yapılandırmayı etkinleştireceğiz. Emin misiniz?", + "DEACTIVATE_WARN_TITLE": "SMTP yapılandırmasını devre dışı bırak", + "DEACTIVATE_WARN_DESCRIPTION": "Bir SMTP yapılandırmasını devre dışı bırakmak üzeresiniz. Emin misiniz?", + "DEACTIVATED": "SMTP yapılandırması devre dışı bırakıldı", + "DELETE_TITLE": "SMTP yapılandırmasını sil", + "DELETE_DESCRIPTION": "Bir yapılandırmayı silmek üzeresiniz. Gönderen adını yazarak bu işlemi onaylayın", + "DELETED": "SMTP yapılandırması silindi", + "SENDER": "Bu SMTP yapılandırmasını silmek için {{value}} yazın.", + "TEST_TITLE": "SMTP yapılandırmanızı test edin", + "TEST_DESCRIPTION": "Bu sağlayıcı için SMTP yapılandırmanızı test etmek üzere bir e-posta adresi belirtin", + "TEST_EMAIL": "E-posta adresi", + "TEST_RESULT": "Test sonucu" + } + }, + "CREATE": { + "TITLE": "SMTP sağlayıcısı ekle", + "DESCRIPTION": "Aşağıdaki sağlayıcılardan birini veya birkaçını seçin.", + "STEPS": { + "TITLE": "{{ value }} SMTP Sağlayıcısı Ekle", + "CREATE_DESC_TITLE": "{{ value }} SMTP ayarlarınızı adım adım girin", + "CURRENT_DESC_TITLE": "Bunlar SMTP ayarlarınız", + "PROVIDER_SETTINGS": "SMTP Sağlayıcı Ayarları", + "SENDER_SETTINGS": "Gönderen Ayarları", + "NEXT_STEPS": "Sonraki Adımlar", + "ACTIVATE": { + "TITLE": "SMTP Sağlayıcınızı Etkinleştirin", + "DESCRIPTION": "ZITADEL bu SMTP Sağlayıcısını bildirim göndermek için etkinleştirene kadar kullanamaz. Bu sağlayıcıyı etkinleştirirseniz aktif olan diğer tüm sağlayıcılar devre dışı kalacaktır." + }, + "DEACTIVATE": { + "TITLE": "SMTP Sağlayıcınızı Devre Dışı Bırakın", + "DESCRIPTION": "Bu SMTP Sağlayıcısını devre dışı bırakırsanız, ZITADEL onu tekrar etkinleştirene kadar bildirim göndermek için kullanamaz." + }, + "SAVE_SETTINGS": "Ayarlarınızı kaydedin", + "TEST": { + "TITLE": "Ayarlarınızı test edin", + "DESCRIPTION": "SMTP sağlayıcı ayarlarınızı test edebilir ve kaydetmeden önce test sonucunu kontrol edebilirsiniz", + "RESULT": "E-postanız başarıyla gönderildi" + } + } + }, + "DETAIL": { + "TITLE": "SMTP Sağlayıcı Ayarları" + }, + "EMPTY": "Kullanılabilir SMTP sağlayıcısı yok", + "STEPS": { + "SENDGRID": {} + } + }, + "APP": { + "LIST": "Uygulamalar", + "COMPLIANCE": "OIDC Uyumluluğu", + "URLS": "URL'ler", + "CONFIGURATION": "Yapılandırma", + "TOKEN": "Token Ayarları", + "PAGES": { + "TITLE": "Uygulama", + "ID": "ID", + "DESCRIPTION": "Burada uygulama verilerinizi ve yapılandırmasını düzenleyebilirsiniz.", + "CREATE": "Uygulama oluştur", + "CREATE_SELECT_PROJECT": "Önce projenizi seçin", + "CREATE_NEW_PROJECT": "veya yeni projenizin adını girin", + "CREATE_DESC_TITLE": "Uygulama Detaylarınızı Adım Adım Girin", + "CREATE_DESC_SUB": "Önerilen bir yapılandırma otomatik olarak oluşturulacaktır.", + "STATE": "Durum", + "DATECREATED": "Oluşturuldu", + "DATECHANGED": "Değiştirildi", + "URLS": "URL'ler", + "DELETE": "Uygulamayı Sil", + "JUMPTOPROJECT": "Roller, yetkilendirmeler ve daha fazlasını yapılandırmak için projeye gidin.", + "DETAIL": { + "TITLE": "Detay", + "STATE": { + "0": "Tanımlanmamış", + "1": "Aktif", + "2": "Pasif" + } + }, + "DIALOG": { + "CONFIG": { + "TITLE": "OIDC Yapılandırmasını Değiştir" + }, + "DELETE": { + "TITLE": "Uygulamayı Sil", + "DESCRIPTION": "Bu uygulamayı gerçekten silmek istiyor musunuz?" + } + }, + "NEXTSTEPS": { + "TITLE": "Sonraki Adımlar", + "0": { + "TITLE": "Roller ekle", + "DESC": "Proje rollerinizi girin" + }, + "1": { + "TITLE": "Kullanıcı ekle", + "DESC": "Organizasyonunuzun yeni kullanıcılarını ekleyin" + }, + "2": { + "TITLE": "Yardım ve Destek", + "DESC": "Uygulama oluşturma hakkındaki belgelerimizi okuyun veya desteğimizle iletişime geçin" + } + } + }, + "NAMEDIALOG": { + "TITLE": "Uygulamayı Yeniden Adlandır", + "DESCRIPTION": "Uygulamanız için yeni adı girin", + "NAME": "Yeni Ad" + }, + "NAME": "Ad", + "TYPE": "Uygulama Türü", + "AUTHMETHOD": "Kimlik Doğrulama Yöntemi", + "AUTHMETHODSECTION": "Kimlik Doğrulama Yöntemi", + "GRANT": "Grant Türleri", + "ADDITIONALORIGINS": "Ek Origin'ler", + "ADDITIONALORIGINSDESC": "Redirect olarak kullanılmayan uygulamanıza ek Origin'ler eklemek istiyorsanız bunu burada yapabilirsiniz.", + "ORIGINS": "Origin'ler", + "NOTANORIGIN": "Girilen değer bir origin değil", + "PROSWITCH": "Ben bir profesyonelim. Bu sihirbazı atla.", + "NAMEANDTYPESECTION": "Ad ve Tür", + "TITLEFIRST": "Uygulamanın adı", + "TYPETITLE": "Uygulama türü", + "OIDC": { + "WELLKNOWN": "Diğer bağlantılar discovery endpoint'ten alınabilir.", + "INFO": { + "ISSUER": "Yayınlayıcı", + "CLIENTID": "Client ID" + }, + "CURRENT": "Mevcut Yapılandırma", + "TOKENSECTIONTITLE": "AuthToken Seçenekleri", + "REDIRECTSECTIONTITLE": "Yönlendirme Ayarları", + "REDIRECTTITLE": "Girişin yönlendirileceği URI'leri belirtin.", + "POSTREDIRECTTITLE": "Bu çıkış sonrası yönlendirme URI'sidir.", + "REDIRECTDESCRIPTIONWEB": "Yönlendirme URI'leri https:// ile başlamalıdır. http:// sadece geliştirme modu etkinken geçerlidir.", + "REDIRECTDESCRIPTIONNATIVE": "Yönlendirme URI'leri kendi protokolünüz, http://127.0.0.1, http://[::1] veya http://localhost ile başlamalıdır.", + "REDIRECTNOTVALID": "Bu yönlendirme URI'si geçerli değil.", + "COMMAORENTERSEPERATION": "↵ ile ayırın", + "TYPEREQUIRED": "Tür gereklidir.", + "TITLE": "OIDC Yapılandırması", + "CLIENTID": "Client ID", + "CLIENTSECRET": "Client Secret", + "CLIENTSECRET_NOSECRET": "Seçtiğiniz kimlik doğrulama akışıyla gizli anahtar gerekmez ve bu nedenle mevcut değildir.", + "CLIENTSECRET_DESCRIPTION": "Dialog kapandığında kaybolacağı için client secret'ınızı güvenli bir yerde saklayın.", + "REGENERATESECRET": "Client Secret'ı Yeniden Oluştur", + "DEVMODE": "Geliştirme Modu", + "DEVMODE_ENABLED": "Etkinleştirildi", + "DEVMODE_DISABLED": "Devre Dışı", + "DEVMODEDESC": "Dikkat: Geliştirme modu etkinleştirildiğinde yönlendirme URI'leri doğrulanmayacaktır.", + "SKIPNATIVEAPPSUCCESSPAGE": "Giriş Başarı Sayfasını Atla", + "SKIPNATIVEAPPSUCCESSPAGE_DESCRIPTION": "Bu yerel uygulama için giriş sonrası başarı sayfasını atla.", + "REDIRECT": "Yönlendirme URI'leri", + "REDIRECTSECTION": "Yönlendirme URI'leri", + "POSTLOGOUTREDIRECT": "Çıkış Sonrası URI'ler", + "RESPONSESECTION": "Response Türleri", + "GRANTSECTION": "Grant Türleri", + "GRANTTITLE": "Grant türlerinizi seçin. Not: Implicit sadece tarayıcı tabanlı uygulamalar için kullanılabilir.", + "APPTYPE": { + "0": "Web", + "1": "User Agent", + "2": "Native" + }, + "RESPONSETYPE": "Response Türleri", + "RESPONSE": { + "0": "Code", + "1": "ID Token", + "2": "Token-ID Token" + }, + "REFRESHTOKEN": "Refresh Token", + "GRANTTYPE": "Grant Türleri", + "GRANT": { + "0": "Authorization Code", + "1": "Implicit", + "2": "Refresh Token", + "3": "Device Code", + "4": "Token Exchange" + }, + "AUTHMETHOD": { + "0": "Basic", + "1": "Post", + "2": "None", + "3": "Private Key JWT" + }, + "TOKENTYPE": "Auth Token Türü", + "TOKENTYPE0": "Bearer Token", + "TOKENTYPE1": "JWT", + "UNSECUREREDIRECT": "Ne yaptığınızı bildiğinizi umuyorum.", + "OVERVIEWSECTION": "Genel Bakış", + "OVERVIEWTITLE": "Artık bitirdiniz. Yapılandırmanızı gözden geçirin.", + "ACCESSTOKENROLEASSERTION": "Access token'a kullanıcı rollerini ekle", + "ACCESSTOKENROLEASSERTION_DESCRIPTION": "Seçilirse, kimliği doğrulanmış kullanıcının istenen rolleri access token'a eklenir.", + "IDTOKENROLEASSERTION": "ID Token içinde kullanıcı rolleri", + "IDTOKENROLEASSERTION_DESCRIPTION": "Seçilirse, kimliği doğrulanmış kullanıcının istenen rolleri ID token'a eklenir.", + "IDTOKENUSERINFOASSERTION": "ID Token içinde Kullanıcı Bilgisi", + "IDTOKENUSERINFOASSERTION_DESCRIPTION": "İstemcilerin ID token'dan profil, e-posta, telefon ve adres bilgilerini almasını sağlar.", + "CLOCKSKEW": "İstemcilerin OP ve istemci saat farklılığını ele almasını sağlar. Süre (0-5s) exp claim'ine eklenecek ve iats, auth_time ve nbf'den çıkarılacaktır.", + "RECOMMENDED": "önerilen", + "NOTRECOMMENDED": "önerilmeyen", + "SELECTION": { + "APPTYPE": { + "WEB": { + "TITLE": "Web", + "DESCRIPTION": ".net, PHP, Node.js, Java vb. gibi düzenli Web uygulamaları." + }, + "NATIVE": { + "TITLE": "Native", + "DESCRIPTION": "Mobil Uygulamalar, Masaüstü, Akıllı Cihazlar vb." + }, + "USERAGENT": { + "TITLE": "User Agent", + "DESCRIPTION": "Tek Sayfa Uygulamaları (SPA) ve genel olarak tarayıcılarda çalıştırılan tüm JS framework'leri" + } + } + } + }, + "API": { + "INFO": { + "CLIENTID": "Client ID" + }, + "REGENERATESECRET": "Client Secret'ı Yeniden Oluştur", + "SELECTION": { + "TITLE": "API", + "DESCRIPTION": "Genel olarak API'ler" + }, + "AUTHMETHOD": { + "0": "Basic", + "1": "Private Key JWT" + } + }, + "SAML": { + "SELECTION": { + "TITLE": "SAML", + "DESCRIPTION": "SAML Uygulamaları" + }, + "CONFIGSECTION": "SAML Yapılandırması", + "CHOOSEMETADATASOURCE": "Aşağıdaki seçeneklerden birini kullanarak SAML yapılandırmanızı sağlayın:", + "METADATAOPT1": "Seçenek 1. Metadata dosyasının bulunduğu URL'yi belirtin", + "METADATAOPT2": "Seçenek 2. Metadata XML'inizi içeren bir dosya yükleyin", + "METADATAOPT3": "Seçenek 3. ENTITYID ve ACS URL sağlayarak minimal bir metadata dosyası oluşturun", + "UPLOAD": "XML dosyası yükle", + "METADATA": "Metadata", + "METADATAFROMFILE": "Dosyadan metadata", + "CERTIFICATE": "SAML sertifikası", + "DOWNLOADCERT": "SAML sertifikasını indir", + "CREATEMETADATA": "Metadata oluştur", + "ENTITYID": "Entity ID", + "ACSURL": "ACS endpoint URL" + }, + "AUTHMETHODS": { + "CODE": { + "TITLE": "Code", + "DESCRIPTION": "Authorization code'u token'lar ile değiştir" + }, + "PKCE": { + "TITLE": "PKCE", + "DESCRIPTION": "Daha fazla güvenlik için statik client secret yerine rastgele hash kullan" + }, + "POST": { + "TITLE": "POST", + "DESCRIPTION": "Client_id ve client_secret'ı formun bir parçası olarak gönder" + }, + "PK_JWT": { + "TITLE": "Private Key JWT", + "DESCRIPTION": "Uygulamanızı yetkilendirmek için özel anahtar kullanın" + }, + "BASIC": { + "TITLE": "Basic", + "DESCRIPTION": "Kullanıcı Adı ve Şifre ile kimlik doğrulama" + }, + "IMPLICIT": { + "TITLE": "Implicit", + "DESCRIPTION": "Token'ları doğrudan authorization endpoint'ten al" + }, + "DEVICECODE": { + "TITLE": "Device Code", + "DESCRIPTION": "Cihazı bilgisayar veya akıllı telefonda yetkilendirin." + }, + "CUSTOM": { + "TITLE": "Özel", + "DESCRIPTION": "Ayarınız başka hiçbir seçeneğe karşılık gelmiyor." + } + }, + "TOAST": { + "REACTIVATED": "Uygulama yeniden etkinleştirildi.", + "DEACTIVATED": "Uygulama devre dışı bırakıldı.", + "OIDCUPDATED": "Uygulama güncellendi.", + "APIUPDATED": "Uygulama güncellendi", + "UPDATED": "Uygulama güncellendi.", + "CREATED": "Uygulama oluşturuldu.", + "CLIENTSECRETREGENERATED": "client secret oluşturuldu.", + "DELETED": "Uygulama silindi.", + "CONFIGCHANGED": "Değişiklikler tespit edildi!" + }, + "LOGINV2": { + "USEV2": "Yeni Giriş UI'sını kullan", + "BASEURL": "Yeni Giriş UI'sı için özel base URL" + } + }, + "GENDERS": { + "0": "Bilinmeyen", + "1": "Kadın", + "2": "Erkek", + "3": "Diğer" + }, + "LANGUAGES": { + "de": "Deutsch", + "en": "English", + "es": "Español", + "fr": "Français", + "it": "Italiano", + "ja": "日本語", + "pl": "Polski", + "zh": "简体中文", + "bg": "Български", + "pt": "Portuguese", + "mk": "Македонски", + "cs": "Čeština", + "ru": "Русский", + "nl": "Nederlands", + "sv": "Svenska", + "id": "Bahasa Indonesia", + "hu": "Magyar", + "ko": "한국어", + "ro": "Română", + "tr": "Türkçe" + }, + "MEMBER": { + "ADD": "Yönetici Ekle", + "CREATIONTYPE": "Oluşturma Türü", + "CREATIONTYPES": { + "3": "IAM", + "2": "Organizasyon", + "0": "Sahip Olunan Proje", + "1": "Verilen Proje", + "4": "Proje" + }, + "EDITROLE": "Rolleri düzenle", + "EDITFOR": "Kullanıcı için rolleri düzenle: {{value}}", + "DIALOG": { + "DELETE_TITLE": "Yöneticiyi Kaldır", + "DELETE_DESCRIPTION": "Bir yöneticiyi kaldırmak üzeresiniz. Emin misiniz?" + }, + "SHOWDETAILS": "Detayları görmek için tıklayın." + }, + "ROLESLABEL": "Roller", + "GRANTS": { + "TITLE": "Yetkilendirmeler", + "DESC": "Bunlar organizasyonunuzdaki tüm yetkilendirmelerdir.", + "DELETE": "Yetkilendirmeyi Sil", + "EMPTY": "Yetkilendirme bulunamadı", + "ADD": "Yetkilendirme Oluştur", + "ADD_BTN": "Yeni", + "PROJECT": { + "TITLE": "Yetkilendirme", + "DESCRIPTION": "Belirtilen proje için yetkilendirmeleri tanımlayın. Sadece izinleriniz olan proje ve kullanıcı girişlerini görebileceğinizi unutmayın." + }, + "USER": { + "TITLE": "Yetkilendirme", + "DESCRIPTION": "Belirtilen kullanıcı için yetkilendirmeleri tanımlayın. Sadece izinleriniz olan proje ve kullanıcı girişlerini görebileceğinizi unutmayın." + }, + "CREATE": { + "TITLE": "Yetkilendirme oluştur", + "DESCRIPTION": "Organizasyon, proje ve ilgili rolleri arayın." + }, + "EDIT": { + "TITLE": "Yetkilendirmeyi değiştir" + }, + "DETAIL": { + "TITLE": "Yetkilendirme Detayı", + "DESCRIPTION": "Burada yetkilendirmenin tüm detaylarını görebilirsiniz." + }, + "TOAST": { + "UPDATED": "Yetkilendirme güncellendi.", + "REMOVED": "Yetkilendirme kaldırıldı", + "BULKREMOVED": "Yetkilendirmeler kaldırıldı.", + "CANTSHOWINFO": "Bu kullanıcının ait olduğu organizasyonun üyesi olmadığınız için bu kullanıcının profilini ziyaret edemezsiniz" + }, + "DIALOG": { + "DELETE_TITLE": "Yetkilendirmeyi sil", + "DELETE_DESCRIPTION": "Bir yetkilendirmeyi silmek üzeresiniz. Devam etmek istiyor musunuz?", + "BULK_DELETE_TITLE": "Yetkilendirmeleri sil", + "BULK_DELETE_DESCRIPTION": "Birden fazla yetkilendirmeyi silmek üzeresiniz. Devam etmek istiyor musunuz?" + } + }, + "CHANGES": { + "LISTTITLE": "Son Değişiklikler", + "BOTTOM": "Listenin sonuna ulaştınız.", + "LOADMORE": "Daha fazla yükle", + "ORG": { + "TITLE": "Aktivite", + "DESCRIPTION": "Burada organizasyon değişikliği oluşturan en son olayları görebilirsiniz." + }, + "PROJECT": { + "TITLE": "Aktivite", + "DESCRIPTION": "Burada proje değişikliği oluşturan en son olayları görebilirsiniz." + }, + "USER": { + "TITLE": "Aktivite", + "DESCRIPTION": "Burada kullanıcı değişikliği oluşturan en son olayları görebilirsiniz." + } + } +} diff --git a/console/src/assets/i18n/zh.json b/console/src/assets/i18n/zh.json index 4aa5ad0ef2..d13ea28e04 100644 --- a/console/src/assets/i18n/zh.json +++ b/console/src/assets/i18n/zh.json @@ -1545,7 +1545,8 @@ "id": "Bahasa Indonesia", "hu": "Magyar", "ko": "한국어", - "ro": "Română" + "ro": "Română", + "tr": "Türkçe" } }, "SMTP": { @@ -1624,12 +1625,8 @@ "FEATURES": { "LOGINDEFAULTORG": "登录默认组织", "LOGINDEFAULTORG_DESCRIPTION": "如果没有设置组织上下文,登录界面将使用默认组织的设置(而不是实例的设置)", - "OIDCLEGACYINTROSPECTION": "OIDC 传统内省", - "OIDCLEGACYINTROSPECTION_DESCRIPTION": "我们最近出于性能原因重构了内省端点。如果出现意外错误,可以使用此功能回滚到传统实现。", "OIDCTOKENEXCHANGE": "OIDC 令牌交换", "OIDCTOKENEXCHANGE_DESCRIPTION": "启用 OIDC 令牌端点的实验性 urn:ietf:params:oauth:grant-type:token-exchange 授权类型。令牌交换可用于请求具有较少范围的令牌或模拟其他用户。请参阅安全策略以允许在实例上模拟。", - "OIDCTRIGGERINTROSPECTIONPROJECTIONS": "OIDC 触发内省投影", - "OIDCTRIGGERINTROSPECTIONPROJECTIONS_DESCRIPTION": "在内省请求期间启用投影触发器。如果内省响应中存在明显的一致性问题,这可以作为一个解决方法,但可能会影响性能。我们计划在未来删除内省请求的触发器。", "USERSCHEMA": "用户架构", "USERSCHEMA_DESCRIPTION": "用户架构允许管理用户的数据架构。如果启用此标志,您将可以使用新的 API 及其功能。", "ACTIONS": "操作", @@ -1644,8 +1641,6 @@ "ENABLEBACKCHANNELLOGOUT_DESCRIPTION": "Back-Channel 注销实现了 OpenID Connect Back-Channel Logout 1.0,可用于通知客户端在 OpenID 提供商处终止会话。", "PERMISSIONCHECKV2": "权限检查 V2", "PERMISSIONCHECKV2_DESCRIPTION": "如果启用该标志,您将能够使用新的 API 及其功能。", - "WEBKEY": "Web 密钥", - "WEBKEY_DESCRIPTION": "如果启用该标志,您将能够使用新的 API 及其功能。", "STATES": { "INHERITED": "继承", "ENABLED": "已启用", @@ -1799,7 +1794,8 @@ "id": "Bahasa Indonesia", "hu": "Magyar", "ko": "한국어", - "ro": "Română" + "ro": "Română", + "tr": "Türkçe" }, "KEYS": { "emailVerificationDoneText": "电子邮件验证完成", @@ -2754,7 +2750,8 @@ "id": "Bahasa Indonesia", "hu": "Magyar", "ko": "한국어", - "ro": "Română" + "ro": "Română", + "tr": "Türkçe" }, "MEMBER": { "ADD": "添加管理者", diff --git a/console/turbo.json b/console/turbo.json new file mode 100644 index 0000000000..f92aab1119 --- /dev/null +++ b/console/turbo.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://turbo.build/schema.json", + "extends": ["//"], + "tasks": { + "generate": { + "dependsOn": ["@zitadel/proto#generate"], + "outputs": ["src/app/proto/generated/**"] + }, + "build": { + "dependsOn": ["generate", "@zitadel/client#build"], + "outputs": ["dist/**"] + }, + "dev": { + "dependsOn": ["generate", "@zitadel/client#build"], + "cache": false, + "persistent": true + }, + "start": { + "dependsOn": ["generate", "@zitadel/client#build"], + "cache": false, + "persistent": true + } + } +} diff --git a/console/yarn.lock b/console/yarn.lock deleted file mode 100644 index 3763923846..0000000000 --- a/console/yarn.lock +++ /dev/null @@ -1,9336 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@ampproject/remapping@2.2.1": - version "2.2.1" - resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.1.tgz#99e8e11851128b8702cd57c33684f1d0f260b630" - integrity sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg== - dependencies: - "@jridgewell/gen-mapping" "^0.3.0" - "@jridgewell/trace-mapping" "^0.3.9" - -"@ampproject/remapping@^2.2.0": - version "2.3.0" - resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4" - integrity sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw== - dependencies: - "@jridgewell/gen-mapping" "^0.3.5" - "@jridgewell/trace-mapping" "^0.3.24" - -"@angular-devkit/architect@0.1602.16": - version "0.1602.16" - resolved "https://registry.yarnpkg.com/@angular-devkit/architect/-/architect-0.1602.16.tgz#7ffcaa5840037682d8205054c2c3bb84a053aeb7" - integrity sha512-aWEeGU4UlbrSKpcAZsldVNxNXAWEeu9hM2BPk77GftbRC8PBMWpgYyrJWTz2ryn8aSmGKT3T8OyBH4gZA/667w== - dependencies: - "@angular-devkit/core" "16.2.16" - rxjs "7.8.1" - -"@angular-devkit/build-angular@^16.2.2": - version "16.2.16" - resolved "https://registry.yarnpkg.com/@angular-devkit/build-angular/-/build-angular-16.2.16.tgz#8055710725e2cdf3c57a53d93a1c49eb3d41caf5" - integrity sha512-gEni21kza41xaRnVWP1sMuiWHS/rdoym5FEEGDo9PG60LwRC4lekIgT09GpTlmMu007UEfo0ccQnGroD6+MqWg== - dependencies: - "@ampproject/remapping" "2.2.1" - "@angular-devkit/architect" "0.1602.16" - "@angular-devkit/build-webpack" "0.1602.16" - "@angular-devkit/core" "16.2.16" - "@babel/core" "7.22.9" - "@babel/generator" "7.22.9" - "@babel/helper-annotate-as-pure" "7.22.5" - "@babel/helper-split-export-declaration" "7.22.6" - "@babel/plugin-proposal-async-generator-functions" "7.20.7" - "@babel/plugin-transform-async-to-generator" "7.22.5" - "@babel/plugin-transform-runtime" "7.22.9" - "@babel/preset-env" "7.22.9" - "@babel/runtime" "7.22.6" - "@babel/template" "7.22.5" - "@discoveryjs/json-ext" "0.5.7" - "@ngtools/webpack" "16.2.16" - "@vitejs/plugin-basic-ssl" "1.0.1" - ansi-colors "4.1.3" - autoprefixer "10.4.14" - babel-loader "9.1.3" - babel-plugin-istanbul "6.1.1" - browserslist "^4.21.5" - chokidar "3.5.3" - copy-webpack-plugin "11.0.0" - critters "0.0.20" - css-loader "6.8.1" - esbuild-wasm "0.18.17" - fast-glob "3.3.1" - guess-parser "0.4.22" - https-proxy-agent "5.0.1" - inquirer "8.2.4" - jsonc-parser "3.2.0" - karma-source-map-support "1.4.0" - less "4.1.3" - less-loader "11.1.0" - license-webpack-plugin "4.0.2" - loader-utils "3.2.1" - magic-string "0.30.1" - mini-css-extract-plugin "2.7.6" - mrmime "1.0.1" - open "8.4.2" - ora "5.4.1" - parse5-html-rewriting-stream "7.0.0" - picomatch "2.3.1" - piscina "4.0.0" - postcss "8.4.31" - postcss-loader "7.3.3" - resolve-url-loader "5.0.0" - rxjs "7.8.1" - sass "1.64.1" - sass-loader "13.3.2" - semver "7.5.4" - source-map-loader "4.0.1" - source-map-support "0.5.21" - terser "5.19.2" - text-table "0.2.0" - tree-kill "1.2.2" - tslib "2.6.1" - vite "4.5.5" - webpack "5.94.0" - webpack-dev-middleware "6.1.2" - webpack-dev-server "4.15.1" - webpack-merge "5.9.0" - webpack-subresource-integrity "5.1.0" - optionalDependencies: - esbuild "0.18.17" - -"@angular-devkit/build-webpack@0.1602.16": - version "0.1602.16" - resolved "https://registry.yarnpkg.com/@angular-devkit/build-webpack/-/build-webpack-0.1602.16.tgz#0276782df0484b0d976d4913d798b78419e848f1" - integrity sha512-b99Sj0btI0C2GIfzoyP8epDMIOLqSTqXOxw6klGtBLaGZfM5KAxqFzekXh8cAnHxWCj20WdNhezS1eUTLOkaIA== - dependencies: - "@angular-devkit/architect" "0.1602.16" - rxjs "7.8.1" - -"@angular-devkit/core@16.2.16": - version "16.2.16" - resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-16.2.16.tgz#078c48790c7b2b87eb5f78c6b724c4d28cbe26a1" - integrity sha512-5xHs9JFmp78sydrOAg0UGErxfMVv5c2f3RXoikS7eBOOXTWEi5pmnOkOvSJ3loQFGVs3Y7i+u02G3VrF5ZxOrA== - dependencies: - ajv "8.12.0" - ajv-formats "2.1.1" - jsonc-parser "3.2.0" - picomatch "2.3.1" - rxjs "7.8.1" - source-map "0.7.4" - -"@angular-devkit/schematics@16.2.16": - version "16.2.16" - resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-16.2.16.tgz#7f8dbf16fe440f0a9d2b5b25d0dbe16b73356216" - integrity sha512-pF6fdtJh6yLmgA7Gs45JIdxPl2MsTAhYcZIMrX1a6ID64dfwtF0MP8fDE6vrWInV1zXbzzf7l7PeKuqVtTSzKg== - dependencies: - "@angular-devkit/core" "16.2.16" - jsonc-parser "3.2.0" - magic-string "0.30.1" - ora "5.4.1" - rxjs "7.8.1" - -"@angular-eslint/builder@18.3.0": - version "18.3.0" - resolved "https://registry.yarnpkg.com/@angular-eslint/builder/-/builder-18.3.0.tgz#e4a62f45c1d2c37572be4018ec2eefd515d1aacc" - integrity sha512-httEQyqyBw3+0CRtAa7muFxHrauRfkEfk/jmrh5fn2Eiu+I53hAqFPgrwVi1V6AP/kj2zbAiWhd5xM3pMJdoRQ== - -"@angular-eslint/bundled-angular-compiler@16.2.0": - version "16.2.0" - resolved "https://registry.yarnpkg.com/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-16.2.0.tgz#09d0637d738850a2c6f0523f19632e992f790102" - integrity sha512-ct9orDYxkMl2+uvM7UBfgV28Dq57V4dEs+Drh7cD673JIMa6sXbgmd0QEtm8W3cmyK/jcTzmuoufxbH7hOxd6g== - -"@angular-eslint/bundled-angular-compiler@18.0.0": - version "18.0.0" - resolved "https://registry.yarnpkg.com/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-18.0.0.tgz#b95769124ccbfed6a313e0b0b56c4c7fd90eef30" - integrity sha512-c5XNfpWN6vfMoZpnLLeras7nUIVI10ofJu3W3s1s1NpCjP67kY84SPYRJIND1LemVewMQ+mhnP4xJnqvJxC1tA== - -"@angular-eslint/bundled-angular-compiler@18.3.0": - version "18.3.0" - resolved "https://registry.yarnpkg.com/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-18.3.0.tgz#086bf5d529e60a864bbcf3d448ccb9544bfd9b86" - integrity sha512-v/59FxUKnMzymVce99gV43huxoqXWMb85aKvzlNvLN+ScDu6ZE4YMiTQNpfapVL2lkxhs0uwB3jH17EYd5TcsA== - -"@angular-eslint/eslint-plugin-template@16.2.0": - version "16.2.0" - resolved "https://registry.yarnpkg.com/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-16.2.0.tgz#5d1dd0f450020c9bc8d9cbd5fcbf173b15ff3bd3" - integrity sha512-YFdQ6hHX6NlQj0lfogZwfyKjU8pqkJU+Zsk0ehjlXP8VfKFVmDeQT5/Xr6Df9C8pveC3hvq6Jgd8vo67S9Enxg== - dependencies: - "@angular-eslint/bundled-angular-compiler" "16.2.0" - "@angular-eslint/utils" "16.2.0" - "@typescript-eslint/type-utils" "5.62.0" - "@typescript-eslint/utils" "5.62.0" - aria-query "5.3.0" - axobject-query "3.2.1" - -"@angular-eslint/eslint-plugin-template@18.0.0": - version "18.0.0" - resolved "https://registry.yarnpkg.com/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-18.0.0.tgz#d355504af560487b3177fc8ecf62fee292a8e29b" - integrity sha512-KN32zW5eutRLumjJNGM77pZ4dpQe/PlffU2fGGVagHSDRrjaEqBmJ/khecUHjz3+VxYLbVWBM2skfb5jC4Lr2g== - dependencies: - "@angular-eslint/bundled-angular-compiler" "18.0.0" - "@angular-eslint/utils" "18.0.0" - "@typescript-eslint/utils" "8.0.0-alpha.20" - aria-query "5.3.0" - axobject-query "4.0.0" - -"@angular-eslint/eslint-plugin@16.2.0": - version "16.2.0" - resolved "https://registry.yarnpkg.com/@angular-eslint/eslint-plugin/-/eslint-plugin-16.2.0.tgz#2d61d087d208f347c9c472ecd9b0eee1fae1b21b" - integrity sha512-zdiAIox1T+B71HL+A8m+1jWdU34nvPGLhCRw/uZNwHzknsF4tYzNQ9W7T/SC/g/2s1yT2yNosEVNJSGSFvunJg== - dependencies: - "@angular-eslint/utils" "16.2.0" - "@typescript-eslint/utils" "5.62.0" - -"@angular-eslint/eslint-plugin@18.0.0": - version "18.0.0" - resolved "https://registry.yarnpkg.com/@angular-eslint/eslint-plugin/-/eslint-plugin-18.0.0.tgz#67982243625f66a8fb0141d34df6c44855bd6977" - integrity sha512-XhsIR28HiFOg3qbyjr0ZFBvOeFSXowbriFn8pAuiUjYoLJEtNZzPA1Ih/J0Ky5ZXYwcSJbZRQdNR/q1INQEFqA== - dependencies: - "@angular-eslint/bundled-angular-compiler" "18.0.0" - "@angular-eslint/utils" "18.0.0" - "@typescript-eslint/utils" "8.0.0-alpha.20" - -"@angular-eslint/schematics@16.2.0": - version "16.2.0" - resolved "https://registry.yarnpkg.com/@angular-eslint/schematics/-/schematics-16.2.0.tgz#587321a0813beede1ec7fe50413ca089bb4f6bb7" - integrity sha512-2JUVR7hAKx37mgWeDjvyWEMH5uSeeksYuaQT5wwlgIzgrO4BNFuqs6Rgyp2jiYa7BFMX/qHULSa+bSq5J5ceEA== - dependencies: - "@angular-eslint/eslint-plugin" "16.2.0" - "@angular-eslint/eslint-plugin-template" "16.2.0" - "@nx/devkit" "16.5.1" - ignore "5.2.4" - nx "16.5.1" - strip-json-comments "3.1.1" - tmp "0.2.1" - -"@angular-eslint/template-parser@18.3.0": - version "18.3.0" - resolved "https://registry.yarnpkg.com/@angular-eslint/template-parser/-/template-parser-18.3.0.tgz#c64e7e5a5dba9599d23fb3a0ac2e1aef101eeeec" - integrity sha512-1mUquqcnugI4qsoxcYZKZ6WMi6RPelDcJZg2YqGyuaIuhWmi3ZqJZLErSSpjP60+TbYZu7wM8Kchqa1bwJtEaQ== - dependencies: - "@angular-eslint/bundled-angular-compiler" "18.3.0" - eslint-scope "^8.0.2" - -"@angular-eslint/utils@16.2.0": - version "16.2.0" - resolved "https://registry.yarnpkg.com/@angular-eslint/utils/-/utils-16.2.0.tgz#8e6f0c372200049b22bca37e5537034b6f8618d2" - integrity sha512-NxMRwnlIgzmbJQfWkfd9y3Sz0hzjFdK5LH44i+3D5NhpPdZ6SzwHAjMYWoYsmmNQX5tlDXoicYF9Mz9Wz8DJ/A== - dependencies: - "@angular-eslint/bundled-angular-compiler" "16.2.0" - "@typescript-eslint/utils" "5.62.0" - -"@angular-eslint/utils@18.0.0": - version "18.0.0" - resolved "https://registry.yarnpkg.com/@angular-eslint/utils/-/utils-18.0.0.tgz#1ddedf84d3ff5275387d35b22a974f54f5eb33f2" - integrity sha512-ygOlsC5HrknbI8Ah5pa6tGtrpxB0W4UqzZG9Ii7whoWs7OjkBTIbeNy/qaWv1e45MR2/Ytd5BSWK17w0Poyz8w== - dependencies: - "@angular-eslint/bundled-angular-compiler" "18.0.0" - "@typescript-eslint/utils" "8.0.0-alpha.20" - -"@angular/animations@^16.2.12": - version "16.2.12" - resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-16.2.12.tgz#27744d8176e09e70e0f6d837c3abcfcee843a936" - integrity sha512-MD0ElviEfAJY8qMOd6/jjSSvtqER2RDAi0lxe6EtUacC1DHCYkaPrKW4vLqY+tmZBg1yf+6n+uS77pXcHHcA3w== - dependencies: - tslib "^2.3.0" - -"@angular/cdk@^16.2.14": - version "16.2.14" - resolved "https://registry.yarnpkg.com/@angular/cdk/-/cdk-16.2.14.tgz#d26f8f1e7d2466b509e60489b6acf31bfe923acf" - integrity sha512-n6PrGdiVeSTEmM/HEiwIyg6YQUUymZrb5afaNLGFRM5YL0Y8OBqd+XhCjb0OfD/AfgCUtedVEPwNqrfW8KzgGw== - dependencies: - tslib "^2.3.0" - optionalDependencies: - parse5 "^7.1.2" - -"@angular/cli@^16.2.15": - version "16.2.16" - resolved "https://registry.yarnpkg.com/@angular/cli/-/cli-16.2.16.tgz#8886e63c75bbce91c5a550eeaf3b283eaef89533" - integrity sha512-aqfNYZ45ndrf36i+7AhQ9R8BCm025j7TtYaUmvvjT4LwiUg6f6KtlZPB/ivBlXmd1g9oXqW4advL0AIi8A/Ozg== - dependencies: - "@angular-devkit/architect" "0.1602.16" - "@angular-devkit/core" "16.2.16" - "@angular-devkit/schematics" "16.2.16" - "@schematics/angular" "16.2.16" - "@yarnpkg/lockfile" "1.1.0" - ansi-colors "4.1.3" - ini "4.1.1" - inquirer "8.2.4" - jsonc-parser "3.2.0" - npm-package-arg "10.1.0" - npm-pick-manifest "8.0.1" - open "8.4.2" - ora "5.4.1" - pacote "15.2.0" - resolve "1.22.2" - semver "7.5.4" - symbol-observable "4.0.0" - yargs "17.7.2" - -"@angular/common@^16.2.12": - version "16.2.12" - resolved "https://registry.yarnpkg.com/@angular/common/-/common-16.2.12.tgz#aa1d1522701833f1998001caa1ac95c3ac11d077" - integrity sha512-B+WY/cT2VgEaz9HfJitBmgdk4I333XG/ybC98CMC4Wz8E49T8yzivmmxXB3OD6qvjcOB6ftuicl6WBqLbZNg2w== - dependencies: - tslib "^2.3.0" - -"@angular/compiler-cli@^16.2.5": - version "16.2.12" - resolved "https://registry.yarnpkg.com/@angular/compiler-cli/-/compiler-cli-16.2.12.tgz#e24b4bdaf23047b23d7b39e295b7d25b38c5734c" - integrity sha512-pWSrr152562ujh6lsFZR8NfNc5Ljj+zSTQO44DsuB0tZjwEpnRcjJEgzuhGXr+CoiBf+jTSPZKemtSktDk5aaA== - dependencies: - "@babel/core" "7.23.2" - "@jridgewell/sourcemap-codec" "^1.4.14" - chokidar "^3.0.0" - convert-source-map "^1.5.1" - reflect-metadata "^0.1.2" - semver "^7.0.0" - tslib "^2.3.0" - yargs "^17.2.1" - -"@angular/compiler@^16.2.12": - version "16.2.12" - resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-16.2.12.tgz#d13366f190706c270b925495fbc12c29097e6b6c" - integrity sha512-6SMXUgSVekGM7R6l1Z9rCtUGtlg58GFmgbpMCsGf+VXxP468Njw8rjT2YZkf5aEPxEuRpSHhDYjqz7n14cwCXQ== - dependencies: - tslib "^2.3.0" - -"@angular/core@^16.2.12": - version "16.2.12" - resolved "https://registry.yarnpkg.com/@angular/core/-/core-16.2.12.tgz#f664204275ee5f5eb46bddc0867e7a514731605f" - integrity sha512-GLLlDeke/NjroaLYOks0uyzFVo6HyLl7VOm0K1QpLXnYvW63W9Ql/T3yguRZa7tRkOAeFZ3jw+1wnBD4O8MoUA== - dependencies: - tslib "^2.3.0" - -"@angular/forms@^16.2.12": - version "16.2.12" - resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-16.2.12.tgz#a533ad61a65080281e709ca68840a1da9f189afc" - integrity sha512-1Eao89hlBgLR3v8tU91vccn21BBKL06WWxl7zLpQmG6Hun+2jrThgOE4Pf3os4fkkbH4Apj0tWL2fNIWe/blbw== - dependencies: - tslib "^2.3.0" - -"@angular/language-service@^18.2.4": - version "18.2.13" - resolved "https://registry.yarnpkg.com/@angular/language-service/-/language-service-18.2.13.tgz#dd5555f7a74ffd5ba361e5bb615d6ea1e1719f93" - integrity sha512-4E4VJDrbOAxS69F9C1twQPbR9AjY47Qlz8+lwg5lJOyUJ4GoEThLbXKfadt/vIeYBwMJ7fIsYWXD0Dlmxh4k+w== - -"@angular/material-moment-adapter@^16.2.14": - version "16.2.14" - resolved "https://registry.yarnpkg.com/@angular/material-moment-adapter/-/material-moment-adapter-16.2.14.tgz#d6972a50fcbb21483ba2888c577e443bebd0b6e6" - integrity sha512-LagTDXEq8XOVLy8CVswCbmq7v9bb84+VikEEN09tz831U/7PHjDZ3xRgpKtv7hXrh8cTZOg3UPQw5tZk0hwh3Q== - dependencies: - tslib "^2.3.0" - -"@angular/material@^16.2.14": - version "16.2.14" - resolved "https://registry.yarnpkg.com/@angular/material/-/material-16.2.14.tgz#4db0c7d14d3d6ac6c8dac83dced0fb8a030b3b49" - integrity sha512-zQIxUb23elPfiIvddqkIDYqQhAHa9ZwMblfbv+ug8bxr4D0Dw360jIarxCgMjAcLj7Ccl3GBqZMUnVeM6cjthw== - dependencies: - "@material/animation" "15.0.0-canary.bc9ae6c9c.0" - "@material/auto-init" "15.0.0-canary.bc9ae6c9c.0" - "@material/banner" "15.0.0-canary.bc9ae6c9c.0" - "@material/base" "15.0.0-canary.bc9ae6c9c.0" - "@material/button" "15.0.0-canary.bc9ae6c9c.0" - "@material/card" "15.0.0-canary.bc9ae6c9c.0" - "@material/checkbox" "15.0.0-canary.bc9ae6c9c.0" - "@material/chips" "15.0.0-canary.bc9ae6c9c.0" - "@material/circular-progress" "15.0.0-canary.bc9ae6c9c.0" - "@material/data-table" "15.0.0-canary.bc9ae6c9c.0" - "@material/density" "15.0.0-canary.bc9ae6c9c.0" - "@material/dialog" "15.0.0-canary.bc9ae6c9c.0" - "@material/dom" "15.0.0-canary.bc9ae6c9c.0" - "@material/drawer" "15.0.0-canary.bc9ae6c9c.0" - "@material/elevation" "15.0.0-canary.bc9ae6c9c.0" - "@material/fab" "15.0.0-canary.bc9ae6c9c.0" - "@material/feature-targeting" "15.0.0-canary.bc9ae6c9c.0" - "@material/floating-label" "15.0.0-canary.bc9ae6c9c.0" - "@material/form-field" "15.0.0-canary.bc9ae6c9c.0" - "@material/icon-button" "15.0.0-canary.bc9ae6c9c.0" - "@material/image-list" "15.0.0-canary.bc9ae6c9c.0" - "@material/layout-grid" "15.0.0-canary.bc9ae6c9c.0" - "@material/line-ripple" "15.0.0-canary.bc9ae6c9c.0" - "@material/linear-progress" "15.0.0-canary.bc9ae6c9c.0" - "@material/list" "15.0.0-canary.bc9ae6c9c.0" - "@material/menu" "15.0.0-canary.bc9ae6c9c.0" - "@material/menu-surface" "15.0.0-canary.bc9ae6c9c.0" - "@material/notched-outline" "15.0.0-canary.bc9ae6c9c.0" - "@material/radio" "15.0.0-canary.bc9ae6c9c.0" - "@material/ripple" "15.0.0-canary.bc9ae6c9c.0" - "@material/rtl" "15.0.0-canary.bc9ae6c9c.0" - "@material/segmented-button" "15.0.0-canary.bc9ae6c9c.0" - "@material/select" "15.0.0-canary.bc9ae6c9c.0" - "@material/shape" "15.0.0-canary.bc9ae6c9c.0" - "@material/slider" "15.0.0-canary.bc9ae6c9c.0" - "@material/snackbar" "15.0.0-canary.bc9ae6c9c.0" - "@material/switch" "15.0.0-canary.bc9ae6c9c.0" - "@material/tab" "15.0.0-canary.bc9ae6c9c.0" - "@material/tab-bar" "15.0.0-canary.bc9ae6c9c.0" - "@material/tab-indicator" "15.0.0-canary.bc9ae6c9c.0" - "@material/tab-scroller" "15.0.0-canary.bc9ae6c9c.0" - "@material/textfield" "15.0.0-canary.bc9ae6c9c.0" - "@material/theme" "15.0.0-canary.bc9ae6c9c.0" - "@material/tooltip" "15.0.0-canary.bc9ae6c9c.0" - "@material/top-app-bar" "15.0.0-canary.bc9ae6c9c.0" - "@material/touch-target" "15.0.0-canary.bc9ae6c9c.0" - "@material/typography" "15.0.0-canary.bc9ae6c9c.0" - tslib "^2.3.0" - -"@angular/platform-browser-dynamic@^16.2.12": - version "16.2.12" - resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-16.2.12.tgz#14488188c06013eb4153ac6e0603975f8b679f70" - integrity sha512-ya54jerNgreCVAR278wZavwjrUWImMr2F8yM5n9HBvsMBbFaAQ83anwbOEiHEF2BlR+gJiEBLfpuPRMw20pHqw== - dependencies: - tslib "^2.3.0" - -"@angular/platform-browser@^16.2.12": - version "16.2.12" - resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-16.2.12.tgz#66b5611066cb3f8bb55f035658e978b50720f3b0" - integrity sha512-NnH7ju1iirmVEsUq432DTm0nZBGQsBrU40M3ZeVHMQ2subnGiyUs3QyzDz8+VWLL/T5xTxWLt9BkDn65vgzlIQ== - dependencies: - tslib "^2.3.0" - -"@angular/router@^16.2.12": - version "16.2.12" - resolved "https://registry.yarnpkg.com/@angular/router/-/router-16.2.12.tgz#2f4cae64ddb7f998832aa340dd3f843cfb85cbc8" - integrity sha512-aU6QnYSza005V9P3W6PpkieL56O0IHps96DjqI1RS8yOJUl3THmokqYN4Fm5+HXy4f390FN9i6ftadYQDKeWmA== - dependencies: - tslib "^2.3.0" - -"@angular/service-worker@^16.2.12": - version "16.2.12" - resolved "https://registry.yarnpkg.com/@angular/service-worker/-/service-worker-16.2.12.tgz#359e72693de7d1e8015d1beb02689753ede96de6" - integrity sha512-o0z0s4c76NmRASa+mUHn/q6vUKQNa06mGmLBDKm84vRQ1sQ2TJv+R1p8K9WkiM5mGy6tjQCDOgaz13TcxMFWOQ== - dependencies: - tslib "^2.3.0" - -"@assemblyscript/loader@^0.10.1": - version "0.10.1" - resolved "https://registry.yarnpkg.com/@assemblyscript/loader/-/loader-0.10.1.tgz#70e45678f06c72fa2e350e8553ec4a4d72b92e06" - integrity sha512-H71nDOOL8Y7kWRLqf6Sums+01Q5msqBW2KhDUTemh1tvY04eSkSXrK0uj/4mmY0Xr16/3zyZmsrxN7CKuRbNRg== - -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.26.2": - version "7.26.2" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.26.2.tgz#4b5fab97d33338eff916235055f0ebc21e573a85" - integrity sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ== - dependencies: - "@babel/helper-validator-identifier" "^7.25.9" - js-tokens "^4.0.0" - picocolors "^1.0.0" - -"@babel/code-frame@^7.22.13", "@babel/code-frame@^7.22.5", "@babel/code-frame@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.25.7.tgz#438f2c524071531d643c6f0188e1e28f130cebc7" - integrity sha512-0xZJFNE5XMpENsgfHYTw8FbX4kv53mFLn2i3XPoq69LyhYSCBJtitaHx9QnsVTrsogI4Z3+HtEfZ2/GFPOtf5g== - dependencies: - "@babel/highlight" "^7.25.7" - picocolors "^1.0.0" - -"@babel/compat-data@^7.22.6", "@babel/compat-data@^7.22.9", "@babel/compat-data@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.25.7.tgz#b8479fe0018ef0ac87b6b7a5c6916fcd67ae2c9c" - integrity sha512-9ickoLz+hcXCeh7jrcin+/SLWm+GkxE2kTvoYyp38p4WkdFXfQJxDFGWp/YHjiKLPx06z2A7W8XKuqbReXDzsw== - -"@babel/core@7.22.9": - version "7.22.9" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.22.9.tgz#bd96492c68822198f33e8a256061da3cf391f58f" - integrity sha512-G2EgeufBcYw27U4hhoIwFcgc1XU7TlXJ3mv04oOv1WCuo900U/anZSPzEqNjwdjgffkk2Gs0AN0dW1CKVLcG7w== - dependencies: - "@ampproject/remapping" "^2.2.0" - "@babel/code-frame" "^7.22.5" - "@babel/generator" "^7.22.9" - "@babel/helper-compilation-targets" "^7.22.9" - "@babel/helper-module-transforms" "^7.22.9" - "@babel/helpers" "^7.22.6" - "@babel/parser" "^7.22.7" - "@babel/template" "^7.22.5" - "@babel/traverse" "^7.22.8" - "@babel/types" "^7.22.5" - convert-source-map "^1.7.0" - debug "^4.1.0" - gensync "^1.0.0-beta.2" - json5 "^2.2.2" - semver "^6.3.1" - -"@babel/core@7.23.2": - version "7.23.2" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.23.2.tgz#ed10df0d580fff67c5f3ee70fd22e2e4c90a9f94" - integrity sha512-n7s51eWdaWZ3vGT2tD4T7J6eJs3QoBXydv7vkUM06Bf1cbVD2Kc2UrkzhiQwobfV7NwOnQXYL7UBJ5VPU+RGoQ== - dependencies: - "@ampproject/remapping" "^2.2.0" - "@babel/code-frame" "^7.22.13" - "@babel/generator" "^7.23.0" - "@babel/helper-compilation-targets" "^7.22.15" - "@babel/helper-module-transforms" "^7.23.0" - "@babel/helpers" "^7.23.2" - "@babel/parser" "^7.23.0" - "@babel/template" "^7.22.15" - "@babel/traverse" "^7.23.2" - "@babel/types" "^7.23.0" - convert-source-map "^2.0.0" - debug "^4.1.0" - gensync "^1.0.0-beta.2" - json5 "^2.2.3" - semver "^6.3.1" - -"@babel/core@^7.12.3": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.25.7.tgz#1b3d144157575daf132a3bc80b2b18e6e3ca6ece" - integrity sha512-yJ474Zv3cwiSOO9nXJuqzvwEeM+chDuQ8GJirw+pZ91sCGCyOZ3dJkVE09fTV0VEVzXyLWhh3G/AolYTPX7Mow== - dependencies: - "@ampproject/remapping" "^2.2.0" - "@babel/code-frame" "^7.25.7" - "@babel/generator" "^7.25.7" - "@babel/helper-compilation-targets" "^7.25.7" - "@babel/helper-module-transforms" "^7.25.7" - "@babel/helpers" "^7.25.7" - "@babel/parser" "^7.25.7" - "@babel/template" "^7.25.7" - "@babel/traverse" "^7.25.7" - "@babel/types" "^7.25.7" - convert-source-map "^2.0.0" - debug "^4.1.0" - gensync "^1.0.0-beta.2" - json5 "^2.2.3" - semver "^6.3.1" - -"@babel/generator@7.22.9": - version "7.22.9" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.22.9.tgz#572ecfa7a31002fa1de2a9d91621fd895da8493d" - integrity sha512-KtLMbmicyuK2Ak/FTCJVbDnkN1SlT8/kceFTiuDiiRUUSMnHMidxSCdG4ndkTOHHpoomWe/4xkvHkEOncwjYIw== - dependencies: - "@babel/types" "^7.22.5" - "@jridgewell/gen-mapping" "^0.3.2" - "@jridgewell/trace-mapping" "^0.3.17" - jsesc "^2.5.1" - -"@babel/generator@^7.22.9", "@babel/generator@^7.23.0", "@babel/generator@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.25.7.tgz#de86acbeb975a3e11ee92dd52223e6b03b479c56" - integrity sha512-5Dqpl5fyV9pIAD62yK9P7fcA768uVPUyrQmqpqstHWgMma4feF1x/oFysBCVZLY5wJ2GkMUCdsNDnGZrPoR6rA== - dependencies: - "@babel/types" "^7.25.7" - "@jridgewell/gen-mapping" "^0.3.5" - "@jridgewell/trace-mapping" "^0.3.25" - jsesc "^3.0.2" - -"@babel/helper-annotate-as-pure@7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz#e7f06737b197d580a01edf75d97e2c8be99d3882" - integrity sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg== - dependencies: - "@babel/types" "^7.22.5" - -"@babel/helper-annotate-as-pure@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.7.tgz#63f02dbfa1f7cb75a9bdb832f300582f30bb8972" - integrity sha512-4xwU8StnqnlIhhioZf1tqnVWeQ9pvH/ujS8hRfw/WOza+/a+1qv69BWNy+oY231maTCWgKWhfBU7kDpsds6zAA== - dependencies: - "@babel/types" "^7.25.7" - -"@babel/helper-builder-binary-assignment-operator-visitor@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.25.7.tgz#d721650c1f595371e0a23ee816f1c3c488c0d622" - integrity sha512-12xfNeKNH7jubQNm7PAkzlLwEmCs1tfuX3UjIw6vP6QXi+leKh6+LyC/+Ed4EIQermwd58wsyh070yjDHFlNGg== - dependencies: - "@babel/traverse" "^7.25.7" - "@babel/types" "^7.25.7" - -"@babel/helper-compilation-targets@^7.22.15", "@babel/helper-compilation-targets@^7.22.6", "@babel/helper-compilation-targets@^7.22.9", "@babel/helper-compilation-targets@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.7.tgz#11260ac3322dda0ef53edfae6e97b961449f5fa4" - integrity sha512-DniTEax0sv6isaw6qSQSfV4gVRNtw2rte8HHM45t9ZR0xILaufBRNkpMifCRiAPyvL4ACD6v0gfCwCmtOQaV4A== - dependencies: - "@babel/compat-data" "^7.25.7" - "@babel/helper-validator-option" "^7.25.7" - browserslist "^4.24.0" - lru-cache "^5.1.1" - semver "^6.3.1" - -"@babel/helper-create-class-features-plugin@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.7.tgz#5d65074c76cae75607421c00d6bd517fe1892d6b" - integrity sha512-bD4WQhbkx80mAyj/WCm4ZHcF4rDxkoLFO6ph8/5/mQ3z4vAzltQXAmbc7GvVJx5H+lk5Mi5EmbTeox5nMGCsbw== - dependencies: - "@babel/helper-annotate-as-pure" "^7.25.7" - "@babel/helper-member-expression-to-functions" "^7.25.7" - "@babel/helper-optimise-call-expression" "^7.25.7" - "@babel/helper-replace-supers" "^7.25.7" - "@babel/helper-skip-transparent-expression-wrappers" "^7.25.7" - "@babel/traverse" "^7.25.7" - semver "^6.3.1" - -"@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.25.7.tgz#dcb464f0e2cdfe0c25cc2a0a59c37ab940ce894e" - integrity sha512-byHhumTj/X47wJ6C6eLpK7wW/WBEcnUeb7D0FNc/jFQnQVw7DOso3Zz5u9x/zLrFVkHa89ZGDbkAa1D54NdrCQ== - dependencies: - "@babel/helper-annotate-as-pure" "^7.25.7" - regexpu-core "^6.1.1" - semver "^6.3.1" - -"@babel/helper-define-polyfill-provider@^0.4.4": - version "0.4.4" - resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.4.tgz#64df615451cb30e94b59a9696022cffac9a10088" - integrity sha512-QcJMILQCu2jm5TFPGA3lCpJJTeEP+mqeXooG/NZbg/h5FTFi6V0+99ahlRsW8/kRLyb24LZVCCiclDedhLKcBA== - 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" - -"@babel/helper-define-polyfill-provider@^0.5.0": - version "0.5.0" - resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.5.0.tgz#465805b7361f461e86c680f1de21eaf88c25901b" - integrity sha512-NovQquuQLAQ5HuyjCz7WQP9MjRj7dx++yspwiyUiGl9ZyadHRSql1HZh5ogRd8W8w6YM6EQ/NTB8rgjLt5W65Q== - 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" - -"@babel/helper-define-polyfill-provider@^0.6.2": - version "0.6.2" - resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.2.tgz#18594f789c3594acb24cfdb4a7f7b7d2e8bd912d" - integrity sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ== - 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" - -"@babel/helper-environment-visitor@^7.18.9": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz#4b31ba9551d1f90781ba83491dd59cf9b269f7d9" - integrity sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ== - dependencies: - "@babel/types" "^7.24.7" - -"@babel/helper-member-expression-to-functions@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.7.tgz#541a33b071f0355a63a0fa4bdf9ac360116b8574" - integrity sha512-O31Ssjd5K6lPbTX9AAYpSKrZmLeagt9uwschJd+Ixo6QiRyfpvgtVQp8qrDR9UNFjZ8+DO34ZkdrN+BnPXemeA== - dependencies: - "@babel/traverse" "^7.25.7" - "@babel/types" "^7.25.7" - -"@babel/helper-module-imports@^7.22.5", "@babel/helper-module-imports@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.25.7.tgz#dba00d9523539152906ba49263e36d7261040472" - integrity sha512-o0xCgpNmRohmnoWKQ0Ij8IdddjyBFE4T2kagL/x6M3+4zUgc+4qTOUBoNe4XxDskt1HPKO007ZPiMgLDq2s7Kw== - dependencies: - "@babel/traverse" "^7.25.7" - "@babel/types" "^7.25.7" - -"@babel/helper-module-transforms@^7.22.9", "@babel/helper-module-transforms@^7.23.0", "@babel/helper-module-transforms@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.25.7.tgz#2ac9372c5e001b19bc62f1fe7d96a18cb0901d1a" - integrity sha512-k/6f8dKG3yDz/qCwSM+RKovjMix563SLxQFo0UhRNo239SP6n9u5/eLtKD6EAjwta2JHJ49CsD8pms2HdNiMMQ== - dependencies: - "@babel/helper-module-imports" "^7.25.7" - "@babel/helper-simple-access" "^7.25.7" - "@babel/helper-validator-identifier" "^7.25.7" - "@babel/traverse" "^7.25.7" - -"@babel/helper-optimise-call-expression@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.7.tgz#1de1b99688e987af723eed44fa7fc0ee7b97d77a" - integrity sha512-VAwcwuYhv/AT+Vfr28c9y6SHzTan1ryqrydSTFGjU0uDJHw3uZ+PduI8plCLkRsDnqK2DMEDmwrOQRsK/Ykjng== - dependencies: - "@babel/types" "^7.25.7" - -"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.20.2", "@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.25.7", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.7.tgz#8ec5b21812d992e1ef88a9b068260537b6f0e36c" - integrity sha512-eaPZai0PiqCi09pPs3pAFfl/zYgGaE6IdXtYvmf0qlcDTd3WCtO7JWCcRd64e0EQrcYgiHibEZnOGsSY4QSgaw== - -"@babel/helper-remap-async-to-generator@^7.18.9", "@babel/helper-remap-async-to-generator@^7.22.5", "@babel/helper-remap-async-to-generator@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.7.tgz#9efdc39df5f489bcd15533c912b6c723a0a65021" - integrity sha512-kRGE89hLnPfcz6fTrlNU+uhgcwv0mBE4Gv3P9Ke9kLVJYpi4AMVVEElXvB5CabrPZW4nCM8P8UyyjrzCM0O2sw== - dependencies: - "@babel/helper-annotate-as-pure" "^7.25.7" - "@babel/helper-wrap-function" "^7.25.7" - "@babel/traverse" "^7.25.7" - -"@babel/helper-replace-supers@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.25.7.tgz#38cfda3b6e990879c71d08d0fef9236b62bd75f5" - integrity sha512-iy8JhqlUW9PtZkd4pHM96v6BdJ66Ba9yWSE4z0W4TvSZwLBPkyDsiIU3ENe4SmrzRBs76F7rQXTy1lYC49n6Lw== - dependencies: - "@babel/helper-member-expression-to-functions" "^7.25.7" - "@babel/helper-optimise-call-expression" "^7.25.7" - "@babel/traverse" "^7.25.7" - -"@babel/helper-simple-access@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.25.7.tgz#5eb9f6a60c5d6b2e0f76057004f8dacbddfae1c0" - integrity sha512-FPGAkJmyoChQeM+ruBGIDyrT2tKfZJO8NcxdC+CWNJi7N8/rZpSxK7yvBJ5O/nF1gfu5KzN7VKG3YVSLFfRSxQ== - dependencies: - "@babel/traverse" "^7.25.7" - "@babel/types" "^7.25.7" - -"@babel/helper-skip-transparent-expression-wrappers@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.7.tgz#382831c91038b1a6d32643f5f49505b8442cb87c" - integrity sha512-pPbNbchZBkPMD50K0p3JGcFMNLVUCuU/ABybm/PGNj4JiHrpmNyqqCphBk4i19xXtNV0JhldQJJtbSW5aUvbyA== - dependencies: - "@babel/traverse" "^7.25.7" - "@babel/types" "^7.25.7" - -"@babel/helper-split-export-declaration@7.22.6": - version "7.22.6" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz#322c61b7310c0997fe4c323955667f18fcefb91c" - integrity sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g== - dependencies: - "@babel/types" "^7.22.5" - -"@babel/helper-string-parser@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.25.7.tgz#d50e8d37b1176207b4fe9acedec386c565a44a54" - integrity sha512-CbkjYdsJNHFk8uqpEkpCvRs3YRp9tY6FmFY7wLMSYuGYkrdUi7r2lc4/wqsvlHoMznX3WJ9IP8giGPq68T/Y6g== - -"@babel/helper-validator-identifier@^7.25.7", "@babel/helper-validator-identifier@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7" - integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ== - -"@babel/helper-validator-option@^7.22.5", "@babel/helper-validator-option@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.25.7.tgz#97d1d684448228b30b506d90cace495d6f492729" - integrity sha512-ytbPLsm+GjArDYXJ8Ydr1c/KJuutjF2besPNbIZnZ6MKUxi/uTA22t2ymmA4WFjZFpjiAMO0xuuJPqK2nvDVfQ== - -"@babel/helper-wrap-function@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.25.7.tgz#9f6021dd1c4fdf4ad515c809967fc4bac9a70fe7" - integrity sha512-MA0roW3JF2bD1ptAaJnvcabsVlNQShUaThyJbCDD4bCp8NEgiFvpoqRI2YS22hHlc2thjO/fTg2ShLMC3jygAg== - dependencies: - "@babel/template" "^7.25.7" - "@babel/traverse" "^7.25.7" - "@babel/types" "^7.25.7" - -"@babel/helpers@^7.22.6", "@babel/helpers@^7.23.2", "@babel/helpers@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.25.7.tgz#091b52cb697a171fe0136ab62e54e407211f09c2" - integrity sha512-Sv6pASx7Esm38KQpF/U/OXLwPPrdGHNKoeblRxgZRLXnAtnkEe4ptJPDtAZM7fBLadbc1Q07kQpSiGQ0Jg6tRA== - dependencies: - "@babel/template" "^7.25.7" - "@babel/types" "^7.25.7" - -"@babel/highlight@^7.25.7": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.25.9.tgz#8141ce68fc73757946f983b343f1231f4691acc6" - integrity sha512-llL88JShoCsth8fF8R4SJnIn+WLvR6ccFxu1H3FlMhDontdcmZWf2HgIZ7AIqV3Xcck1idlohrN4EUBQz6klbw== - dependencies: - "@babel/helper-validator-identifier" "^7.25.9" - chalk "^2.4.2" - js-tokens "^4.0.0" - picocolors "^1.0.0" - -"@babel/parser@^7.14.7", "@babel/parser@^7.22.5", "@babel/parser@^7.22.7", "@babel/parser@^7.23.0", "@babel/parser@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.25.7.tgz#99b927720f4ddbfeb8cd195a363ed4532f87c590" - integrity sha512-aZn7ETtQsjjGG5HruveUK06cU3Hljuhd9Iojm4M8WWv3wLE6OkE5PWbDUkItmMgegmccaITudyuW5RPYrYlgWw== - dependencies: - "@babel/types" "^7.25.7" - -"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.22.5": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.7.tgz#c5f755e911dfac7ef6957300c0f9c4a8c18c06f4" - integrity sha512-wxyWg2RYaSUYgmd9MR0FyRGyeOMQE/Uzr1wzd/g5cf5bwi9A4v6HFdDm7y1MgDtod/fLOSTZY6jDgV0xU9d5bA== - dependencies: - "@babel/helper-plugin-utils" "^7.25.7" - -"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.22.5": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.25.7.tgz#3b7ea04492ded990978b6deaa1dfca120ad4455a" - integrity sha512-Xwg6tZpLxc4iQjorYsyGMyfJE7nP5MV8t/Ka58BgiA7Jw0fRqQNcANlLfdJ/yvBt9z9LD2We+BEkT7vLqZRWng== - dependencies: - "@babel/helper-plugin-utils" "^7.25.7" - "@babel/helper-skip-transparent-expression-wrappers" "^7.25.7" - "@babel/plugin-transform-optional-chaining" "^7.25.7" - -"@babel/plugin-proposal-async-generator-functions@7.20.7": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.7.tgz#bfb7276d2d573cb67ba379984a2334e262ba5326" - integrity sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA== - dependencies: - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/helper-remap-async-to-generator" "^7.18.9" - "@babel/plugin-syntax-async-generators" "^7.8.4" - -"@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2": - version "7.21.0-placeholder-for-preset-env.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz#7844f9289546efa9febac2de4cfe358a050bd703" - integrity sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w== - -"@babel/plugin-proposal-unicode-property-regex@^7.4.4": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz#af613d2cd5e643643b65cded64207b15c85cb78e" - integrity sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-syntax-async-generators@^7.8.4": - version "7.8.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" - integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-class-properties@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" - integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== - dependencies: - "@babel/helper-plugin-utils" "^7.12.13" - -"@babel/plugin-syntax-class-static-block@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz#195df89b146b4b78b3bf897fd7a257c84659d406" - integrity sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-syntax-dynamic-import@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3" - integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-export-namespace-from@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz#028964a9ba80dbc094c915c487ad7c4e7a66465a" - integrity sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - -"@babel/plugin-syntax-import-assertions@^7.22.5": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.25.7.tgz#8ce248f9f4ed4b7ed4cb2e0eb4ed9efd9f52921f" - integrity sha512-ZvZQRmME0zfJnDQnVBKYzHxXT7lYBB3Revz1GuS7oLXWMgqUPX4G+DDbT30ICClht9WKV34QVrZhSw6WdklwZQ== - dependencies: - "@babel/helper-plugin-utils" "^7.25.7" - -"@babel/plugin-syntax-import-attributes@^7.22.5": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.25.7.tgz#d78dd0499d30df19a598e63ab895e21b909bc43f" - integrity sha512-AqVo+dguCgmpi/3mYBdu9lkngOBlQ2w2vnNpa6gfiCxQZLzV4ZbhsXitJ2Yblkoe1VQwtHSaNmIaGll/26YWRw== - dependencies: - "@babel/helper-plugin-utils" "^7.25.7" - -"@babel/plugin-syntax-import-meta@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51" - integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-syntax-json-strings@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" - integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-logical-assignment-operators@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" - integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" - integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-numeric-separator@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" - integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-syntax-object-rest-spread@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" - integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-optional-catch-binding@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" - integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-optional-chaining@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" - integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-private-property-in-object@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz#0dc6671ec0ea22b6e94a1114f857970cd39de1ad" - integrity sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-syntax-top-level-await@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c" - integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-syntax-unicode-sets-regex@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz#d49a3b3e6b52e5be6740022317580234a6a47357" - integrity sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-arrow-functions@^7.22.5": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.25.7.tgz#1b9ed22e6890a0e9ff470371c73b8c749bcec386" - integrity sha512-EJN2mKxDwfOUCPxMO6MUI58RN3ganiRAG/MS/S3HfB6QFNjroAMelQo/gybyYq97WerCBAZoyrAoW8Tzdq2jWg== - dependencies: - "@babel/helper-plugin-utils" "^7.25.7" - -"@babel/plugin-transform-async-generator-functions@^7.22.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.7.tgz#af61a02b30d7bff5108c63bd39ac7938403426d7" - integrity sha512-4B6OhTrwYKHYYgcwErvZjbmH9X5TxQBsaBHdzEIB4l71gR5jh/tuHGlb9in47udL2+wVUcOz5XXhhfhVJwEpEg== - dependencies: - "@babel/helper-plugin-utils" "^7.25.7" - "@babel/helper-remap-async-to-generator" "^7.25.7" - "@babel/plugin-syntax-async-generators" "^7.8.4" - "@babel/traverse" "^7.25.7" - -"@babel/plugin-transform-async-to-generator@7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.22.5.tgz#c7a85f44e46f8952f6d27fe57c2ed3cc084c3775" - integrity sha512-b1A8D8ZzE/VhNDoV1MSJTnpKkCG5bJo+19R4o4oy03zM7ws8yEMK755j61Dc3EyvdysbqH5BOOTquJ7ZX9C6vQ== - dependencies: - "@babel/helper-module-imports" "^7.22.5" - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-remap-async-to-generator" "^7.22.5" - -"@babel/plugin-transform-async-to-generator@^7.22.5": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.25.7.tgz#a44c7323f8d4285a6c568dd43c5c361d6367ec52" - integrity sha512-ZUCjAavsh5CESCmi/xCpX1qcCaAglzs/7tmuvoFnJgA1dM7gQplsguljoTg+Ru8WENpX89cQyAtWoaE0I3X3Pg== - dependencies: - "@babel/helper-module-imports" "^7.25.7" - "@babel/helper-plugin-utils" "^7.25.7" - "@babel/helper-remap-async-to-generator" "^7.25.7" - -"@babel/plugin-transform-block-scoped-functions@^7.22.5": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.25.7.tgz#e0b8843d5571719a2f1bf7e284117a3379fcc17c" - integrity sha512-xHttvIM9fvqW+0a3tZlYcZYSBpSWzGBFIt/sYG3tcdSzBB8ZeVgz2gBP7Df+sM0N1850jrviYSSeUuc+135dmQ== - dependencies: - "@babel/helper-plugin-utils" "^7.25.7" - -"@babel/plugin-transform-block-scoping@^7.22.5": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.7.tgz#6dab95e98adf780ceef1b1c3ab0e55cd20dd410a" - integrity sha512-ZEPJSkVZaeTFG/m2PARwLZQ+OG0vFIhPlKHK/JdIMy8DbRJ/htz6LRrTFtdzxi9EHmcwbNPAKDnadpNSIW+Aow== - dependencies: - "@babel/helper-plugin-utils" "^7.25.7" - -"@babel/plugin-transform-class-properties@^7.22.5": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.7.tgz#a389cfca7a10ac80e3ff4c75fca08bd097ad1523" - integrity sha512-mhyfEW4gufjIqYFo9krXHJ3ElbFLIze5IDp+wQTxoPd+mwFb1NxatNAwmv8Q8Iuxv7Zc+q8EkiMQwc9IhyGf4g== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.25.7" - "@babel/helper-plugin-utils" "^7.25.7" - -"@babel/plugin-transform-class-static-block@^7.22.5": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.25.7.tgz#d2cf3c812e3b3162d56aadf4566f45c30538cb2c" - integrity sha512-rvUUtoVlkDWtDWxGAiiQj0aNktTPn3eFynBcMC2IhsXweehwgdI9ODe+XjWw515kEmv22sSOTp/rxIRuTiB7zg== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.25.7" - "@babel/helper-plugin-utils" "^7.25.7" - "@babel/plugin-syntax-class-static-block" "^7.14.5" - -"@babel/plugin-transform-classes@^7.22.6": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.7.tgz#5103206cf80d02283bbbd044509ea3b65d0906bb" - integrity sha512-9j9rnl+YCQY0IGoeipXvnk3niWicIB6kCsWRGLwX241qSXpbA4MKxtp/EdvFxsc4zI5vqfLxzOd0twIJ7I99zg== - dependencies: - "@babel/helper-annotate-as-pure" "^7.25.7" - "@babel/helper-compilation-targets" "^7.25.7" - "@babel/helper-plugin-utils" "^7.25.7" - "@babel/helper-replace-supers" "^7.25.7" - "@babel/traverse" "^7.25.7" - globals "^11.1.0" - -"@babel/plugin-transform-computed-properties@^7.22.5": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.7.tgz#7f621f0aa1354b5348a935ab12e3903842466f65" - integrity sha512-QIv+imtM+EtNxg/XBKL3hiWjgdLjMOmZ+XzQwSgmBfKbfxUjBzGgVPklUuE55eq5/uVoh8gg3dqlrwR/jw3ZeA== - dependencies: - "@babel/helper-plugin-utils" "^7.25.7" - "@babel/template" "^7.25.7" - -"@babel/plugin-transform-destructuring@^7.22.5": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.25.7.tgz#f6f26a9feefb5aa41fd45b6f5838901b5333d560" - integrity sha512-xKcfLTlJYUczdaM1+epcdh1UGewJqr9zATgrNHcLBcV2QmfvPPEixo/sK/syql9cEmbr7ulu5HMFG5vbbt/sEA== - dependencies: - "@babel/helper-plugin-utils" "^7.25.7" - -"@babel/plugin-transform-dotall-regex@^7.22.5", "@babel/plugin-transform-dotall-regex@^7.4.4": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.25.7.tgz#9d775c4a3ff1aea64045300fcd4309b4a610ef02" - integrity sha512-kXzXMMRzAtJdDEgQBLF4oaiT6ZCU3oWHgpARnTKDAqPkDJ+bs3NrZb310YYevR5QlRo3Kn7dzzIdHbZm1VzJdQ== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.25.7" - "@babel/helper-plugin-utils" "^7.25.7" - -"@babel/plugin-transform-duplicate-keys@^7.22.5": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.25.7.tgz#fbba7d1155eab76bd4f2a038cbd5d65883bd7a93" - integrity sha512-by+v2CjoL3aMnWDOyCIg+yxU9KXSRa9tN6MbqggH5xvymmr9p4AMjYkNlQy4brMceBnUyHZ9G8RnpvT8wP7Cfg== - dependencies: - "@babel/helper-plugin-utils" "^7.25.7" - -"@babel/plugin-transform-dynamic-import@^7.22.5": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.7.tgz#31905ab2cfa94dcf1b1f8ce66096720b2908e518" - integrity sha512-UvcLuual4h7/GfylKm2IAA3aph9rwvAM2XBA0uPKU3lca+Maai4jBjjEVUS568ld6kJcgbouuumCBhMd/Yz17w== - dependencies: - "@babel/helper-plugin-utils" "^7.25.7" - "@babel/plugin-syntax-dynamic-import" "^7.8.3" - -"@babel/plugin-transform-exponentiation-operator@^7.22.5": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.25.7.tgz#5961a3a23a398faccd6cddb34a2182807d75fb5f" - integrity sha512-yjqtpstPfZ0h/y40fAXRv2snciYr0OAoMXY/0ClC7tm4C/nG5NJKmIItlaYlLbIVAWNfrYuy9dq1bE0SbX0PEg== - dependencies: - "@babel/helper-builder-binary-assignment-operator-visitor" "^7.25.7" - "@babel/helper-plugin-utils" "^7.25.7" - -"@babel/plugin-transform-export-namespace-from@^7.22.5": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.25.7.tgz#beb2679db6fd3bdfe6ad6de2c8cac84a86ef2da1" - integrity sha512-h3MDAP5l34NQkkNulsTNyjdaR+OiB0Im67VU//sFupouP8Q6m9Spy7l66DcaAQxtmCqGdanPByLsnwFttxKISQ== - dependencies: - "@babel/helper-plugin-utils" "^7.25.7" - "@babel/plugin-syntax-export-namespace-from" "^7.8.3" - -"@babel/plugin-transform-for-of@^7.22.5": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.25.7.tgz#0acfea0f27aa290818b5b48a5a44b3f03fc13669" - integrity sha512-n/TaiBGJxYFWvpJDfsxSj9lEEE44BFM1EPGz4KEiTipTgkoFVVcCmzAL3qA7fdQU96dpo4gGf5HBx/KnDvqiHw== - dependencies: - "@babel/helper-plugin-utils" "^7.25.7" - "@babel/helper-skip-transparent-expression-wrappers" "^7.25.7" - -"@babel/plugin-transform-function-name@^7.22.5": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.7.tgz#7e394ccea3693902a8b50ded8b6ae1fa7b8519fd" - integrity sha512-5MCTNcjCMxQ63Tdu9rxyN6cAWurqfrDZ76qvVPrGYdBxIj+EawuuxTu/+dgJlhK5eRz3v1gLwp6XwS8XaX2NiQ== - dependencies: - "@babel/helper-compilation-targets" "^7.25.7" - "@babel/helper-plugin-utils" "^7.25.7" - "@babel/traverse" "^7.25.7" - -"@babel/plugin-transform-json-strings@^7.22.5": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.25.7.tgz#6626433554aff4bd6f76a2c621a1f40e802dfb0a" - integrity sha512-Ot43PrL9TEAiCe8C/2erAjXMeVSnE/BLEx6eyrKLNFCCw5jvhTHKyHxdI1pA0kz5njZRYAnMO2KObGqOCRDYSA== - dependencies: - "@babel/helper-plugin-utils" "^7.25.7" - "@babel/plugin-syntax-json-strings" "^7.8.3" - -"@babel/plugin-transform-literals@^7.22.5": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.7.tgz#70cbdc742f2cfdb1a63ea2cbd018d12a60b213c3" - integrity sha512-fwzkLrSu2fESR/cm4t6vqd7ebNIopz2QHGtjoU+dswQo/P6lwAG04Q98lliE3jkz/XqnbGFLnUcE0q0CVUf92w== - dependencies: - "@babel/helper-plugin-utils" "^7.25.7" - -"@babel/plugin-transform-logical-assignment-operators@^7.22.5": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.25.7.tgz#93847feb513a1f191c5f5d903d991a0ee24fe99b" - integrity sha512-iImzbA55BjiovLyG2bggWS+V+OLkaBorNvc/yJoeeDQGztknRnDdYfp2d/UPmunZYEnZi6Lg8QcTmNMHOB0lGA== - dependencies: - "@babel/helper-plugin-utils" "^7.25.7" - "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" - -"@babel/plugin-transform-member-expression-literals@^7.22.5": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.25.7.tgz#0a36c3fbd450cc9e6485c507f005fa3d1bc8fca5" - integrity sha512-Std3kXwpXfRV0QtQy5JJcRpkqP8/wG4XL7hSKZmGlxPlDqmpXtEPRmhF7ztnlTCtUN3eXRUJp+sBEZjaIBVYaw== - dependencies: - "@babel/helper-plugin-utils" "^7.25.7" - -"@babel/plugin-transform-modules-amd@^7.22.5": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.25.7.tgz#bb4e543b5611f6c8c685a2fd485408713a3adf3d" - integrity sha512-CgselSGCGzjQvKzghCvDTxKHP3iooenLpJDO842ehn5D2G5fJB222ptnDwQho0WjEvg7zyoxb9P+wiYxiJX5yA== - dependencies: - "@babel/helper-module-transforms" "^7.25.7" - "@babel/helper-plugin-utils" "^7.25.7" - -"@babel/plugin-transform-modules-commonjs@^7.22.5": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.25.7.tgz#173f0c791bb7407c092ce6d77ee90eb3f2d1d2fd" - integrity sha512-L9Gcahi0kKFYXvweO6n0wc3ZG1ChpSFdgG+eV1WYZ3/dGbJK7vvk91FgGgak8YwRgrCuihF8tE/Xg07EkL5COg== - dependencies: - "@babel/helper-module-transforms" "^7.25.7" - "@babel/helper-plugin-utils" "^7.25.7" - "@babel/helper-simple-access" "^7.25.7" - -"@babel/plugin-transform-modules-systemjs@^7.22.5": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.7.tgz#8b14d319a177cc9c85ef8b0512afd429d9e2e60b" - integrity sha512-t9jZIvBmOXJsiuyOwhrIGs8dVcD6jDyg2icw1VL4A/g+FnWyJKwUfSSU2nwJuMV2Zqui856El9u+ElB+j9fV1g== - dependencies: - "@babel/helper-module-transforms" "^7.25.7" - "@babel/helper-plugin-utils" "^7.25.7" - "@babel/helper-validator-identifier" "^7.25.7" - "@babel/traverse" "^7.25.7" - -"@babel/plugin-transform-modules-umd@^7.22.5": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.25.7.tgz#00ee7a7e124289549381bfb0e24d87fd7f848367" - integrity sha512-p88Jg6QqsaPh+EB7I9GJrIqi1Zt4ZBHUQtjw3z1bzEXcLh6GfPqzZJ6G+G1HBGKUNukT58MnKG7EN7zXQBCODw== - dependencies: - "@babel/helper-module-transforms" "^7.25.7" - "@babel/helper-plugin-utils" "^7.25.7" - -"@babel/plugin-transform-named-capturing-groups-regex@^7.22.5": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.25.7.tgz#a2f3f6d7f38693b462542951748f0a72a34d196d" - integrity sha512-BtAT9LzCISKG3Dsdw5uso4oV1+v2NlVXIIomKJgQybotJY3OwCwJmkongjHgwGKoZXd0qG5UZ12JUlDQ07W6Ow== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.25.7" - "@babel/helper-plugin-utils" "^7.25.7" - -"@babel/plugin-transform-new-target@^7.22.5": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.25.7.tgz#52b2bde523b76c548749f38dc3054f1f45e82bc9" - integrity sha512-CfCS2jDsbcZaVYxRFo2qtavW8SpdzmBXC2LOI4oO0rP+JSRDxxF3inF4GcPsLgfb5FjkhXG5/yR/lxuRs2pySA== - dependencies: - "@babel/helper-plugin-utils" "^7.25.7" - -"@babel/plugin-transform-nullish-coalescing-operator@^7.22.5": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.25.7.tgz#0af84b86d4332654c43cf028dbdcf878b00ac168" - integrity sha512-FbuJ63/4LEL32mIxrxwYaqjJxpbzxPVQj5a+Ebrc8JICV6YX8nE53jY+K0RZT3um56GoNWgkS2BQ/uLGTjtwfw== - dependencies: - "@babel/helper-plugin-utils" "^7.25.7" - "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" - -"@babel/plugin-transform-numeric-separator@^7.22.5": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.25.7.tgz#a516b78f894d1c08283f39d809b2048fd2f29448" - integrity sha512-8CbutzSSh4hmD+jJHIA8vdTNk15kAzOnFLVVgBSMGr28rt85ouT01/rezMecks9pkU939wDInImwCKv4ahU4IA== - dependencies: - "@babel/helper-plugin-utils" "^7.25.7" - "@babel/plugin-syntax-numeric-separator" "^7.10.4" - -"@babel/plugin-transform-object-rest-spread@^7.22.5": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.25.7.tgz#fa0916521be96fd434e2db59780b24b308c6d169" - integrity sha512-1JdVKPhD7Y5PvgfFy0Mv2brdrolzpzSoUq2pr6xsR+m+3viGGeHEokFKsCgOkbeFOQxfB1Vt2F0cPJLRpFI4Zg== - dependencies: - "@babel/helper-compilation-targets" "^7.25.7" - "@babel/helper-plugin-utils" "^7.25.7" - "@babel/plugin-syntax-object-rest-spread" "^7.8.3" - "@babel/plugin-transform-parameters" "^7.25.7" - -"@babel/plugin-transform-object-super@^7.22.5": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.25.7.tgz#582a9cea8cf0a1e02732be5b5a703a38dedf5661" - integrity sha512-pWT6UXCEW3u1t2tcAGtE15ornCBvopHj9Bps9D2DsH15APgNVOTwwczGckX+WkAvBmuoYKRCFa4DK+jM8vh5AA== - dependencies: - "@babel/helper-plugin-utils" "^7.25.7" - "@babel/helper-replace-supers" "^7.25.7" - -"@babel/plugin-transform-optional-catch-binding@^7.22.5": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.25.7.tgz#400e2d891f9288f5231694234696aa67164e4913" - integrity sha512-m9obYBA39mDPN7lJzD5WkGGb0GO54PPLXsbcnj1Hyeu8mSRz7Gb4b1A6zxNX32ZuUySDK4G6it8SDFWD1nCnqg== - dependencies: - "@babel/helper-plugin-utils" "^7.25.7" - "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" - -"@babel/plugin-transform-optional-chaining@^7.22.6", "@babel/plugin-transform-optional-chaining@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.25.7.tgz#b7f7c9321aa1d8414e67799c28d87c23682e4d68" - integrity sha512-h39agClImgPWg4H8mYVAbD1qP9vClFbEjqoJmt87Zen8pjqK8FTPUwrOXAvqu5soytwxrLMd2fx2KSCp2CHcNg== - dependencies: - "@babel/helper-plugin-utils" "^7.25.7" - "@babel/helper-skip-transparent-expression-wrappers" "^7.25.7" - "@babel/plugin-syntax-optional-chaining" "^7.8.3" - -"@babel/plugin-transform-parameters@^7.22.5", "@babel/plugin-transform-parameters@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.25.7.tgz#80c38b03ef580f6d6bffe1c5254bb35986859ac7" - integrity sha512-FYiTvku63me9+1Nz7TOx4YMtW3tWXzfANZtrzHhUZrz4d47EEtMQhzFoZWESfXuAMMT5mwzD4+y1N8ONAX6lMQ== - dependencies: - "@babel/helper-plugin-utils" "^7.25.7" - -"@babel/plugin-transform-private-methods@^7.22.5": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.7.tgz#c790a04f837b4bd61d6b0317b43aa11ff67dce80" - integrity sha512-KY0hh2FluNxMLwOCHbxVOKfdB5sjWG4M183885FmaqWWiGMhRZq4DQRKH6mHdEucbJnyDyYiZNwNG424RymJjA== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.25.7" - "@babel/helper-plugin-utils" "^7.25.7" - -"@babel/plugin-transform-private-property-in-object@^7.22.5": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.25.7.tgz#aff877efd05b57c4ad04611d8de97bf155a53369" - integrity sha512-LzA5ESzBy7tqj00Yjey9yWfs3FKy4EmJyKOSWld144OxkTji81WWnUT8nkLUn+imN/zHL8ZQlOu/MTUAhHaX3g== - dependencies: - "@babel/helper-annotate-as-pure" "^7.25.7" - "@babel/helper-create-class-features-plugin" "^7.25.7" - "@babel/helper-plugin-utils" "^7.25.7" - "@babel/plugin-syntax-private-property-in-object" "^7.14.5" - -"@babel/plugin-transform-property-literals@^7.22.5": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.25.7.tgz#a8612b4ea4e10430f00012ecf0155662c7d6550d" - integrity sha512-lQEeetGKfFi0wHbt8ClQrUSUMfEeI3MMm74Z73T9/kuz990yYVtfofjf3NuA42Jy3auFOpbjDyCSiIkTs1VIYw== - dependencies: - "@babel/helper-plugin-utils" "^7.25.7" - -"@babel/plugin-transform-regenerator@^7.22.5": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.25.7.tgz#6eb006e6d26f627bc2f7844a9f19770721ad6f3e" - integrity sha512-mgDoQCRjrY3XK95UuV60tZlFCQGXEtMg8H+IsW72ldw1ih1jZhzYXbJvghmAEpg5UVhhnCeia1CkGttUvCkiMQ== - dependencies: - "@babel/helper-plugin-utils" "^7.25.7" - regenerator-transform "^0.15.2" - -"@babel/plugin-transform-reserved-words@^7.22.5": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.25.7.tgz#dc56b25e02afaabef3ce0c5b06b0916e8523e995" - integrity sha512-3OfyfRRqiGeOvIWSagcwUTVk2hXBsr/ww7bLn6TRTuXnexA+Udov2icFOxFX9abaj4l96ooYkcNN1qi2Zvqwng== - dependencies: - "@babel/helper-plugin-utils" "^7.25.7" - -"@babel/plugin-transform-runtime@7.22.9": - version "7.22.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.22.9.tgz#a87b11e170cbbfb018e6a2bf91f5c6e533b9e027" - integrity sha512-9KjBH61AGJetCPYp/IEyLEp47SyybZb0nDRpBvmtEkm+rUIwxdlKpyNHI1TmsGkeuLclJdleQHRZ8XLBnnh8CQ== - dependencies: - "@babel/helper-module-imports" "^7.22.5" - "@babel/helper-plugin-utils" "^7.22.5" - babel-plugin-polyfill-corejs2 "^0.4.4" - babel-plugin-polyfill-corejs3 "^0.8.2" - babel-plugin-polyfill-regenerator "^0.5.1" - semver "^6.3.1" - -"@babel/plugin-transform-shorthand-properties@^7.22.5": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.25.7.tgz#92690a9c671915602d91533c278cc8f6bf12275f" - integrity sha512-uBbxNwimHi5Bv3hUccmOFlUy3ATO6WagTApenHz9KzoIdn0XeACdB12ZJ4cjhuB2WSi80Ez2FWzJnarccriJeA== - dependencies: - "@babel/helper-plugin-utils" "^7.25.7" - -"@babel/plugin-transform-spread@^7.22.5": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.25.7.tgz#df83e899a9fc66284ee601a7b738568435b92998" - integrity sha512-Mm6aeymI0PBh44xNIv/qvo8nmbkpZze1KvR8MkEqbIREDxoiWTi18Zr2jryfRMwDfVZF9foKh060fWgni44luw== - dependencies: - "@babel/helper-plugin-utils" "^7.25.7" - "@babel/helper-skip-transparent-expression-wrappers" "^7.25.7" - -"@babel/plugin-transform-sticky-regex@^7.22.5": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.25.7.tgz#341c7002bef7f29037be7fb9684e374442dd0d17" - integrity sha512-ZFAeNkpGuLnAQ/NCsXJ6xik7Id+tHuS+NT+ue/2+rn/31zcdnupCdmunOizEaP0JsUmTFSTOPoQY7PkK2pttXw== - dependencies: - "@babel/helper-plugin-utils" "^7.25.7" - -"@babel/plugin-transform-template-literals@^7.22.5": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.25.7.tgz#e566c581bb16d8541dd8701093bb3457adfce16b" - integrity sha512-SI274k0nUsFFmyQupiO7+wKATAmMFf8iFgq2O+vVFXZ0SV9lNfT1NGzBEhjquFmD8I9sqHLguH+gZVN3vww2AA== - dependencies: - "@babel/helper-plugin-utils" "^7.25.7" - -"@babel/plugin-transform-typeof-symbol@^7.22.5": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.25.7.tgz#debb1287182efd20488f126be343328c679b66eb" - integrity sha512-OmWmQtTHnO8RSUbL0NTdtpbZHeNTnm68Gj5pA4Y2blFNh+V4iZR68V1qL9cI37J21ZN7AaCnkfdHtLExQPf2uA== - dependencies: - "@babel/helper-plugin-utils" "^7.25.7" - -"@babel/plugin-transform-unicode-escapes@^7.22.5": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.25.7.tgz#973592b6d13a914794e1de8cf1383e50e0f87f81" - integrity sha512-BN87D7KpbdiABA+t3HbVqHzKWUDN3dymLaTnPFAMyc8lV+KN3+YzNhVRNdinaCPA4AUqx7ubXbQ9shRjYBl3SQ== - dependencies: - "@babel/helper-plugin-utils" "^7.25.7" - -"@babel/plugin-transform-unicode-property-regex@^7.22.5": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.25.7.tgz#25349197cce964b1343f74fa7cfdf791a1b1919e" - integrity sha512-IWfR89zcEPQGB/iB408uGtSPlQd3Jpq11Im86vUgcmSTcoWAiQMCTOa2K2yNNqFJEBVICKhayctee65Ka8OB0w== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.25.7" - "@babel/helper-plugin-utils" "^7.25.7" - -"@babel/plugin-transform-unicode-regex@^7.22.5": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.25.7.tgz#f93a93441baf61f713b6d5552aaa856bfab34809" - integrity sha512-8JKfg/hiuA3qXnlLx8qtv5HWRbgyFx2hMMtpDDuU2rTckpKkGu4ycK5yYHwuEa16/quXfoxHBIApEsNyMWnt0g== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.25.7" - "@babel/helper-plugin-utils" "^7.25.7" - -"@babel/plugin-transform-unicode-sets-regex@^7.22.5": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.7.tgz#d1b3295d29e0f8f4df76abc909ad1ebee919560c" - integrity sha512-YRW8o9vzImwmh4Q3Rffd09bH5/hvY0pxg+1H1i0f7APoUeg12G7+HhLj9ZFNIrYkgBXhIijPJ+IXypN0hLTIbw== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.25.7" - "@babel/helper-plugin-utils" "^7.25.7" - -"@babel/preset-env@7.22.9": - version "7.22.9" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.22.9.tgz#57f17108eb5dfd4c5c25a44c1977eba1df310ac7" - integrity sha512-wNi5H/Emkhll/bqPjsjQorSykrlfY5OWakd6AulLvMEytpKasMVUpVy8RL4qBIBs5Ac6/5i0/Rv0b/Fg6Eag/g== - dependencies: - "@babel/compat-data" "^7.22.9" - "@babel/helper-compilation-targets" "^7.22.9" - "@babel/helper-plugin-utils" "^7.22.5" - "@babel/helper-validator-option" "^7.22.5" - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.22.5" - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.22.5" - "@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.22.5" - "@babel/plugin-syntax-import-attributes" "^7.22.5" - "@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.22.5" - "@babel/plugin-transform-async-generator-functions" "^7.22.7" - "@babel/plugin-transform-async-to-generator" "^7.22.5" - "@babel/plugin-transform-block-scoped-functions" "^7.22.5" - "@babel/plugin-transform-block-scoping" "^7.22.5" - "@babel/plugin-transform-class-properties" "^7.22.5" - "@babel/plugin-transform-class-static-block" "^7.22.5" - "@babel/plugin-transform-classes" "^7.22.6" - "@babel/plugin-transform-computed-properties" "^7.22.5" - "@babel/plugin-transform-destructuring" "^7.22.5" - "@babel/plugin-transform-dotall-regex" "^7.22.5" - "@babel/plugin-transform-duplicate-keys" "^7.22.5" - "@babel/plugin-transform-dynamic-import" "^7.22.5" - "@babel/plugin-transform-exponentiation-operator" "^7.22.5" - "@babel/plugin-transform-export-namespace-from" "^7.22.5" - "@babel/plugin-transform-for-of" "^7.22.5" - "@babel/plugin-transform-function-name" "^7.22.5" - "@babel/plugin-transform-json-strings" "^7.22.5" - "@babel/plugin-transform-literals" "^7.22.5" - "@babel/plugin-transform-logical-assignment-operators" "^7.22.5" - "@babel/plugin-transform-member-expression-literals" "^7.22.5" - "@babel/plugin-transform-modules-amd" "^7.22.5" - "@babel/plugin-transform-modules-commonjs" "^7.22.5" - "@babel/plugin-transform-modules-systemjs" "^7.22.5" - "@babel/plugin-transform-modules-umd" "^7.22.5" - "@babel/plugin-transform-named-capturing-groups-regex" "^7.22.5" - "@babel/plugin-transform-new-target" "^7.22.5" - "@babel/plugin-transform-nullish-coalescing-operator" "^7.22.5" - "@babel/plugin-transform-numeric-separator" "^7.22.5" - "@babel/plugin-transform-object-rest-spread" "^7.22.5" - "@babel/plugin-transform-object-super" "^7.22.5" - "@babel/plugin-transform-optional-catch-binding" "^7.22.5" - "@babel/plugin-transform-optional-chaining" "^7.22.6" - "@babel/plugin-transform-parameters" "^7.22.5" - "@babel/plugin-transform-private-methods" "^7.22.5" - "@babel/plugin-transform-private-property-in-object" "^7.22.5" - "@babel/plugin-transform-property-literals" "^7.22.5" - "@babel/plugin-transform-regenerator" "^7.22.5" - "@babel/plugin-transform-reserved-words" "^7.22.5" - "@babel/plugin-transform-shorthand-properties" "^7.22.5" - "@babel/plugin-transform-spread" "^7.22.5" - "@babel/plugin-transform-sticky-regex" "^7.22.5" - "@babel/plugin-transform-template-literals" "^7.22.5" - "@babel/plugin-transform-typeof-symbol" "^7.22.5" - "@babel/plugin-transform-unicode-escapes" "^7.22.5" - "@babel/plugin-transform-unicode-property-regex" "^7.22.5" - "@babel/plugin-transform-unicode-regex" "^7.22.5" - "@babel/plugin-transform-unicode-sets-regex" "^7.22.5" - "@babel/preset-modules" "^0.1.5" - "@babel/types" "^7.22.5" - babel-plugin-polyfill-corejs2 "^0.4.4" - babel-plugin-polyfill-corejs3 "^0.8.2" - babel-plugin-polyfill-regenerator "^0.5.1" - core-js-compat "^3.31.0" - semver "^6.3.1" - -"@babel/preset-modules@^0.1.5": - version "0.1.6" - resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.6.tgz#31bcdd8f19538437339d17af00d177d854d9d458" - integrity sha512-ID2yj6K/4lKfhuU3+EX4UvNbIt7eACFbHmNUjzA+ep+B5971CknnA/9DEWKbRokfbbtblxxxXFJJrH47UEAMVg== - dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - "@babel/plugin-proposal-unicode-property-regex" "^7.4.4" - "@babel/plugin-transform-dotall-regex" "^7.4.4" - "@babel/types" "^7.4.4" - esutils "^2.0.2" - -"@babel/runtime@7.22.6": - version "7.22.6" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.6.tgz#57d64b9ae3cff1d67eb067ae117dac087f5bd438" - integrity sha512-wDb5pWm4WDdF6LFUde3Jl8WzPA+3ZbxYqkC6xAXuD3irdEHN1k0NfTRrJD8ZD378SJ61miMLCqIOXYhd8x+AJQ== - dependencies: - regenerator-runtime "^0.13.11" - -"@babel/runtime@^7.8.4": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.25.7.tgz#7ffb53c37a8f247c8c4d335e89cdf16a2e0d0fb6" - integrity sha512-FjoyLe754PMiYsFaN5C94ttGiOmBNYTf6pLr4xXHAT5uctHb092PBszndLDR5XA/jghQvn4n7JMHl7dmTgbm9w== - dependencies: - regenerator-runtime "^0.14.0" - -"@babel/template@7.22.5": - version "7.22.5" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.5.tgz#0c8c4d944509875849bd0344ff0050756eefc6ec" - integrity sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw== - dependencies: - "@babel/code-frame" "^7.22.5" - "@babel/parser" "^7.22.5" - "@babel/types" "^7.22.5" - -"@babel/template@^7.22.15", "@babel/template@^7.22.5", "@babel/template@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.25.7.tgz#27f69ce382855d915b14ab0fe5fb4cbf88fa0769" - integrity sha512-wRwtAgI3bAS+JGU2upWNL9lSlDcRCqD05BZ1n3X2ONLH1WilFP6O1otQjeMK/1g0pvYcXC7b/qVUB1keofjtZA== - dependencies: - "@babel/code-frame" "^7.25.7" - "@babel/parser" "^7.25.7" - "@babel/types" "^7.25.7" - -"@babel/traverse@^7.22.8", "@babel/traverse@^7.23.2", "@babel/traverse@^7.25.7": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.25.7.tgz#83e367619be1cab8e4f2892ef30ba04c26a40fa8" - integrity sha512-jatJPT1Zjqvh/1FyJs6qAHL+Dzb7sTb+xr7Q+gM1b+1oBsMsQQ4FkVKb6dFlJvLlVssqkRzV05Jzervt9yhnzg== - dependencies: - "@babel/code-frame" "^7.25.7" - "@babel/generator" "^7.25.7" - "@babel/parser" "^7.25.7" - "@babel/template" "^7.25.7" - "@babel/types" "^7.25.7" - debug "^4.3.1" - globals "^11.1.0" - -"@babel/types@^7.22.5", "@babel/types@^7.23.0", "@babel/types@^7.24.7", "@babel/types@^7.25.7", "@babel/types@^7.4.4": - version "7.25.7" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.25.7.tgz#1b7725c1d3a59f328cb700ce704c46371e6eef9b" - integrity sha512-vwIVdXG+j+FOpkwqHRcBgHLYNL7XMkufrlaFvL9o6Ai9sJn9+PdyIL5qa0XzTZw084c+u9LOls53eoZWP/W5WQ== - dependencies: - "@babel/helper-string-parser" "^7.25.7" - "@babel/helper-validator-identifier" "^7.25.7" - to-fast-properties "^2.0.0" - -"@bufbuild/buf-darwin-arm64@1.50.1": - version "1.50.1" - resolved "https://registry.yarnpkg.com/@bufbuild/buf-darwin-arm64/-/buf-darwin-arm64-1.50.1.tgz#218bf184196f7458900f593ce76de696ea1a074e" - integrity sha512-uoRLgqucLP5qLIIDovsHN+7QoeBC0fqfZ1LhwZfNUd95py3ymEOmOFmP0JCbW349a8zmH2HhS5yOySe1yXwP2w== - -"@bufbuild/buf-darwin-x64@1.50.1": - version "1.50.1" - resolved "https://registry.yarnpkg.com/@bufbuild/buf-darwin-x64/-/buf-darwin-x64-1.50.1.tgz#5d3fd1b859227979a9c7bd148bf16ce8a6f0c956" - integrity sha512-b5efryjfQ/rTnftcjcbluAJK1bSHCacSK0O/ldMbNQRUwstUieqX/NHIxdQcrorHqW26VppWmQuB88JYxffABw== - -"@bufbuild/buf-linux-aarch64@1.50.1": - version "1.50.1" - resolved "https://registry.yarnpkg.com/@bufbuild/buf-linux-aarch64/-/buf-linux-aarch64-1.50.1.tgz#b96faa0f15b6eb956a39b60252c488f5afec0115" - integrity sha512-ZE3nfVyENkHNV+lZw9ScjmQnPNlzIZrGLYeqFTQllnbUWehvZACTBY8L4ak5eXf6NRyGIksAEuCPnnrht5WigA== - -"@bufbuild/buf-linux-armv7@1.50.1": - version "1.50.1" - resolved "https://registry.yarnpkg.com/@bufbuild/buf-linux-armv7/-/buf-linux-armv7-1.50.1.tgz#9b9c2ca41034c6f2f59d854585ea6a27ec578c3c" - integrity sha512-Bc/V1ySgFIaTa8sdCEwX3Y4vkyULlDeuUKyqUSSQjiVweeuoxOp8jLwQMFoQCdYFwY02yY0MlNR/mhyowGCt9w== - -"@bufbuild/buf-linux-x64@1.50.1": - version "1.50.1" - resolved "https://registry.yarnpkg.com/@bufbuild/buf-linux-x64/-/buf-linux-x64-1.50.1.tgz#ac5daffde0b9f20df2289a488f9b0f5000689557" - integrity sha512-2aHe+LmsTIaOH3ViH8EcOVx4yStZznJYmZ/NQ/cV5kvD3JAVJv4u5MtTJAd6DDj9BmEhXtjXK9hWtRi7S0vF2w== - -"@bufbuild/buf-win32-arm64@1.50.1": - version "1.50.1" - resolved "https://registry.yarnpkg.com/@bufbuild/buf-win32-arm64/-/buf-win32-arm64-1.50.1.tgz#3d276ba4b9102dcb73291fa4e471de305b5d6b04" - integrity sha512-fqNQ9Ke/arleUulnePEyG2+iW2eB6xO6rcVovFY4EorZUTZ/EzrtdmVn87AfWJle3HxcXcIXP0onrcKBrwXUuQ== - -"@bufbuild/buf-win32-x64@1.50.1": - version "1.50.1" - resolved "https://registry.yarnpkg.com/@bufbuild/buf-win32-x64/-/buf-win32-x64-1.50.1.tgz#ccf0b131881b7cf56b0c11954ab397ccafa6f962" - integrity sha512-+jd1Dopk24WQtcSgden+dOtGu/OMAuBtnsfkv59pqbCbdZqNCsqrC7VluJ4/5y5PiNcB7hcA26RETkPtkInluA== - -"@bufbuild/buf@^1.41.0": - version "1.50.1" - resolved "https://registry.yarnpkg.com/@bufbuild/buf/-/buf-1.50.1.tgz#679c7c0fccea02b784eb7f6cfd213cae58defec0" - integrity sha512-KVXBUfKe13STjoSZ9VPzomJUwYnJ01c1b54nJf4a90H/tg0Q/K0z0Wxz0Dr43vuuoDvVHAuBOZPs/XIwSH33IQ== - optionalDependencies: - "@bufbuild/buf-darwin-arm64" "1.50.1" - "@bufbuild/buf-darwin-x64" "1.50.1" - "@bufbuild/buf-linux-aarch64" "1.50.1" - "@bufbuild/buf-linux-armv7" "1.50.1" - "@bufbuild/buf-linux-x64" "1.50.1" - "@bufbuild/buf-win32-arm64" "1.50.1" - "@bufbuild/buf-win32-x64" "1.50.1" - -"@bufbuild/protobuf@^2.2.2": - version "2.2.3" - resolved "https://registry.yarnpkg.com/@bufbuild/protobuf/-/protobuf-2.2.3.tgz#9cd136f6b687e63e9b517b3a54211ece942897ee" - integrity sha512-tFQoXHJdkEOSwj5tRIZSPNUuXK3RaR7T1nUrPgbYX1pUbvqqaaZAsfo+NXBPsz5rZMSKVFrgK1WL8Q/MSLvprg== - -"@colors/colors@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" - integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== - -"@connectrpc/connect-node@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@connectrpc/connect-node/-/connect-node-2.0.0.tgz#1ea4eff7f2633fbe3d80378e1420bd213d9d83a4" - integrity sha512-DoI5T+SUvlS/8QBsxt2iDoUg15dSxqhckegrgZpWOtADtmGohBIVbx1UjtWmjLBrP4RdD0FeBw+XyRUSbpKnJQ== - -"@connectrpc/connect-web@^2.0.0": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@connectrpc/connect-web/-/connect-web-2.0.2.tgz#02f109e21eb06b31ee2558eeed39b1bf03cc9089" - integrity sha512-QANMFPiL2o66BdBEctg4TsQLe5ozsBLqcle3dCBp7BwGlNGTY6NnNnqmt+YRnpeMW88GgomJwWNMGCrRD9pRKA== - -"@connectrpc/connect@^2.0.0": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@connectrpc/connect/-/connect-2.0.2.tgz#acd05e1408737c860e732137dc6f960321d2c628" - integrity sha512-xZuylIUNvNlH52e/4eQsZvY4QZyDJRtEFEDnn/yBrv5Xi5ZZI/p8X+GAHH35ucVaBvv9u7OzHZo8+tEh1EFTxA== - -"@ctrl/ngx-codemirror@^6.1.0": - version "6.1.0" - resolved "https://registry.yarnpkg.com/@ctrl/ngx-codemirror/-/ngx-codemirror-6.1.0.tgz#9324a56e4b709be9c515364d21e05e1d7589f009" - integrity sha512-73QeoNbnluZalWmNw+SOFctsE+oz0+4Bl9KhlJOIfbCPK/U6OIc+vQNr28hMAp15Y/Idde3LntTwLBDFW0bRVA== - dependencies: - "@types/codemirror" "^5.60.5" - tslib "^2.3.0" - -"@ctrl/tinycolor@^3.6.0": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz#b6c75a56a1947cc916ea058772d666a2c8932f31" - integrity sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA== - -"@discoveryjs/json-ext@0.5.7": - version "0.5.7" - resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" - integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== - -"@esbuild/android-arm64@0.18.17": - version "0.18.17" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.18.17.tgz#9e00eb6865ed5f2dbe71a1e96f2c52254cd92903" - integrity sha512-9np+YYdNDed5+Jgr1TdWBsozZ85U1Oa3xW0c7TWqH0y2aGghXtZsuT8nYRbzOMcl0bXZXjOGbksoTtVOlWrRZg== - -"@esbuild/android-arm64@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz#984b4f9c8d0377443cc2dfcef266d02244593622" - integrity sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ== - -"@esbuild/android-arm@0.18.17": - version "0.18.17" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.18.17.tgz#1aa013b65524f4e9f794946b415b32ae963a4618" - integrity sha512-wHsmJG/dnL3OkpAcwbgoBTTMHVi4Uyou3F5mf58ZtmUyIKfcdA7TROav/6tCzET4A3QW2Q2FC+eFneMU+iyOxg== - -"@esbuild/android-arm@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.18.20.tgz#fedb265bc3a589c84cc11f810804f234947c3682" - integrity sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw== - -"@esbuild/android-x64@0.18.17": - version "0.18.17" - resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.18.17.tgz#c2bd0469b04ded352de011fae34a7a1d4dcecb79" - integrity sha512-O+FeWB/+xya0aLg23hHEM2E3hbfwZzjqumKMSIqcHbNvDa+dza2D0yLuymRBQQnC34CWrsJUXyH2MG5VnLd6uw== - -"@esbuild/android-x64@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.18.20.tgz#35cf419c4cfc8babe8893d296cd990e9e9f756f2" - integrity sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg== - -"@esbuild/darwin-arm64@0.18.17": - version "0.18.17" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.18.17.tgz#0c21a59cb5bd7a2cec66c7a42431dca42aefeddd" - integrity sha512-M9uJ9VSB1oli2BE/dJs3zVr9kcCBBsE883prage1NWz6pBS++1oNn/7soPNS3+1DGj0FrkSvnED4Bmlu1VAE9g== - -"@esbuild/darwin-arm64@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz#08172cbeccf95fbc383399a7f39cfbddaeb0d7c1" - integrity sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA== - -"@esbuild/darwin-x64@0.18.17": - version "0.18.17" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.18.17.tgz#92f8763ff6f97dff1c28a584da7b51b585e87a7b" - integrity sha512-XDre+J5YeIJDMfp3n0279DFNrGCXlxOuGsWIkRb1NThMZ0BsrWXoTg23Jer7fEXQ9Ye5QjrvXpxnhzl3bHtk0g== - -"@esbuild/darwin-x64@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz#d70d5790d8bf475556b67d0f8b7c5bdff053d85d" - integrity sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ== - -"@esbuild/freebsd-arm64@0.18.17": - version "0.18.17" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.17.tgz#934f74bdf4022e143ba2f21d421b50fd0fead8f8" - integrity sha512-cjTzGa3QlNfERa0+ptykyxs5A6FEUQQF0MuilYXYBGdBxD3vxJcKnzDlhDCa1VAJCmAxed6mYhA2KaJIbtiNuQ== - -"@esbuild/freebsd-arm64@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz#98755cd12707f93f210e2494d6a4b51b96977f54" - integrity sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw== - -"@esbuild/freebsd-x64@0.18.17": - version "0.18.17" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.18.17.tgz#16b6e90ba26ecc865eab71c56696258ec7f5d8bf" - integrity sha512-sOxEvR8d7V7Kw8QqzxWc7bFfnWnGdaFBut1dRUYtu+EIRXefBc/eIsiUiShnW0hM3FmQ5Zf27suDuHsKgZ5QrA== - -"@esbuild/freebsd-x64@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz#c1eb2bff03915f87c29cece4c1a7fa1f423b066e" - integrity sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ== - -"@esbuild/linux-arm64@0.18.17": - version "0.18.17" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.18.17.tgz#179a58e8d4c72116eb068563629349f8f4b48072" - integrity sha512-c9w3tE7qA3CYWjT+M3BMbwMt+0JYOp3vCMKgVBrCl1nwjAlOMYzEo+gG7QaZ9AtqZFj5MbUc885wuBBmu6aADQ== - -"@esbuild/linux-arm64@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz#bad4238bd8f4fc25b5a021280c770ab5fc3a02a0" - integrity sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA== - -"@esbuild/linux-arm@0.18.17": - version "0.18.17" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.18.17.tgz#9d78cf87a310ae9ed985c3915d5126578665c7b5" - integrity sha512-2d3Lw6wkwgSLC2fIvXKoMNGVaeY8qdN0IC3rfuVxJp89CRfA3e3VqWifGDfuakPmp90+ZirmTfye1n4ncjv2lg== - -"@esbuild/linux-arm@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz#3e617c61f33508a27150ee417543c8ab5acc73b0" - integrity sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg== - -"@esbuild/linux-ia32@0.18.17": - version "0.18.17" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.18.17.tgz#6fed202602d37361bca376c9d113266a722a908c" - integrity sha512-1DS9F966pn5pPnqXYz16dQqWIB0dmDfAQZd6jSSpiT9eX1NzKh07J6VKR3AoXXXEk6CqZMojiVDSZi1SlmKVdg== - -"@esbuild/linux-ia32@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz#699391cccba9aee6019b7f9892eb99219f1570a7" - integrity sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA== - -"@esbuild/linux-loong64@0.18.17": - version "0.18.17" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.18.17.tgz#cdc60304830be1e74560c704bfd72cab8a02fa06" - integrity sha512-EvLsxCk6ZF0fpCB6w6eOI2Fc8KW5N6sHlIovNe8uOFObL2O+Mr0bflPHyHwLT6rwMg9r77WOAWb2FqCQrVnwFg== - -"@esbuild/linux-loong64@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz#e6fccb7aac178dd2ffb9860465ac89d7f23b977d" - integrity sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg== - -"@esbuild/linux-mips64el@0.18.17": - version "0.18.17" - resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.18.17.tgz#c367b2855bb0902f5576291a2049812af2088086" - integrity sha512-e0bIdHA5p6l+lwqTE36NAW5hHtw2tNRmHlGBygZC14QObsA3bD4C6sXLJjvnDIjSKhW1/0S3eDy+QmX/uZWEYQ== - -"@esbuild/linux-mips64el@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz#eeff3a937de9c2310de30622a957ad1bd9183231" - integrity sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ== - -"@esbuild/linux-ppc64@0.18.17": - version "0.18.17" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.18.17.tgz#7fdc0083d42d64a4651711ee0a7964f489242f45" - integrity sha512-BAAilJ0M5O2uMxHYGjFKn4nJKF6fNCdP1E0o5t5fvMYYzeIqy2JdAP88Az5LHt9qBoUa4tDaRpfWt21ep5/WqQ== - -"@esbuild/linux-ppc64@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz#2f7156bde20b01527993e6881435ad79ba9599fb" - integrity sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA== - -"@esbuild/linux-riscv64@0.18.17": - version "0.18.17" - resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.18.17.tgz#5198a417f3f5b86b10c95647b8bc032e5b6b2b1c" - integrity sha512-Wh/HW2MPnC3b8BqRSIme/9Zhab36PPH+3zam5pqGRH4pE+4xTrVLx2+XdGp6fVS3L2x+DrsIcsbMleex8fbE6g== - -"@esbuild/linux-riscv64@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz#6628389f210123d8b4743045af8caa7d4ddfc7a6" - integrity sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A== - -"@esbuild/linux-s390x@0.18.17": - version "0.18.17" - resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.18.17.tgz#7459c2fecdee2d582f0697fb76a4041f4ad1dd1e" - integrity sha512-j/34jAl3ul3PNcK3pfI0NSlBANduT2UO5kZ7FCaK33XFv3chDhICLY8wJJWIhiQ+YNdQ9dxqQctRg2bvrMlYgg== - -"@esbuild/linux-s390x@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz#255e81fb289b101026131858ab99fba63dcf0071" - integrity sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ== - -"@esbuild/linux-x64@0.18.17": - version "0.18.17" - resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.18.17.tgz#948cdbf46d81c81ebd7225a7633009bc56a4488c" - integrity sha512-QM50vJ/y+8I60qEmFxMoxIx4de03pGo2HwxdBeFd4nMh364X6TIBZ6VQ5UQmPbQWUVWHWws5MmJXlHAXvJEmpQ== - -"@esbuild/linux-x64@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz#c7690b3417af318a9b6f96df3031a8865176d338" - integrity sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w== - -"@esbuild/netbsd-x64@0.18.17": - version "0.18.17" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.18.17.tgz#6bb89668c0e093c5a575ded08e1d308bd7fd63e7" - integrity sha512-/jGlhWR7Sj9JPZHzXyyMZ1RFMkNPjC6QIAan0sDOtIo2TYk3tZn5UDrkE0XgsTQCxWTTOcMPf9p6Rh2hXtl5TQ== - -"@esbuild/netbsd-x64@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz#30e8cd8a3dded63975e2df2438ca109601ebe0d1" - integrity sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A== - -"@esbuild/openbsd-x64@0.18.17": - version "0.18.17" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.18.17.tgz#abac2ae75fef820ef6c2c48da4666d092584c79d" - integrity sha512-rSEeYaGgyGGf4qZM2NonMhMOP/5EHp4u9ehFiBrg7stH6BYEEjlkVREuDEcQ0LfIl53OXLxNbfuIj7mr5m29TA== - -"@esbuild/openbsd-x64@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz#7812af31b205055874c8082ea9cf9ab0da6217ae" - integrity sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg== - -"@esbuild/sunos-x64@0.18.17": - version "0.18.17" - resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.18.17.tgz#74a45fe1db8ea96898f1a9bb401dcf1dadfc8371" - integrity sha512-Y7ZBbkLqlSgn4+zot4KUNYst0bFoO68tRgI6mY2FIM+b7ZbyNVtNbDP5y8qlu4/knZZ73fgJDlXID+ohY5zt5g== - -"@esbuild/sunos-x64@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz#d5c275c3b4e73c9b0ecd38d1ca62c020f887ab9d" - integrity sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ== - -"@esbuild/win32-arm64@0.18.17": - version "0.18.17" - resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.18.17.tgz#fd95ffd217995589058a4ed8ac17ee72a3d7f615" - integrity sha512-bwPmTJsEQcbZk26oYpc4c/8PvTY3J5/QK8jM19DVlEsAB41M39aWovWoHtNm78sd6ip6prilxeHosPADXtEJFw== - -"@esbuild/win32-arm64@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz#73bc7f5a9f8a77805f357fab97f290d0e4820ac9" - integrity sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg== - -"@esbuild/win32-ia32@0.18.17": - version "0.18.17" - resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.18.17.tgz#9b7ef5d0df97593a80f946b482e34fcba3fa4aaf" - integrity sha512-H/XaPtPKli2MhW+3CQueo6Ni3Avggi6hP/YvgkEe1aSaxw+AeO8MFjq8DlgfTd9Iz4Yih3QCZI6YLMoyccnPRg== - -"@esbuild/win32-ia32@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz#ec93cbf0ef1085cc12e71e0d661d20569ff42102" - integrity sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g== - -"@esbuild/win32-x64@0.18.17": - version "0.18.17" - resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.18.17.tgz#bcb2e042631b3c15792058e189ed879a22b2968b" - integrity sha512-fGEb8f2BSA3CW7riJVurug65ACLuQAzKq0SSqkY2b2yHHH0MzDfbLyKIGzHwOI/gkHcxM/leuSW6D5w/LMNitA== - -"@esbuild/win32-x64@0.18.20": - version "0.18.20" - resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz#786c5f41f043b07afb1af37683d7c33668858f6d" - integrity sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ== - -"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": - version "4.4.0" - resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" - integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== - dependencies: - eslint-visitor-keys "^3.3.0" - -"@eslint-community/regexpp@^4.4.0", "@eslint-community/regexpp@^4.6.1": - version "4.11.1" - resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.11.1.tgz#a547badfc719eb3e5f4b556325e542fbe9d7a18f" - integrity sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q== - -"@eslint/eslintrc@^2.1.4": - version "2.1.4" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz#388a269f0f25c1b6adc317b5a2c55714894c70ad" - integrity sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ== - 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" - -"@eslint/js@8.57.1": - version "8.57.1" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.1.tgz#de633db3ec2ef6a3c89e2f19038063e8a122e2c2" - integrity sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q== - -"@fortawesome/angular-fontawesome@^0.13.0": - version "0.13.0" - resolved "https://registry.yarnpkg.com/@fortawesome/angular-fontawesome/-/angular-fontawesome-0.13.0.tgz#3e343ff5ea62934cb0309a52bbf732adecf216fc" - integrity sha512-gzSPRdveOXNO7NIiMgTyB46aiHG0i98KinnAEqHXi8qzraM/kCcHn/0y3f4MhemX6kftwsFli0IU8RyHmtXlSQ== - dependencies: - tslib "^2.4.1" - -"@fortawesome/fontawesome-common-types@6.7.2": - version "6.7.2" - resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.2.tgz#7123d74b0c1e726794aed1184795dbce12186470" - integrity sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg== - -"@fortawesome/fontawesome-svg-core@^6.7.2": - version "6.7.2" - resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.2.tgz#0ac6013724d5cc327c1eb81335b91300a4fce2f2" - integrity sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA== - dependencies: - "@fortawesome/fontawesome-common-types" "6.7.2" - -"@fortawesome/free-brands-svg-icons@^6.7.2": - version "6.7.2" - resolved "https://registry.yarnpkg.com/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.7.2.tgz#4ebee8098f31da5446dda81edc344025eb59b27e" - integrity sha512-zu0evbcRTgjKfrr77/2XX+bU+kuGfjm0LbajJHVIgBWNIDzrhpRxiCPNT8DW5AdmSsq7Mcf9D1bH0aSeSUSM+Q== - dependencies: - "@fortawesome/fontawesome-common-types" "6.7.2" - -"@gar/promisify@^1.1.3": - version "1.1.3" - resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6" - integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw== - -"@humanwhocodes/config-array@^0.13.0": - version "0.13.0" - resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.13.0.tgz#fb907624df3256d04b9aa2df50d7aa97ec648748" - integrity sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw== - dependencies: - "@humanwhocodes/object-schema" "^2.0.3" - debug "^4.3.1" - minimatch "^3.0.5" - -"@humanwhocodes/module-importer@^1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" - integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== - -"@humanwhocodes/object-schema@^2.0.3": - version "2.0.3" - resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3" - integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA== - -"@isaacs/cliui@^8.0.2": - version "8.0.2" - resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" - integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== - dependencies: - string-width "^5.1.2" - string-width-cjs "npm:string-width@^4.2.0" - strip-ansi "^7.0.1" - strip-ansi-cjs "npm:strip-ansi@^6.0.1" - wrap-ansi "^8.1.0" - wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" - -"@istanbuljs/load-nyc-config@^1.0.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" - integrity sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ== - 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" - -"@istanbuljs/schema@^0.1.2": - version "0.1.3" - resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" - integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== - -"@jridgewell/gen-mapping@^0.3.0", "@jridgewell/gen-mapping@^0.3.2", "@jridgewell/gen-mapping@^0.3.5": - version "0.3.5" - resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz#dcce6aff74bdf6dad1a95802b69b04a2fcb1fb36" - integrity sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg== - dependencies: - "@jridgewell/set-array" "^1.2.1" - "@jridgewell/sourcemap-codec" "^1.4.10" - "@jridgewell/trace-mapping" "^0.3.24" - -"@jridgewell/resolve-uri@^3.1.0": - version "3.1.2" - resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" - integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== - -"@jridgewell/set-array@^1.2.1": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280" - integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== - -"@jridgewell/source-map@^0.3.3": - version "0.3.6" - resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.6.tgz#9d71ca886e32502eb9362c9a74a46787c36df81a" - integrity sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ== - dependencies: - "@jridgewell/gen-mapping" "^0.3.5" - "@jridgewell/trace-mapping" "^0.3.25" - -"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.4.15": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" - integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== - -"@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.20", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25", "@jridgewell/trace-mapping@^0.3.9": - version "0.3.25" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" - integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== - dependencies: - "@jridgewell/resolve-uri" "^3.1.0" - "@jridgewell/sourcemap-codec" "^1.4.14" - -"@leichtgewicht/ip-codec@^2.0.1": - version "2.0.5" - resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz#4fc56c15c580b9adb7dc3c333a134e540b44bfb1" - integrity sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw== - -"@material/animation@15.0.0-canary.bc9ae6c9c.0": - version "15.0.0-canary.bc9ae6c9c.0" - resolved "https://registry.yarnpkg.com/@material/animation/-/animation-15.0.0-canary.bc9ae6c9c.0.tgz#7c27a42b027fcc2cd9a97c9d3b8f54a16b47333d" - integrity sha512-leRf+BcZTfC/iSigLXnYgcHAGvFVQveoJT5+2PIRdyPI/bIG7hhciRgacHRsCKC0sGya81dDblLgdkjSUemYLw== - dependencies: - tslib "^2.1.0" - -"@material/auto-init@15.0.0-canary.bc9ae6c9c.0": - version "15.0.0-canary.bc9ae6c9c.0" - resolved "https://registry.yarnpkg.com/@material/auto-init/-/auto-init-15.0.0-canary.bc9ae6c9c.0.tgz#9536732573cbe3db9613683496884592387c1e7b" - integrity sha512-uxzDq7q3c0Bu1pAsMugc1Ik9ftQYQqZY+5e2ybNplT8gTImJhNt4M2mMiMHbMANk2l3UgICmUyRSomgPBWCPIA== - dependencies: - "@material/base" "15.0.0-canary.bc9ae6c9c.0" - tslib "^2.1.0" - -"@material/banner@15.0.0-canary.bc9ae6c9c.0": - version "15.0.0-canary.bc9ae6c9c.0" - resolved "https://registry.yarnpkg.com/@material/banner/-/banner-15.0.0-canary.bc9ae6c9c.0.tgz#5b1053ebc4a07bfb5f92f6b457e87cd15ed6ebf7" - integrity sha512-SHeVoidCUFVhXANN6MNWxK9SZoTSgpIP8GZB7kAl52BywLxtV+FirTtLXkg/8RUkxZRyRWl7HvQ0ZFZa7QQAyA== - dependencies: - "@material/base" "15.0.0-canary.bc9ae6c9c.0" - "@material/button" "15.0.0-canary.bc9ae6c9c.0" - "@material/dom" "15.0.0-canary.bc9ae6c9c.0" - "@material/elevation" "15.0.0-canary.bc9ae6c9c.0" - "@material/feature-targeting" "15.0.0-canary.bc9ae6c9c.0" - "@material/ripple" "15.0.0-canary.bc9ae6c9c.0" - "@material/rtl" "15.0.0-canary.bc9ae6c9c.0" - "@material/shape" "15.0.0-canary.bc9ae6c9c.0" - "@material/theme" "15.0.0-canary.bc9ae6c9c.0" - "@material/tokens" "15.0.0-canary.bc9ae6c9c.0" - "@material/typography" "15.0.0-canary.bc9ae6c9c.0" - tslib "^2.1.0" - -"@material/base@15.0.0-canary.bc9ae6c9c.0": - version "15.0.0-canary.bc9ae6c9c.0" - resolved "https://registry.yarnpkg.com/@material/base/-/base-15.0.0-canary.bc9ae6c9c.0.tgz#99f7243759cc6833707f0bb555db723ea78b9eff" - integrity sha512-Fc3vGuOf+duGo22HTRP6dHdc+MUe0VqQfWOuKrn/wXKD62m0QQR2TqJd3rRhCumH557T5QUyheW943M3E+IGfg== - dependencies: - tslib "^2.1.0" - -"@material/button@15.0.0-canary.bc9ae6c9c.0": - version "15.0.0-canary.bc9ae6c9c.0" - resolved "https://registry.yarnpkg.com/@material/button/-/button-15.0.0-canary.bc9ae6c9c.0.tgz#adb43ffb0bf57cd634a0c31b6a5f26123e78c2c8" - integrity sha512-3AQgwrPZCTWHDJvwgKq7Cj+BurQ4wTjDdGL+FEnIGUAjJDskwi1yzx5tW2Wf/NxIi7IoPFyOY3UB41jwMiOrnw== - dependencies: - "@material/density" "15.0.0-canary.bc9ae6c9c.0" - "@material/dom" "15.0.0-canary.bc9ae6c9c.0" - "@material/elevation" "15.0.0-canary.bc9ae6c9c.0" - "@material/feature-targeting" "15.0.0-canary.bc9ae6c9c.0" - "@material/focus-ring" "15.0.0-canary.bc9ae6c9c.0" - "@material/ripple" "15.0.0-canary.bc9ae6c9c.0" - "@material/rtl" "15.0.0-canary.bc9ae6c9c.0" - "@material/shape" "15.0.0-canary.bc9ae6c9c.0" - "@material/theme" "15.0.0-canary.bc9ae6c9c.0" - "@material/tokens" "15.0.0-canary.bc9ae6c9c.0" - "@material/touch-target" "15.0.0-canary.bc9ae6c9c.0" - "@material/typography" "15.0.0-canary.bc9ae6c9c.0" - tslib "^2.1.0" - -"@material/card@15.0.0-canary.bc9ae6c9c.0": - version "15.0.0-canary.bc9ae6c9c.0" - resolved "https://registry.yarnpkg.com/@material/card/-/card-15.0.0-canary.bc9ae6c9c.0.tgz#772ba3d7397335740c3c2058f039be82696aa884" - integrity sha512-nPlhiWvbLmooTnBmV5gmzB0eLWSgLKsSRBYAbIBmO76Okgz1y+fQNLag+lpm/TDaHVsn5fmQJH8e0zIg0rYsQA== - dependencies: - "@material/dom" "15.0.0-canary.bc9ae6c9c.0" - "@material/elevation" "15.0.0-canary.bc9ae6c9c.0" - "@material/feature-targeting" "15.0.0-canary.bc9ae6c9c.0" - "@material/ripple" "15.0.0-canary.bc9ae6c9c.0" - "@material/rtl" "15.0.0-canary.bc9ae6c9c.0" - "@material/shape" "15.0.0-canary.bc9ae6c9c.0" - "@material/theme" "15.0.0-canary.bc9ae6c9c.0" - "@material/tokens" "15.0.0-canary.bc9ae6c9c.0" - tslib "^2.1.0" - -"@material/checkbox@15.0.0-canary.bc9ae6c9c.0": - version "15.0.0-canary.bc9ae6c9c.0" - resolved "https://registry.yarnpkg.com/@material/checkbox/-/checkbox-15.0.0-canary.bc9ae6c9c.0.tgz#b13784c068b137386c43ae409517176b986c5d49" - integrity sha512-4tpNnO1L0IppoMF3oeQn8F17t2n0WHB0D7mdJK9rhrujen/fLbekkIC82APB3fdGtLGg3qeNqDqPsJm1YnmrwA== - dependencies: - "@material/animation" "15.0.0-canary.bc9ae6c9c.0" - "@material/base" "15.0.0-canary.bc9ae6c9c.0" - "@material/density" "15.0.0-canary.bc9ae6c9c.0" - "@material/dom" "15.0.0-canary.bc9ae6c9c.0" - "@material/feature-targeting" "15.0.0-canary.bc9ae6c9c.0" - "@material/focus-ring" "15.0.0-canary.bc9ae6c9c.0" - "@material/ripple" "15.0.0-canary.bc9ae6c9c.0" - "@material/rtl" "15.0.0-canary.bc9ae6c9c.0" - "@material/theme" "15.0.0-canary.bc9ae6c9c.0" - "@material/touch-target" "15.0.0-canary.bc9ae6c9c.0" - tslib "^2.1.0" - -"@material/chips@15.0.0-canary.bc9ae6c9c.0": - version "15.0.0-canary.bc9ae6c9c.0" - resolved "https://registry.yarnpkg.com/@material/chips/-/chips-15.0.0-canary.bc9ae6c9c.0.tgz#a77ee7bf8ea9146156996c5632496ebca27520e9" - integrity sha512-fqHKvE5bSWK0bXVkf57MWxZtytGqYBZvvHIOs4JI9HPHEhaJy4CpSw562BEtbm3yFxxALoQknvPW2KYzvADnmA== - dependencies: - "@material/animation" "15.0.0-canary.bc9ae6c9c.0" - "@material/base" "15.0.0-canary.bc9ae6c9c.0" - "@material/checkbox" "15.0.0-canary.bc9ae6c9c.0" - "@material/density" "15.0.0-canary.bc9ae6c9c.0" - "@material/dom" "15.0.0-canary.bc9ae6c9c.0" - "@material/elevation" "15.0.0-canary.bc9ae6c9c.0" - "@material/feature-targeting" "15.0.0-canary.bc9ae6c9c.0" - "@material/focus-ring" "15.0.0-canary.bc9ae6c9c.0" - "@material/ripple" "15.0.0-canary.bc9ae6c9c.0" - "@material/rtl" "15.0.0-canary.bc9ae6c9c.0" - "@material/shape" "15.0.0-canary.bc9ae6c9c.0" - "@material/theme" "15.0.0-canary.bc9ae6c9c.0" - "@material/tokens" "15.0.0-canary.bc9ae6c9c.0" - "@material/touch-target" "15.0.0-canary.bc9ae6c9c.0" - "@material/typography" "15.0.0-canary.bc9ae6c9c.0" - safevalues "^0.3.4" - tslib "^2.1.0" - -"@material/circular-progress@15.0.0-canary.bc9ae6c9c.0": - version "15.0.0-canary.bc9ae6c9c.0" - resolved "https://registry.yarnpkg.com/@material/circular-progress/-/circular-progress-15.0.0-canary.bc9ae6c9c.0.tgz#800cb10a3a66f125a5ed8d4ae9fffdf236da5984" - integrity sha512-Lxe8BGAxQwCQqrLhrYrIP0Uok10h7aYS3RBXP41ph+5GmwJd5zdyE2t93qm2dyThvU6qKuXw9726Dtq/N+wvZQ== - dependencies: - "@material/animation" "15.0.0-canary.bc9ae6c9c.0" - "@material/base" "15.0.0-canary.bc9ae6c9c.0" - "@material/dom" "15.0.0-canary.bc9ae6c9c.0" - "@material/feature-targeting" "15.0.0-canary.bc9ae6c9c.0" - "@material/progress-indicator" "15.0.0-canary.bc9ae6c9c.0" - "@material/rtl" "15.0.0-canary.bc9ae6c9c.0" - "@material/theme" "15.0.0-canary.bc9ae6c9c.0" - tslib "^2.1.0" - -"@material/data-table@15.0.0-canary.bc9ae6c9c.0": - version "15.0.0-canary.bc9ae6c9c.0" - resolved "https://registry.yarnpkg.com/@material/data-table/-/data-table-15.0.0-canary.bc9ae6c9c.0.tgz#0b5b51ed771f9bba8a1b4746448dec25000325c1" - integrity sha512-j/7qplT9+sUpfe4pyWhPbl01qJA+OoNAG3VMJruBBR461ZBKyTi7ssKH9yksFGZ8eCEPkOsk/+kDxsiZvRWkeQ== - dependencies: - "@material/animation" "15.0.0-canary.bc9ae6c9c.0" - "@material/base" "15.0.0-canary.bc9ae6c9c.0" - "@material/checkbox" "15.0.0-canary.bc9ae6c9c.0" - "@material/density" "15.0.0-canary.bc9ae6c9c.0" - "@material/dom" "15.0.0-canary.bc9ae6c9c.0" - "@material/elevation" "15.0.0-canary.bc9ae6c9c.0" - "@material/feature-targeting" "15.0.0-canary.bc9ae6c9c.0" - "@material/icon-button" "15.0.0-canary.bc9ae6c9c.0" - "@material/linear-progress" "15.0.0-canary.bc9ae6c9c.0" - "@material/list" "15.0.0-canary.bc9ae6c9c.0" - "@material/menu" "15.0.0-canary.bc9ae6c9c.0" - "@material/rtl" "15.0.0-canary.bc9ae6c9c.0" - "@material/select" "15.0.0-canary.bc9ae6c9c.0" - "@material/shape" "15.0.0-canary.bc9ae6c9c.0" - "@material/theme" "15.0.0-canary.bc9ae6c9c.0" - "@material/tokens" "15.0.0-canary.bc9ae6c9c.0" - "@material/touch-target" "15.0.0-canary.bc9ae6c9c.0" - "@material/typography" "15.0.0-canary.bc9ae6c9c.0" - tslib "^2.1.0" - -"@material/density@15.0.0-canary.bc9ae6c9c.0": - version "15.0.0-canary.bc9ae6c9c.0" - resolved "https://registry.yarnpkg.com/@material/density/-/density-15.0.0-canary.bc9ae6c9c.0.tgz#83d7ef248a8d1818cddb01bcbfc947ab0ae6a952" - integrity sha512-Zt3u07fXrBWLW06Tl5fgvjicxNQMkFdawLyNTzZ5TvbXfVkErILLePwwGaw8LNcvzqJP6ABLA8jiR+sKNoJQCg== - dependencies: - tslib "^2.1.0" - -"@material/dialog@15.0.0-canary.bc9ae6c9c.0": - version "15.0.0-canary.bc9ae6c9c.0" - resolved "https://registry.yarnpkg.com/@material/dialog/-/dialog-15.0.0-canary.bc9ae6c9c.0.tgz#a12e676c9d41009a1f4d5617f386d6b00d6ecdf0" - integrity sha512-o+9a/fmwJ9+gY3Z/uhj/PMVJDq7it1NTWKJn2GwAKdB+fDkT4hb9qEdcxMPyvJJ5ups+XiKZo03+tZrD+38c1w== - dependencies: - "@material/animation" "15.0.0-canary.bc9ae6c9c.0" - "@material/base" "15.0.0-canary.bc9ae6c9c.0" - "@material/button" "15.0.0-canary.bc9ae6c9c.0" - "@material/dom" "15.0.0-canary.bc9ae6c9c.0" - "@material/elevation" "15.0.0-canary.bc9ae6c9c.0" - "@material/feature-targeting" "15.0.0-canary.bc9ae6c9c.0" - "@material/icon-button" "15.0.0-canary.bc9ae6c9c.0" - "@material/ripple" "15.0.0-canary.bc9ae6c9c.0" - "@material/rtl" "15.0.0-canary.bc9ae6c9c.0" - "@material/shape" "15.0.0-canary.bc9ae6c9c.0" - "@material/theme" "15.0.0-canary.bc9ae6c9c.0" - "@material/tokens" "15.0.0-canary.bc9ae6c9c.0" - "@material/touch-target" "15.0.0-canary.bc9ae6c9c.0" - "@material/typography" "15.0.0-canary.bc9ae6c9c.0" - tslib "^2.1.0" - -"@material/dom@15.0.0-canary.bc9ae6c9c.0": - version "15.0.0-canary.bc9ae6c9c.0" - resolved "https://registry.yarnpkg.com/@material/dom/-/dom-15.0.0-canary.bc9ae6c9c.0.tgz#960d25fdfed237c542560278465edb9c33ed44ec" - integrity sha512-ly78R7aoCJtundSUu0UROU+5pQD5Piae0Y1MkN6bs0724azeazX1KeXFeaf06JOXnlr5/41ol+fSUPowjoqnOg== - dependencies: - "@material/feature-targeting" "15.0.0-canary.bc9ae6c9c.0" - "@material/rtl" "15.0.0-canary.bc9ae6c9c.0" - tslib "^2.1.0" - -"@material/drawer@15.0.0-canary.bc9ae6c9c.0": - version "15.0.0-canary.bc9ae6c9c.0" - resolved "https://registry.yarnpkg.com/@material/drawer/-/drawer-15.0.0-canary.bc9ae6c9c.0.tgz#68838f1a12ddd2bb56795bd187d0ce0192689ce5" - integrity sha512-PFL4cEFnt7VTxDsuspFVNhsFDYyumjU0VWfj3PWB7XudsEfQ3lo85D3HCEtTTbRsCainGN8bgYNDNafLBqiigw== - dependencies: - "@material/animation" "15.0.0-canary.bc9ae6c9c.0" - "@material/base" "15.0.0-canary.bc9ae6c9c.0" - "@material/dom" "15.0.0-canary.bc9ae6c9c.0" - "@material/elevation" "15.0.0-canary.bc9ae6c9c.0" - "@material/feature-targeting" "15.0.0-canary.bc9ae6c9c.0" - "@material/list" "15.0.0-canary.bc9ae6c9c.0" - "@material/ripple" "15.0.0-canary.bc9ae6c9c.0" - "@material/rtl" "15.0.0-canary.bc9ae6c9c.0" - "@material/shape" "15.0.0-canary.bc9ae6c9c.0" - "@material/theme" "15.0.0-canary.bc9ae6c9c.0" - "@material/typography" "15.0.0-canary.bc9ae6c9c.0" - tslib "^2.1.0" - -"@material/elevation@15.0.0-canary.bc9ae6c9c.0": - version "15.0.0-canary.bc9ae6c9c.0" - resolved "https://registry.yarnpkg.com/@material/elevation/-/elevation-15.0.0-canary.bc9ae6c9c.0.tgz#d8ca5f4b1f387c95326a6220a21178d4e965b30c" - integrity sha512-Ro+Pk8jFuap+T0B0shA3xI1hs2b89dNQ2EIPCNjNMp87emHKAzJfhKb7EZGIwv3+gFLlVaLyIVkb94I89KLsyg== - dependencies: - "@material/animation" "15.0.0-canary.bc9ae6c9c.0" - "@material/base" "15.0.0-canary.bc9ae6c9c.0" - "@material/feature-targeting" "15.0.0-canary.bc9ae6c9c.0" - "@material/rtl" "15.0.0-canary.bc9ae6c9c.0" - "@material/theme" "15.0.0-canary.bc9ae6c9c.0" - tslib "^2.1.0" - -"@material/fab@15.0.0-canary.bc9ae6c9c.0": - version "15.0.0-canary.bc9ae6c9c.0" - resolved "https://registry.yarnpkg.com/@material/fab/-/fab-15.0.0-canary.bc9ae6c9c.0.tgz#7e75ae184555a6568e882e854657ad1515b34c00" - integrity sha512-dvU0KWMRglwJEQwmQtFAmJcAjzg9VFF6Aqj78bJYu/DAIGFJ1VTTTSgoXM/XCm1YyQEZ7kZRvxBO37CH54rSDg== - dependencies: - "@material/animation" "15.0.0-canary.bc9ae6c9c.0" - "@material/dom" "15.0.0-canary.bc9ae6c9c.0" - "@material/elevation" "15.0.0-canary.bc9ae6c9c.0" - "@material/feature-targeting" "15.0.0-canary.bc9ae6c9c.0" - "@material/focus-ring" "15.0.0-canary.bc9ae6c9c.0" - "@material/ripple" "15.0.0-canary.bc9ae6c9c.0" - "@material/rtl" "15.0.0-canary.bc9ae6c9c.0" - "@material/shape" "15.0.0-canary.bc9ae6c9c.0" - "@material/theme" "15.0.0-canary.bc9ae6c9c.0" - "@material/tokens" "15.0.0-canary.bc9ae6c9c.0" - "@material/touch-target" "15.0.0-canary.bc9ae6c9c.0" - "@material/typography" "15.0.0-canary.bc9ae6c9c.0" - tslib "^2.1.0" - -"@material/feature-targeting@15.0.0-canary.bc9ae6c9c.0": - version "15.0.0-canary.bc9ae6c9c.0" - resolved "https://registry.yarnpkg.com/@material/feature-targeting/-/feature-targeting-15.0.0-canary.bc9ae6c9c.0.tgz#f5fd69774664f20f176b3825072d7f2e48de7621" - integrity sha512-wkDjVcoVEYYaJvun28IXdln/foLgPD7n9ZC9TY76GErGCwTq+HWpU6wBAAk+ePmpRFDayw4vI4wBlaWGxLtysQ== - dependencies: - tslib "^2.1.0" - -"@material/floating-label@15.0.0-canary.bc9ae6c9c.0": - version "15.0.0-canary.bc9ae6c9c.0" - resolved "https://registry.yarnpkg.com/@material/floating-label/-/floating-label-15.0.0-canary.bc9ae6c9c.0.tgz#b1245304edd6dbeedeae0499f292e79f8b2c479a" - integrity sha512-bUWPtXzZITOD/2mkvLkEPO1ngDWmb74y0Kgbz6llHLOQBtycyJIpuoQJ1q2Ez0NM/tFLwPphhAgRqmL3YQ/Kzw== - dependencies: - "@material/animation" "15.0.0-canary.bc9ae6c9c.0" - "@material/base" "15.0.0-canary.bc9ae6c9c.0" - "@material/dom" "15.0.0-canary.bc9ae6c9c.0" - "@material/feature-targeting" "15.0.0-canary.bc9ae6c9c.0" - "@material/rtl" "15.0.0-canary.bc9ae6c9c.0" - "@material/theme" "15.0.0-canary.bc9ae6c9c.0" - "@material/typography" "15.0.0-canary.bc9ae6c9c.0" - tslib "^2.1.0" - -"@material/focus-ring@15.0.0-canary.bc9ae6c9c.0": - version "15.0.0-canary.bc9ae6c9c.0" - resolved "https://registry.yarnpkg.com/@material/focus-ring/-/focus-ring-15.0.0-canary.bc9ae6c9c.0.tgz#063396eefa5638edbbf99ac713c1087da1f8434c" - integrity sha512-cZHThVose3GvAlJzpJoBI1iqL6d1/Jj9hXrR+r8Mwtb1hBIUEG3hxfsRd4vGREuzROPlf0OgNf/V+YHoSwgR5w== - dependencies: - "@material/dom" "15.0.0-canary.bc9ae6c9c.0" - "@material/feature-targeting" "15.0.0-canary.bc9ae6c9c.0" - "@material/rtl" "15.0.0-canary.bc9ae6c9c.0" - -"@material/form-field@15.0.0-canary.bc9ae6c9c.0": - version "15.0.0-canary.bc9ae6c9c.0" - resolved "https://registry.yarnpkg.com/@material/form-field/-/form-field-15.0.0-canary.bc9ae6c9c.0.tgz#76d23e14f910a28081ccb438e094e04bbffadf19" - integrity sha512-+JFXy5X44Gue1CbZZAQ6YejnI203lebYwL0i6k0ylDpWHEOdD5xkF2PyHR28r9/65Ebcbwbff6q7kI1SGoT7MA== - dependencies: - "@material/base" "15.0.0-canary.bc9ae6c9c.0" - "@material/feature-targeting" "15.0.0-canary.bc9ae6c9c.0" - "@material/ripple" "15.0.0-canary.bc9ae6c9c.0" - "@material/rtl" "15.0.0-canary.bc9ae6c9c.0" - "@material/theme" "15.0.0-canary.bc9ae6c9c.0" - "@material/typography" "15.0.0-canary.bc9ae6c9c.0" - tslib "^2.1.0" - -"@material/icon-button@15.0.0-canary.bc9ae6c9c.0": - version "15.0.0-canary.bc9ae6c9c.0" - resolved "https://registry.yarnpkg.com/@material/icon-button/-/icon-button-15.0.0-canary.bc9ae6c9c.0.tgz#67246733d5e1aef1953208d3dfac01425d560ede" - integrity sha512-1a0MHgyIwOs4RzxrVljsqSizGYFlM1zY2AZaLDsgT4G3kzsplTx8HZQ022GpUCjAygW+WLvg4z1qAhQHvsbqlw== - dependencies: - "@material/base" "15.0.0-canary.bc9ae6c9c.0" - "@material/density" "15.0.0-canary.bc9ae6c9c.0" - "@material/dom" "15.0.0-canary.bc9ae6c9c.0" - "@material/elevation" "15.0.0-canary.bc9ae6c9c.0" - "@material/feature-targeting" "15.0.0-canary.bc9ae6c9c.0" - "@material/focus-ring" "15.0.0-canary.bc9ae6c9c.0" - "@material/ripple" "15.0.0-canary.bc9ae6c9c.0" - "@material/rtl" "15.0.0-canary.bc9ae6c9c.0" - "@material/theme" "15.0.0-canary.bc9ae6c9c.0" - "@material/touch-target" "15.0.0-canary.bc9ae6c9c.0" - tslib "^2.1.0" - -"@material/image-list@15.0.0-canary.bc9ae6c9c.0": - version "15.0.0-canary.bc9ae6c9c.0" - resolved "https://registry.yarnpkg.com/@material/image-list/-/image-list-15.0.0-canary.bc9ae6c9c.0.tgz#9a765ec6caa7e4761a19048679912abc759d7988" - integrity sha512-WKWmiYap2iu4QdqmeUSliLlN4O2Ueqa0OuVAYHn/TCzmQ2xmnhZ1pvDLbs6TplpOmlki7vFfe+aSt5SU9gwfOQ== - dependencies: - "@material/feature-targeting" "15.0.0-canary.bc9ae6c9c.0" - "@material/shape" "15.0.0-canary.bc9ae6c9c.0" - "@material/theme" "15.0.0-canary.bc9ae6c9c.0" - "@material/typography" "15.0.0-canary.bc9ae6c9c.0" - tslib "^2.1.0" - -"@material/layout-grid@15.0.0-canary.bc9ae6c9c.0": - version "15.0.0-canary.bc9ae6c9c.0" - resolved "https://registry.yarnpkg.com/@material/layout-grid/-/layout-grid-15.0.0-canary.bc9ae6c9c.0.tgz#44f972c0975baa36e14c8d82b69957b7e59c25d3" - integrity sha512-5GqmT6oTZhUGWIb+CLD0ZNyDyTiJsr/rm9oRIi3+vCujACwxFkON9tzBlZohdtFS16nuzUusthN6Jt9UrJcN6Q== - dependencies: - tslib "^2.1.0" - -"@material/line-ripple@15.0.0-canary.bc9ae6c9c.0": - version "15.0.0-canary.bc9ae6c9c.0" - resolved "https://registry.yarnpkg.com/@material/line-ripple/-/line-ripple-15.0.0-canary.bc9ae6c9c.0.tgz#0de6f3f4bcca06056ab0dec23a84a7a99fb0ecc4" - integrity sha512-8S30WXEuUdgDdBulzUDlPXD6qMzwCX9SxYb5mGDYLwl199cpSGdXHtGgEcCjokvnpLhdZhcT1Dsxeo1g2Evh5Q== - dependencies: - "@material/animation" "15.0.0-canary.bc9ae6c9c.0" - "@material/base" "15.0.0-canary.bc9ae6c9c.0" - "@material/feature-targeting" "15.0.0-canary.bc9ae6c9c.0" - "@material/theme" "15.0.0-canary.bc9ae6c9c.0" - tslib "^2.1.0" - -"@material/linear-progress@15.0.0-canary.bc9ae6c9c.0": - version "15.0.0-canary.bc9ae6c9c.0" - resolved "https://registry.yarnpkg.com/@material/linear-progress/-/linear-progress-15.0.0-canary.bc9ae6c9c.0.tgz#12650b19c776542b0b084792ca1d6894dbd54cf4" - integrity sha512-6EJpjrz6aoH2/gXLg9iMe0yF2C42hpQyZoHpmcgTLKeci85ktDvJIjwup8tnk8ULQyFiGiIrhXw2v2RSsiFjvQ== - dependencies: - "@material/animation" "15.0.0-canary.bc9ae6c9c.0" - "@material/base" "15.0.0-canary.bc9ae6c9c.0" - "@material/dom" "15.0.0-canary.bc9ae6c9c.0" - "@material/feature-targeting" "15.0.0-canary.bc9ae6c9c.0" - "@material/progress-indicator" "15.0.0-canary.bc9ae6c9c.0" - "@material/rtl" "15.0.0-canary.bc9ae6c9c.0" - "@material/theme" "15.0.0-canary.bc9ae6c9c.0" - tslib "^2.1.0" - -"@material/list@15.0.0-canary.bc9ae6c9c.0": - version "15.0.0-canary.bc9ae6c9c.0" - resolved "https://registry.yarnpkg.com/@material/list/-/list-15.0.0-canary.bc9ae6c9c.0.tgz#daaf0ca8cb9b68fb2df0877c12571741b8098ddb" - integrity sha512-TQ1ppqiCMQj/P7bGD4edbIIv4goczZUoiUAaPq/feb1dflvrFMzYqJ7tQRRCyBL8nRhJoI2x99tk8Q2RXvlGUQ== - dependencies: - "@material/base" "15.0.0-canary.bc9ae6c9c.0" - "@material/density" "15.0.0-canary.bc9ae6c9c.0" - "@material/dom" "15.0.0-canary.bc9ae6c9c.0" - "@material/feature-targeting" "15.0.0-canary.bc9ae6c9c.0" - "@material/ripple" "15.0.0-canary.bc9ae6c9c.0" - "@material/rtl" "15.0.0-canary.bc9ae6c9c.0" - "@material/shape" "15.0.0-canary.bc9ae6c9c.0" - "@material/theme" "15.0.0-canary.bc9ae6c9c.0" - "@material/tokens" "15.0.0-canary.bc9ae6c9c.0" - "@material/typography" "15.0.0-canary.bc9ae6c9c.0" - tslib "^2.1.0" - -"@material/menu-surface@15.0.0-canary.bc9ae6c9c.0": - version "15.0.0-canary.bc9ae6c9c.0" - resolved "https://registry.yarnpkg.com/@material/menu-surface/-/menu-surface-15.0.0-canary.bc9ae6c9c.0.tgz#213cc9b251e626c54e1f799b3b52d74659b3c549" - integrity sha512-dMtSPN+olTWE+08M5qe4ea1IZOhVryYqzK0Gyb2u1G75rSArUxCOB5rr6OC/ST3Mq3RS6zGuYo7srZt4534K9Q== - dependencies: - "@material/animation" "15.0.0-canary.bc9ae6c9c.0" - "@material/base" "15.0.0-canary.bc9ae6c9c.0" - "@material/elevation" "15.0.0-canary.bc9ae6c9c.0" - "@material/feature-targeting" "15.0.0-canary.bc9ae6c9c.0" - "@material/rtl" "15.0.0-canary.bc9ae6c9c.0" - "@material/shape" "15.0.0-canary.bc9ae6c9c.0" - "@material/theme" "15.0.0-canary.bc9ae6c9c.0" - tslib "^2.1.0" - -"@material/menu@15.0.0-canary.bc9ae6c9c.0": - version "15.0.0-canary.bc9ae6c9c.0" - resolved "https://registry.yarnpkg.com/@material/menu/-/menu-15.0.0-canary.bc9ae6c9c.0.tgz#162fbd5b608fbf6edd4a65b3963db947c0e4c96b" - integrity sha512-IlAh61xzrzxXs38QZlt74UYt8J431zGznSzDtB1Fqs6YFNd11QPKoiRXn1J2Qu/lUxbFV7i8NBKMCKtia0n6/Q== - dependencies: - "@material/base" "15.0.0-canary.bc9ae6c9c.0" - "@material/dom" "15.0.0-canary.bc9ae6c9c.0" - "@material/elevation" "15.0.0-canary.bc9ae6c9c.0" - "@material/feature-targeting" "15.0.0-canary.bc9ae6c9c.0" - "@material/list" "15.0.0-canary.bc9ae6c9c.0" - "@material/menu-surface" "15.0.0-canary.bc9ae6c9c.0" - "@material/ripple" "15.0.0-canary.bc9ae6c9c.0" - "@material/rtl" "15.0.0-canary.bc9ae6c9c.0" - "@material/shape" "15.0.0-canary.bc9ae6c9c.0" - "@material/theme" "15.0.0-canary.bc9ae6c9c.0" - "@material/tokens" "15.0.0-canary.bc9ae6c9c.0" - tslib "^2.1.0" - -"@material/notched-outline@15.0.0-canary.bc9ae6c9c.0": - version "15.0.0-canary.bc9ae6c9c.0" - resolved "https://registry.yarnpkg.com/@material/notched-outline/-/notched-outline-15.0.0-canary.bc9ae6c9c.0.tgz#94d4c7646e75fad9ca78ad66487a3f7445030664" - integrity sha512-WuurMg44xexkvLTBTnsO0A+qnzFjpcPdvgWBGstBepYozsvSF9zJGdb1x7Zv1MmqbpYh/Ohnuxtb/Y3jOh6irg== - dependencies: - "@material/base" "15.0.0-canary.bc9ae6c9c.0" - "@material/feature-targeting" "15.0.0-canary.bc9ae6c9c.0" - "@material/floating-label" "15.0.0-canary.bc9ae6c9c.0" - "@material/rtl" "15.0.0-canary.bc9ae6c9c.0" - "@material/shape" "15.0.0-canary.bc9ae6c9c.0" - "@material/theme" "15.0.0-canary.bc9ae6c9c.0" - tslib "^2.1.0" - -"@material/progress-indicator@15.0.0-canary.bc9ae6c9c.0": - version "15.0.0-canary.bc9ae6c9c.0" - resolved "https://registry.yarnpkg.com/@material/progress-indicator/-/progress-indicator-15.0.0-canary.bc9ae6c9c.0.tgz#b440bff7e8b351af7eaf8fa7663f451e7ee112f4" - integrity sha512-uOnsvqw5F2fkeTnTl4MrYzjI7KCLmmLyZaM0cgLNuLsWVlddQE+SGMl28tENx7DUK3HebWq0FxCP8f25LuDD+w== - dependencies: - tslib "^2.1.0" - -"@material/radio@15.0.0-canary.bc9ae6c9c.0": - version "15.0.0-canary.bc9ae6c9c.0" - resolved "https://registry.yarnpkg.com/@material/radio/-/radio-15.0.0-canary.bc9ae6c9c.0.tgz#18a1724eb4d394faf7a485f116c8353d3685c0ee" - integrity sha512-ehzOK+U1IxQN+OQjgD2lsnf1t7t7RAwQzeO6Czkiuid29ookYbQynWuLWk7NW8H8ohl7lnmfqTP1xSNkkL/F0g== - dependencies: - "@material/animation" "15.0.0-canary.bc9ae6c9c.0" - "@material/base" "15.0.0-canary.bc9ae6c9c.0" - "@material/density" "15.0.0-canary.bc9ae6c9c.0" - "@material/dom" "15.0.0-canary.bc9ae6c9c.0" - "@material/feature-targeting" "15.0.0-canary.bc9ae6c9c.0" - "@material/focus-ring" "15.0.0-canary.bc9ae6c9c.0" - "@material/ripple" "15.0.0-canary.bc9ae6c9c.0" - "@material/theme" "15.0.0-canary.bc9ae6c9c.0" - "@material/touch-target" "15.0.0-canary.bc9ae6c9c.0" - tslib "^2.1.0" - -"@material/ripple@15.0.0-canary.bc9ae6c9c.0": - version "15.0.0-canary.bc9ae6c9c.0" - resolved "https://registry.yarnpkg.com/@material/ripple/-/ripple-15.0.0-canary.bc9ae6c9c.0.tgz#1b64bdb47d1e5016bb0663d8b045a7e63048ad86" - integrity sha512-JfLW+g3GMVDv4cruQ19+HUxpKVdWCldFlIPw1UYezz2h3WTNDy05S3uP2zUdXzZ01C3dkBFviv4nqZ0GCT16MA== - dependencies: - "@material/animation" "15.0.0-canary.bc9ae6c9c.0" - "@material/base" "15.0.0-canary.bc9ae6c9c.0" - "@material/dom" "15.0.0-canary.bc9ae6c9c.0" - "@material/feature-targeting" "15.0.0-canary.bc9ae6c9c.0" - "@material/rtl" "15.0.0-canary.bc9ae6c9c.0" - "@material/theme" "15.0.0-canary.bc9ae6c9c.0" - tslib "^2.1.0" - -"@material/rtl@15.0.0-canary.bc9ae6c9c.0": - version "15.0.0-canary.bc9ae6c9c.0" - resolved "https://registry.yarnpkg.com/@material/rtl/-/rtl-15.0.0-canary.bc9ae6c9c.0.tgz#a9ba66d0cec2d1d38892d3e9cb65157fcf012dfa" - integrity sha512-SkKLNLFp5QtG7/JEFg9R92qq4MzTcZ5As6sWbH7rRg6ahTHoJEuqE+pOb9Vrtbj84k5gtX+vCYPvCILtSlr2uw== - dependencies: - "@material/theme" "15.0.0-canary.bc9ae6c9c.0" - tslib "^2.1.0" - -"@material/segmented-button@15.0.0-canary.bc9ae6c9c.0": - version "15.0.0-canary.bc9ae6c9c.0" - resolved "https://registry.yarnpkg.com/@material/segmented-button/-/segmented-button-15.0.0-canary.bc9ae6c9c.0.tgz#635e5a7dee12163b08a78872a0cacd4121024abd" - integrity sha512-YDwkCWP9l5mIZJ7pZJZ2hMDxfBlIGVJ+deNzr8O+Z7/xC5LGXbl4R5aPtUVHygvXAXxpf5096ZD+dSXzYzvWlw== - dependencies: - "@material/base" "15.0.0-canary.bc9ae6c9c.0" - "@material/elevation" "15.0.0-canary.bc9ae6c9c.0" - "@material/feature-targeting" "15.0.0-canary.bc9ae6c9c.0" - "@material/ripple" "15.0.0-canary.bc9ae6c9c.0" - "@material/theme" "15.0.0-canary.bc9ae6c9c.0" - "@material/touch-target" "15.0.0-canary.bc9ae6c9c.0" - "@material/typography" "15.0.0-canary.bc9ae6c9c.0" - tslib "^2.1.0" - -"@material/select@15.0.0-canary.bc9ae6c9c.0": - version "15.0.0-canary.bc9ae6c9c.0" - resolved "https://registry.yarnpkg.com/@material/select/-/select-15.0.0-canary.bc9ae6c9c.0.tgz#bd5039d0cb123fef358e85fdd4a002556f11598b" - integrity sha512-unfOWVf7T0sixVG+3k3RTuATfzqvCF6QAzA6J9rlCh/Tq4HuIBNDdV4z19IVu4zwmgWYxY0iSvqWUvdJJYwakQ== - dependencies: - "@material/animation" "15.0.0-canary.bc9ae6c9c.0" - "@material/base" "15.0.0-canary.bc9ae6c9c.0" - "@material/density" "15.0.0-canary.bc9ae6c9c.0" - "@material/dom" "15.0.0-canary.bc9ae6c9c.0" - "@material/elevation" "15.0.0-canary.bc9ae6c9c.0" - "@material/feature-targeting" "15.0.0-canary.bc9ae6c9c.0" - "@material/floating-label" "15.0.0-canary.bc9ae6c9c.0" - "@material/line-ripple" "15.0.0-canary.bc9ae6c9c.0" - "@material/list" "15.0.0-canary.bc9ae6c9c.0" - "@material/menu" "15.0.0-canary.bc9ae6c9c.0" - "@material/menu-surface" "15.0.0-canary.bc9ae6c9c.0" - "@material/notched-outline" "15.0.0-canary.bc9ae6c9c.0" - "@material/ripple" "15.0.0-canary.bc9ae6c9c.0" - "@material/rtl" "15.0.0-canary.bc9ae6c9c.0" - "@material/shape" "15.0.0-canary.bc9ae6c9c.0" - "@material/theme" "15.0.0-canary.bc9ae6c9c.0" - "@material/tokens" "15.0.0-canary.bc9ae6c9c.0" - "@material/typography" "15.0.0-canary.bc9ae6c9c.0" - tslib "^2.1.0" - -"@material/shape@15.0.0-canary.bc9ae6c9c.0": - version "15.0.0-canary.bc9ae6c9c.0" - resolved "https://registry.yarnpkg.com/@material/shape/-/shape-15.0.0-canary.bc9ae6c9c.0.tgz#c597f8e439dc40799d2de3cfa62006faaf334a20" - integrity sha512-Dsvr771ZKC46ODzoixLdGwlLEQLfxfLrtnRojXABoZf5G3o9KtJU+J+5Ld5aa960OAsCzzANuaub4iR88b1guA== - dependencies: - "@material/feature-targeting" "15.0.0-canary.bc9ae6c9c.0" - "@material/rtl" "15.0.0-canary.bc9ae6c9c.0" - "@material/theme" "15.0.0-canary.bc9ae6c9c.0" - tslib "^2.1.0" - -"@material/slider@15.0.0-canary.bc9ae6c9c.0": - version "15.0.0-canary.bc9ae6c9c.0" - resolved "https://registry.yarnpkg.com/@material/slider/-/slider-15.0.0-canary.bc9ae6c9c.0.tgz#5f9fa85cb0b95f45042b14a510d20ae894ee027c" - integrity sha512-3AEu+7PwW4DSNLndue47dh2u7ga4hDJRYmuu7wnJCIWJBnLCkp6C92kNc4Rj5iQY2ftJio5aj1gqryluh5tlYg== - dependencies: - "@material/animation" "15.0.0-canary.bc9ae6c9c.0" - "@material/base" "15.0.0-canary.bc9ae6c9c.0" - "@material/dom" "15.0.0-canary.bc9ae6c9c.0" - "@material/elevation" "15.0.0-canary.bc9ae6c9c.0" - "@material/feature-targeting" "15.0.0-canary.bc9ae6c9c.0" - "@material/ripple" "15.0.0-canary.bc9ae6c9c.0" - "@material/rtl" "15.0.0-canary.bc9ae6c9c.0" - "@material/theme" "15.0.0-canary.bc9ae6c9c.0" - "@material/tokens" "15.0.0-canary.bc9ae6c9c.0" - "@material/typography" "15.0.0-canary.bc9ae6c9c.0" - tslib "^2.1.0" - -"@material/snackbar@15.0.0-canary.bc9ae6c9c.0": - version "15.0.0-canary.bc9ae6c9c.0" - resolved "https://registry.yarnpkg.com/@material/snackbar/-/snackbar-15.0.0-canary.bc9ae6c9c.0.tgz#9f482fab88c3be85d06b450b67ac0008b6352875" - integrity sha512-TwwQSYxfGK6mc03/rdDamycND6o+1p61WNd7ElZv1F1CLxB4ihRjbCoH7Qo+oVDaP8CTpjeclka+24RLhQq0mA== - dependencies: - "@material/animation" "15.0.0-canary.bc9ae6c9c.0" - "@material/base" "15.0.0-canary.bc9ae6c9c.0" - "@material/button" "15.0.0-canary.bc9ae6c9c.0" - "@material/dom" "15.0.0-canary.bc9ae6c9c.0" - "@material/elevation" "15.0.0-canary.bc9ae6c9c.0" - "@material/feature-targeting" "15.0.0-canary.bc9ae6c9c.0" - "@material/icon-button" "15.0.0-canary.bc9ae6c9c.0" - "@material/ripple" "15.0.0-canary.bc9ae6c9c.0" - "@material/rtl" "15.0.0-canary.bc9ae6c9c.0" - "@material/shape" "15.0.0-canary.bc9ae6c9c.0" - "@material/theme" "15.0.0-canary.bc9ae6c9c.0" - "@material/tokens" "15.0.0-canary.bc9ae6c9c.0" - "@material/typography" "15.0.0-canary.bc9ae6c9c.0" - tslib "^2.1.0" - -"@material/switch@15.0.0-canary.bc9ae6c9c.0": - version "15.0.0-canary.bc9ae6c9c.0" - resolved "https://registry.yarnpkg.com/@material/switch/-/switch-15.0.0-canary.bc9ae6c9c.0.tgz#3de9394d2f23dc7bcc57bf633dde68498356f194" - integrity sha512-OjUjtT0kRz1ASAsOS+dNzwMwvsjmqy5edK57692qmrP6bL4GblFfBDoiNJ6t0AN4OaKcmL5Hy/xNrTdOZW7Qqw== - dependencies: - "@material/animation" "15.0.0-canary.bc9ae6c9c.0" - "@material/base" "15.0.0-canary.bc9ae6c9c.0" - "@material/density" "15.0.0-canary.bc9ae6c9c.0" - "@material/dom" "15.0.0-canary.bc9ae6c9c.0" - "@material/elevation" "15.0.0-canary.bc9ae6c9c.0" - "@material/feature-targeting" "15.0.0-canary.bc9ae6c9c.0" - "@material/focus-ring" "15.0.0-canary.bc9ae6c9c.0" - "@material/ripple" "15.0.0-canary.bc9ae6c9c.0" - "@material/rtl" "15.0.0-canary.bc9ae6c9c.0" - "@material/shape" "15.0.0-canary.bc9ae6c9c.0" - "@material/theme" "15.0.0-canary.bc9ae6c9c.0" - "@material/tokens" "15.0.0-canary.bc9ae6c9c.0" - safevalues "^0.3.4" - tslib "^2.1.0" - -"@material/tab-bar@15.0.0-canary.bc9ae6c9c.0": - version "15.0.0-canary.bc9ae6c9c.0" - resolved "https://registry.yarnpkg.com/@material/tab-bar/-/tab-bar-15.0.0-canary.bc9ae6c9c.0.tgz#952ce40f811a8fe1d54c1698454c9baf84a57e9d" - integrity sha512-Xmtq0wJGfu5k+zQeFeNsr4bUKv7L+feCmUp/gsapJ655LQKMXOUQZtSv9ZqWOfrCMy55hoF1CzGFV+oN3tyWWQ== - dependencies: - "@material/animation" "15.0.0-canary.bc9ae6c9c.0" - "@material/base" "15.0.0-canary.bc9ae6c9c.0" - "@material/density" "15.0.0-canary.bc9ae6c9c.0" - "@material/elevation" "15.0.0-canary.bc9ae6c9c.0" - "@material/feature-targeting" "15.0.0-canary.bc9ae6c9c.0" - "@material/tab" "15.0.0-canary.bc9ae6c9c.0" - "@material/tab-indicator" "15.0.0-canary.bc9ae6c9c.0" - "@material/tab-scroller" "15.0.0-canary.bc9ae6c9c.0" - "@material/theme" "15.0.0-canary.bc9ae6c9c.0" - "@material/tokens" "15.0.0-canary.bc9ae6c9c.0" - "@material/typography" "15.0.0-canary.bc9ae6c9c.0" - tslib "^2.1.0" - -"@material/tab-indicator@15.0.0-canary.bc9ae6c9c.0": - version "15.0.0-canary.bc9ae6c9c.0" - resolved "https://registry.yarnpkg.com/@material/tab-indicator/-/tab-indicator-15.0.0-canary.bc9ae6c9c.0.tgz#be37f0cf107c23da64efd4f385130d7d22a55b9c" - integrity sha512-despCJYi1GrDDq7F2hvLQkObHnSLZPPDxnOzU16zJ6FNYvIdszgfzn2HgAZ6pl5hLOexQ8cla6cAqjTDuaJBhQ== - dependencies: - "@material/animation" "15.0.0-canary.bc9ae6c9c.0" - "@material/base" "15.0.0-canary.bc9ae6c9c.0" - "@material/feature-targeting" "15.0.0-canary.bc9ae6c9c.0" - "@material/theme" "15.0.0-canary.bc9ae6c9c.0" - tslib "^2.1.0" - -"@material/tab-scroller@15.0.0-canary.bc9ae6c9c.0": - version "15.0.0-canary.bc9ae6c9c.0" - resolved "https://registry.yarnpkg.com/@material/tab-scroller/-/tab-scroller-15.0.0-canary.bc9ae6c9c.0.tgz#fb7f85a6d89cc3ec60c398cf637d201262b9c749" - integrity sha512-QWHG/EWxirj4V9u2IHz+OSY9XCWrnNrPnNgEufxAJVUKV/A8ma1DYeFSQqxhX709R8wKGdycJksg0Flkl7Gq7w== - dependencies: - "@material/animation" "15.0.0-canary.bc9ae6c9c.0" - "@material/base" "15.0.0-canary.bc9ae6c9c.0" - "@material/dom" "15.0.0-canary.bc9ae6c9c.0" - "@material/feature-targeting" "15.0.0-canary.bc9ae6c9c.0" - "@material/tab" "15.0.0-canary.bc9ae6c9c.0" - tslib "^2.1.0" - -"@material/tab@15.0.0-canary.bc9ae6c9c.0": - version "15.0.0-canary.bc9ae6c9c.0" - resolved "https://registry.yarnpkg.com/@material/tab/-/tab-15.0.0-canary.bc9ae6c9c.0.tgz#447482c5d13ce95fa502769e1f4bd91aa28b499f" - integrity sha512-s/L9otAwn/pZwVQZBRQJmPqYeNbjoEbzbjMpDQf/VBG/6dJ+aP03ilIBEkqo8NVnCoChqcdtVCoDNRtbU+yp6w== - dependencies: - "@material/base" "15.0.0-canary.bc9ae6c9c.0" - "@material/elevation" "15.0.0-canary.bc9ae6c9c.0" - "@material/feature-targeting" "15.0.0-canary.bc9ae6c9c.0" - "@material/focus-ring" "15.0.0-canary.bc9ae6c9c.0" - "@material/ripple" "15.0.0-canary.bc9ae6c9c.0" - "@material/rtl" "15.0.0-canary.bc9ae6c9c.0" - "@material/tab-indicator" "15.0.0-canary.bc9ae6c9c.0" - "@material/theme" "15.0.0-canary.bc9ae6c9c.0" - "@material/tokens" "15.0.0-canary.bc9ae6c9c.0" - "@material/typography" "15.0.0-canary.bc9ae6c9c.0" - tslib "^2.1.0" - -"@material/textfield@15.0.0-canary.bc9ae6c9c.0": - version "15.0.0-canary.bc9ae6c9c.0" - resolved "https://registry.yarnpkg.com/@material/textfield/-/textfield-15.0.0-canary.bc9ae6c9c.0.tgz#177df6b286da09015153a3eadb9f6e7ddd990676" - integrity sha512-R3qRex9kCaZIAK8DuxPnVC42R0OaW7AB7fsFknDKeTeVQvRcbnV8E+iWSdqTiGdsi6QQHifX8idUrXw+O45zPw== - dependencies: - "@material/animation" "15.0.0-canary.bc9ae6c9c.0" - "@material/base" "15.0.0-canary.bc9ae6c9c.0" - "@material/density" "15.0.0-canary.bc9ae6c9c.0" - "@material/dom" "15.0.0-canary.bc9ae6c9c.0" - "@material/feature-targeting" "15.0.0-canary.bc9ae6c9c.0" - "@material/floating-label" "15.0.0-canary.bc9ae6c9c.0" - "@material/line-ripple" "15.0.0-canary.bc9ae6c9c.0" - "@material/notched-outline" "15.0.0-canary.bc9ae6c9c.0" - "@material/ripple" "15.0.0-canary.bc9ae6c9c.0" - "@material/rtl" "15.0.0-canary.bc9ae6c9c.0" - "@material/shape" "15.0.0-canary.bc9ae6c9c.0" - "@material/theme" "15.0.0-canary.bc9ae6c9c.0" - "@material/tokens" "15.0.0-canary.bc9ae6c9c.0" - "@material/typography" "15.0.0-canary.bc9ae6c9c.0" - tslib "^2.1.0" - -"@material/theme@15.0.0-canary.bc9ae6c9c.0": - version "15.0.0-canary.bc9ae6c9c.0" - resolved "https://registry.yarnpkg.com/@material/theme/-/theme-15.0.0-canary.bc9ae6c9c.0.tgz#32e8571f6b323cafb3f2f6104c06e40f2d7f37e3" - integrity sha512-CpUwXGE0dbhxQ45Hu9r9wbJtO/MAlv5ER4tBHA9tp/K+SU+lDgurBE2touFMg5INmdfVNtdumxb0nPPLaNQcUg== - dependencies: - "@material/feature-targeting" "15.0.0-canary.bc9ae6c9c.0" - tslib "^2.1.0" - -"@material/tokens@15.0.0-canary.bc9ae6c9c.0": - version "15.0.0-canary.bc9ae6c9c.0" - resolved "https://registry.yarnpkg.com/@material/tokens/-/tokens-15.0.0-canary.bc9ae6c9c.0.tgz#b6833e9186d85c0707ebac2992098b345fe86ecd" - integrity sha512-nbEuGj05txWz6ZMUanpM47SaAD7soyjKILR+XwDell9Zg3bGhsnexCNXPEz2fD+YgomS+jM5XmIcaJJHg/H93Q== - dependencies: - "@material/elevation" "15.0.0-canary.bc9ae6c9c.0" - -"@material/tooltip@15.0.0-canary.bc9ae6c9c.0": - version "15.0.0-canary.bc9ae6c9c.0" - resolved "https://registry.yarnpkg.com/@material/tooltip/-/tooltip-15.0.0-canary.bc9ae6c9c.0.tgz#e5703754d44d0daf9fccbaa66fc4dd3aa22b2a5b" - integrity sha512-UzuXp0b9NuWuYLYpPguxrjbJnCmT/Cco8CkjI/6JajxaeA3o2XEBbQfRMTq8PTafuBjCHTc0b0mQY7rtxUp1Gg== - dependencies: - "@material/animation" "15.0.0-canary.bc9ae6c9c.0" - "@material/base" "15.0.0-canary.bc9ae6c9c.0" - "@material/button" "15.0.0-canary.bc9ae6c9c.0" - "@material/dom" "15.0.0-canary.bc9ae6c9c.0" - "@material/elevation" "15.0.0-canary.bc9ae6c9c.0" - "@material/feature-targeting" "15.0.0-canary.bc9ae6c9c.0" - "@material/rtl" "15.0.0-canary.bc9ae6c9c.0" - "@material/shape" "15.0.0-canary.bc9ae6c9c.0" - "@material/theme" "15.0.0-canary.bc9ae6c9c.0" - "@material/tokens" "15.0.0-canary.bc9ae6c9c.0" - "@material/typography" "15.0.0-canary.bc9ae6c9c.0" - safevalues "^0.3.4" - tslib "^2.1.0" - -"@material/top-app-bar@15.0.0-canary.bc9ae6c9c.0": - version "15.0.0-canary.bc9ae6c9c.0" - resolved "https://registry.yarnpkg.com/@material/top-app-bar/-/top-app-bar-15.0.0-canary.bc9ae6c9c.0.tgz#e996435725f36991a6ca80604e032d21527e076d" - integrity sha512-vJWjsvqtdSD5+yQ/9vgoBtBSCvPJ5uF/DVssv8Hdhgs1PYaAcODUi77kdi0+sy/TaWyOsTkQixqmwnFS16zesA== - dependencies: - "@material/animation" "15.0.0-canary.bc9ae6c9c.0" - "@material/base" "15.0.0-canary.bc9ae6c9c.0" - "@material/elevation" "15.0.0-canary.bc9ae6c9c.0" - "@material/ripple" "15.0.0-canary.bc9ae6c9c.0" - "@material/rtl" "15.0.0-canary.bc9ae6c9c.0" - "@material/shape" "15.0.0-canary.bc9ae6c9c.0" - "@material/theme" "15.0.0-canary.bc9ae6c9c.0" - "@material/typography" "15.0.0-canary.bc9ae6c9c.0" - tslib "^2.1.0" - -"@material/touch-target@15.0.0-canary.bc9ae6c9c.0": - version "15.0.0-canary.bc9ae6c9c.0" - resolved "https://registry.yarnpkg.com/@material/touch-target/-/touch-target-15.0.0-canary.bc9ae6c9c.0.tgz#3416302f86483510e47a8aef9392b0a77784652d" - integrity sha512-AqYh9fjt+tv4ZE0C6MeYHblS2H+XwLbDl2mtyrK0DOEnCVQk5/l5ImKDfhrUdFWHvS4a5nBM4AA+sa7KaroLoA== - dependencies: - "@material/base" "15.0.0-canary.bc9ae6c9c.0" - "@material/feature-targeting" "15.0.0-canary.bc9ae6c9c.0" - "@material/rtl" "15.0.0-canary.bc9ae6c9c.0" - "@material/theme" "15.0.0-canary.bc9ae6c9c.0" - tslib "^2.1.0" - -"@material/typography@15.0.0-canary.bc9ae6c9c.0": - version "15.0.0-canary.bc9ae6c9c.0" - resolved "https://registry.yarnpkg.com/@material/typography/-/typography-15.0.0-canary.bc9ae6c9c.0.tgz#1ca0641ef8a91945ca01a1aa6651db434741b37b" - integrity sha512-CKsG1zyv34AKPNyZC8olER2OdPII64iR2SzQjpqh1UUvmIFiMPk23LvQ1OnC5aCB14pOXzmVgvJt31r9eNdZ6Q== - dependencies: - "@material/feature-targeting" "15.0.0-canary.bc9ae6c9c.0" - "@material/theme" "15.0.0-canary.bc9ae6c9c.0" - tslib "^2.1.0" - -"@netlify/framework-info@^9.8.13": - version "9.9.2" - resolved "https://registry.yarnpkg.com/@netlify/framework-info/-/framework-info-9.9.2.tgz#e25e45eefa2ecababc0bad349f95710747514d42" - integrity sha512-IIJQ/mMCv7IvGRKujVXH9Jbyb19LCVUaFWoABljmbMHmALSFIL0twGx30SuHUPR1cXK6fH7KK5r4UsyvkpJ3EA== - dependencies: - ajv "^8.12.0" - filter-obj "^5.0.0" - find-up "^6.3.0" - is-plain-obj "^4.0.0" - locate-path "^7.0.0" - p-filter "^4.0.0" - p-locate "^6.0.0" - read-package-up "^11.0.0" - semver "^7.3.8" - -"@ngtools/webpack@16.2.16": - version "16.2.16" - resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-16.2.16.tgz#512da8f3459faafd0cc1f7f7cbec96b678377be6" - integrity sha512-4gm2allK0Pjy/Lxb9IGRnhEZNEOJSOTWwy09VOdHouV2ODRK7Tto2LgteaFJUUSLkuvWRsI7pfuA6yrz8KDfHw== - -"@ngx-translate/core@^15.0.0": - version "15.0.0" - resolved "https://registry.yarnpkg.com/@ngx-translate/core/-/core-15.0.0.tgz#0fe55b9bd47e75b03d1123658f15fb7b5a534f3c" - integrity sha512-Am5uiuR0bOOxyoercDnAA3rJVizo4RRqJHo8N3RqJ+XfzVP/I845yEnMADykOHvM6HkVm4SZSnJBOiz0Anx5BA== - -"@nodelib/fs.scandir@2.1.5": - version "2.1.5" - resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" - integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== - dependencies: - "@nodelib/fs.stat" "2.0.5" - run-parallel "^1.1.9" - -"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": - version "2.0.5" - resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" - integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== - -"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8": - version "1.2.8" - resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" - integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== - dependencies: - "@nodelib/fs.scandir" "2.1.5" - fastq "^1.6.0" - -"@npmcli/fs@^2.1.0": - version "2.1.2" - resolved "https://registry.yarnpkg.com/@npmcli/fs/-/fs-2.1.2.tgz#a9e2541a4a2fec2e69c29b35e6060973da79b865" - integrity sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ== - dependencies: - "@gar/promisify" "^1.1.3" - semver "^7.3.5" - -"@npmcli/fs@^3.1.0": - version "3.1.1" - resolved "https://registry.yarnpkg.com/@npmcli/fs/-/fs-3.1.1.tgz#59cdaa5adca95d135fc00f2bb53f5771575ce726" - integrity sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg== - dependencies: - semver "^7.3.5" - -"@npmcli/git@^4.0.0": - version "4.1.0" - resolved "https://registry.yarnpkg.com/@npmcli/git/-/git-4.1.0.tgz#ab0ad3fd82bc4d8c1351b6c62f0fa56e8fe6afa6" - integrity sha512-9hwoB3gStVfa0N31ymBmrX+GuDGdVA/QWShZVqE0HK2Af+7QGGrCTbZia/SW0ImUTjTne7SP91qxDmtXvDHRPQ== - dependencies: - "@npmcli/promise-spawn" "^6.0.0" - lru-cache "^7.4.4" - npm-pick-manifest "^8.0.0" - proc-log "^3.0.0" - promise-inflight "^1.0.1" - promise-retry "^2.0.1" - semver "^7.3.5" - which "^3.0.0" - -"@npmcli/installed-package-contents@^2.0.1": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@npmcli/installed-package-contents/-/installed-package-contents-2.1.0.tgz#63048e5f6e40947a3a88dcbcb4fd9b76fdd37c17" - integrity sha512-c8UuGLeZpm69BryRykLuKRyKFZYJsZSCT4aVY5ds4omyZqJ172ApzgfKJ5eV/r3HgLdUYgFVe54KSFVjKoe27w== - dependencies: - npm-bundled "^3.0.0" - npm-normalize-package-bin "^3.0.0" - -"@npmcli/move-file@^2.0.0": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@npmcli/move-file/-/move-file-2.0.1.tgz#26f6bdc379d87f75e55739bab89db525b06100e4" - integrity sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ== - dependencies: - mkdirp "^1.0.4" - rimraf "^3.0.2" - -"@npmcli/node-gyp@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@npmcli/node-gyp/-/node-gyp-3.0.0.tgz#101b2d0490ef1aa20ed460e4c0813f0db560545a" - integrity sha512-gp8pRXC2oOxu0DUE1/M3bYtb1b3/DbJ5aM113+XJBgfXdussRAsX0YOrOhdd8WvnAR6auDBvJomGAkLKA5ydxA== - -"@npmcli/promise-spawn@^6.0.0", "@npmcli/promise-spawn@^6.0.1": - version "6.0.2" - resolved "https://registry.yarnpkg.com/@npmcli/promise-spawn/-/promise-spawn-6.0.2.tgz#c8bc4fa2bd0f01cb979d8798ba038f314cfa70f2" - integrity sha512-gGq0NJkIGSwdbUt4yhdF8ZrmkGKVz9vAdVzpOfnom+V8PLSmSOVhZwbNvZZS1EYcJN5hzzKBxmmVVAInM6HQLg== - dependencies: - which "^3.0.0" - -"@npmcli/run-script@^6.0.0": - version "6.0.2" - resolved "https://registry.yarnpkg.com/@npmcli/run-script/-/run-script-6.0.2.tgz#a25452d45ee7f7fb8c16dfaf9624423c0c0eb885" - integrity sha512-NCcr1uQo1k5U+SYlnIrbAh3cxy+OQT1VtqiAbxdymSlptbzBb62AjH2xXgjNCoP073hoa1CfCAcwoZ8k96C4nA== - dependencies: - "@npmcli/node-gyp" "^3.0.0" - "@npmcli/promise-spawn" "^6.0.0" - node-gyp "^9.0.0" - read-package-json-fast "^3.0.0" - which "^3.0.0" - -"@nrwl/devkit@16.5.1": - version "16.5.1" - resolved "https://registry.yarnpkg.com/@nrwl/devkit/-/devkit-16.5.1.tgz#43985cc1105e85afd8323586477c4a0d1b2eeee3" - integrity sha512-NB+DE/+AFJ7lKH/WBFyatJEhcZGj25F24ncDkwjZ6MzEiSOGOJS0LaV/R+VUsmS5EHTPXYOpn3zHWWAcJhyOmA== - dependencies: - "@nx/devkit" "16.5.1" - -"@nrwl/tao@16.5.1": - version "16.5.1" - resolved "https://registry.yarnpkg.com/@nrwl/tao/-/tao-16.5.1.tgz#e6e6b1ab73238497d4d9f014b30af18722e73503" - integrity sha512-x+gi/fKdM6uQNIti9exFlm3V5LBP3Y8vOEziO42HdOigyrXa0S0HD2WMpccmp6PclYKhwEDUjKJ39xh5sdh4Ig== - dependencies: - nx "16.5.1" - -"@nx/devkit@16.5.1": - version "16.5.1" - resolved "https://registry.yarnpkg.com/@nx/devkit/-/devkit-16.5.1.tgz#1d6a27895a7c85edebe0ba31e0a394839ad5fdd2" - integrity sha512-T1acZrVVmJw/sJ4PIGidCBYBiBqlg/jT9e8nIGXLSDS20xcLvfo4zBQf8UZLrmHglnwwpDpOWuVJCp2rYA5aDg== - dependencies: - "@nrwl/devkit" "16.5.1" - ejs "^3.1.7" - ignore "^5.0.4" - semver "7.5.3" - tmp "~0.2.1" - tslib "^2.3.0" - -"@nx/nx-darwin-arm64@16.5.1": - version "16.5.1" - resolved "https://registry.yarnpkg.com/@nx/nx-darwin-arm64/-/nx-darwin-arm64-16.5.1.tgz#87111664de492e5ae270ef2adc74553e03d77341" - integrity sha512-q98TFI4B/9N9PmKUr1jcbtD4yAFs1HfYd9jUXXTQOlfO9SbDjnrYJgZ4Fp9rMNfrBhgIQ4x1qx0AukZccKmH9Q== - -"@nx/nx-darwin-x64@16.5.1": - version "16.5.1" - resolved "https://registry.yarnpkg.com/@nx/nx-darwin-x64/-/nx-darwin-x64-16.5.1.tgz#05c34ce8f8f23eeae0529d3c1022ee3e95a608a1" - integrity sha512-j9HmL1l8k7EVJ3eOM5y8COF93gqrydpxCDoz23ZEtsY+JHY77VAiRQsmqBgEx9GGA2dXi9VEdS67B0+1vKariw== - -"@nx/nx-freebsd-x64@16.5.1": - version "16.5.1" - resolved "https://registry.yarnpkg.com/@nx/nx-freebsd-x64/-/nx-freebsd-x64-16.5.1.tgz#b4303ac5066f5c8ced7768097d6c85e8055c7d3a" - integrity sha512-CXSPT01aVS869tvCCF2tZ7LnCa8l41wJ3mTVtWBkjmRde68E5Up093hklRMyXb3kfiDYlfIKWGwrV4r0eH6x1A== - -"@nx/nx-linux-arm-gnueabihf@16.5.1": - version "16.5.1" - resolved "https://registry.yarnpkg.com/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-16.5.1.tgz#4dde9e8c79da9c5a213b6938dff74f65dd79c157" - integrity sha512-BhrumqJSZCWFfLFUKl4CAUwR0Y0G2H5EfFVGKivVecEQbb+INAek1aa6c89evg2/OvetQYsJ+51QknskwqvLsA== - -"@nx/nx-linux-arm64-gnu@16.5.1": - version "16.5.1" - resolved "https://registry.yarnpkg.com/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-16.5.1.tgz#43dcdbd9b39fa91923ab949d161aa25c650f56d9" - integrity sha512-x7MsSG0W+X43WVv7JhiSq2eKvH2suNKdlUHEG09Yt0vm3z0bhtym1UCMUg3IUAK7jy9hhLeDaFVFkC6zo+H/XQ== - -"@nx/nx-linux-arm64-musl@16.5.1": - version "16.5.1" - resolved "https://registry.yarnpkg.com/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-16.5.1.tgz#fc33960cecb0064c3dd3330f393e3a38be8a71b7" - integrity sha512-J+/v/mFjOm74I0PNtH5Ka+fDd+/dWbKhpcZ2R1/6b9agzZk+Ff/SrwJcSYFXXWKbPX+uQ4RcJoytT06Zs3s0ow== - -"@nx/nx-linux-x64-gnu@16.5.1": - version "16.5.1" - resolved "https://registry.yarnpkg.com/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-16.5.1.tgz#2b2ffbb80e29455b6900ec20d4249055590dc58f" - integrity sha512-igooWJ5YxQ94Zft7IqgL+Lw0qHaY15Btw4gfK756g/YTYLZEt4tTvR1y6RnK/wdpE3sa68bFTLVBNCGTyiTiDQ== - -"@nx/nx-linux-x64-musl@16.5.1": - version "16.5.1" - resolved "https://registry.yarnpkg.com/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-16.5.1.tgz#955b2eae615ee6cf1954e24d42c205b1de8772bf" - integrity sha512-zF/exnPqFYbrLAduGhTmZ7zNEyADid2bzNQiIjJkh8Y6NpDwrQIwVIyvIxqynsjMrIs51kBH+8TUjKjj2Jgf5A== - -"@nx/nx-win32-arm64-msvc@16.5.1": - version "16.5.1" - resolved "https://registry.yarnpkg.com/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-16.5.1.tgz#1dc4a7e3662eb757214c46d8db432f61e43a3dd9" - integrity sha512-qtqiLS9Y9TYyAbbpq58kRoOroko4ZXg5oWVqIWFHoxc5bGPweQSJCROEqd1AOl2ZDC6BxfuVHfhDDop1kK05WA== - -"@nx/nx-win32-x64-msvc@16.5.1": - version "16.5.1" - resolved "https://registry.yarnpkg.com/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-16.5.1.tgz#d2f4a1b2bf675bceb6fb16174b836438293f9dca" - integrity sha512-kUJBLakK7iyA9WfsGGQBVennA4jwf5XIgm0lu35oMOphtZIluvzItMt0EYBmylEROpmpEIhHq0P6J9FA+WH0Rg== - -"@parcel/watcher@2.0.4": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@parcel/watcher/-/watcher-2.0.4.tgz#f300fef4cc38008ff4b8c29d92588eced3ce014b" - integrity sha512-cTDi+FUDBIUOBKEtj+nhiJ71AZVlkAsQFuGQTun5tV9mwQBQgZvhCzG+URPQc8myeN32yRVZEfVAPCs1RW+Jvg== - dependencies: - node-addon-api "^3.2.1" - node-gyp-build "^4.3.0" - -"@pkgjs/parseargs@^0.11.0": - version "0.11.0" - resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" - integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== - -"@schematics/angular@16.2.16": - version "16.2.16" - resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-16.2.16.tgz#3f077f398fc7ff88654fd477790af8270585c3af" - integrity sha512-V4cE4R5MbusKaNW9DWsisiSRUoQzbAaBIeJh42yCkg5H/lUdf18hUB7DG6Pl7yH6/tjzzz4SqIVD7N64uCDC2A== - dependencies: - "@angular-devkit/core" "16.2.16" - "@angular-devkit/schematics" "16.2.16" - jsonc-parser "3.2.0" - -"@sigstore/bundle@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@sigstore/bundle/-/bundle-1.1.0.tgz#17f8d813b09348b16eeed66a8cf1c3d6bd3d04f1" - integrity sha512-PFutXEy0SmQxYI4texPw3dd2KewuNqv7OuK1ZFtY2fM754yhvG2KdgwIhRnoEE2uHdtdGNQ8s0lb94dW9sELog== - dependencies: - "@sigstore/protobuf-specs" "^0.2.0" - -"@sigstore/protobuf-specs@^0.2.0": - version "0.2.1" - resolved "https://registry.yarnpkg.com/@sigstore/protobuf-specs/-/protobuf-specs-0.2.1.tgz#be9ef4f3c38052c43bd399d3f792c97ff9e2277b" - integrity sha512-XTWVxnWJu+c1oCshMLwnKvz8ZQJJDVOlciMfgpJBQbThVjKTCG8dwyhgLngBD2KN0ap9F/gOV8rFDEx8uh7R2A== - -"@sigstore/sign@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@sigstore/sign/-/sign-1.0.0.tgz#6b08ebc2f6c92aa5acb07a49784cb6738796f7b4" - integrity sha512-INxFVNQteLtcfGmcoldzV6Je0sbbfh9I16DM4yJPw3j5+TFP8X6uIiA18mvpEa9yyeycAKgPmOA3X9hVdVTPUA== - dependencies: - "@sigstore/bundle" "^1.1.0" - "@sigstore/protobuf-specs" "^0.2.0" - make-fetch-happen "^11.0.1" - -"@sigstore/tuf@^1.0.3": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@sigstore/tuf/-/tuf-1.0.3.tgz#2a65986772ede996485728f027b0514c0b70b160" - integrity sha512-2bRovzs0nJZFlCN3rXirE4gwxCn97JNjMmwpecqlbgV9WcxX7WRuIrgzx/X7Ib7MYRbyUTpBYE0s2x6AmZXnlg== - dependencies: - "@sigstore/protobuf-specs" "^0.2.0" - tuf-js "^1.1.7" - -"@socket.io/component-emitter@~3.1.0": - version "3.1.2" - resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz#821f8442f4175d8f0467b9daf26e3a18e2d02af2" - integrity sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA== - -"@tootallnate/once@1": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" - integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== - -"@tootallnate/once@2": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" - integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== - -"@tufjs/canonical-json@1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@tufjs/canonical-json/-/canonical-json-1.0.0.tgz#eade9fd1f537993bc1f0949f3aea276ecc4fab31" - integrity sha512-QTnf++uxunWvG2z3UFNzAoQPHxnSXOwtaI3iJ+AohhV+5vONuArPjJE7aPXPVXfXJsqrVbZBu9b81AJoSd09IQ== - -"@tufjs/models@1.0.4": - version "1.0.4" - resolved "https://registry.yarnpkg.com/@tufjs/models/-/models-1.0.4.tgz#5a689630f6b9dbda338d4b208019336562f176ef" - integrity sha512-qaGV9ltJP0EO25YfFUPhxRVK0evXFIAGicsVXuRim4Ed9cjPxYhNnNJ49SFmbeLgtxpslIkX317IgpfcHPVj/A== - dependencies: - "@tufjs/canonical-json" "1.0.0" - minimatch "^9.0.0" - -"@types/body-parser@*": - version "1.19.5" - resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.5.tgz#04ce9a3b677dc8bd681a17da1ab9835dc9d3ede4" - integrity sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg== - dependencies: - "@types/connect" "*" - "@types/node" "*" - -"@types/bonjour@^3.5.9": - version "3.5.13" - resolved "https://registry.yarnpkg.com/@types/bonjour/-/bonjour-3.5.13.tgz#adf90ce1a105e81dd1f9c61fdc5afda1bfb92956" - integrity sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ== - dependencies: - "@types/node" "*" - -"@types/codemirror@^5.60.5": - version "5.60.15" - resolved "https://registry.yarnpkg.com/@types/codemirror/-/codemirror-5.60.15.tgz#0f82be6f4126d1e59cf4c4830e56dcd49d3c3e8a" - integrity sha512-dTOvwEQ+ouKJ/rE9LT1Ue2hmP6H1mZv5+CCnNWu2qtiOe2LQa9lCprEY20HxiDmV/Bxh+dXjywmy5aKvoGjULA== - dependencies: - "@types/tern" "*" - -"@types/connect-history-api-fallback@^1.3.5": - version "1.5.4" - resolved "https://registry.yarnpkg.com/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz#7de71645a103056b48ac3ce07b3520b819c1d5b3" - integrity sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw== - dependencies: - "@types/express-serve-static-core" "*" - "@types/node" "*" - -"@types/connect@*": - version "3.4.38" - resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858" - integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug== - dependencies: - "@types/node" "*" - -"@types/cookie@^0.4.1": - version "0.4.1" - resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.4.1.tgz#bfd02c1f2224567676c1545199f87c3a861d878d" - integrity sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q== - -"@types/cors@^2.8.12": - version "2.8.17" - resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.17.tgz#5d718a5e494a8166f569d986794e49c48b216b2b" - integrity sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA== - dependencies: - "@types/node" "*" - -"@types/estree@*", "@types/estree@^1.0.5": - version "1.0.6" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50" - integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw== - -"@types/express-serve-static-core@*", "@types/express-serve-static-core@^5.0.0": - version "5.0.0" - resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-5.0.0.tgz#91f06cda1049e8f17eeab364798ed79c97488a1c" - integrity sha512-AbXMTZGt40T+KON9/Fdxx0B2WK5hsgxcfXJLr5bFpZ7b4JCex2WyQPTEKdXqfHiY5nKKBScZ7yCoO6Pvgxfvnw== - dependencies: - "@types/node" "*" - "@types/qs" "*" - "@types/range-parser" "*" - "@types/send" "*" - -"@types/express-serve-static-core@^4.17.33": - version "4.19.6" - resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz#e01324c2a024ff367d92c66f48553ced0ab50267" - integrity sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A== - dependencies: - "@types/node" "*" - "@types/qs" "*" - "@types/range-parser" "*" - "@types/send" "*" - -"@types/express@*": - version "5.0.0" - resolved "https://registry.yarnpkg.com/@types/express/-/express-5.0.0.tgz#13a7d1f75295e90d19ed6e74cab3678488eaa96c" - integrity sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ== - dependencies: - "@types/body-parser" "*" - "@types/express-serve-static-core" "^5.0.0" - "@types/qs" "*" - "@types/serve-static" "*" - -"@types/express@^4.17.13": - version "4.17.21" - resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.21.tgz#c26d4a151e60efe0084b23dc3369ebc631ed192d" - integrity sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ== - dependencies: - "@types/body-parser" "*" - "@types/express-serve-static-core" "^4.17.33" - "@types/qs" "*" - "@types/serve-static" "*" - -"@types/file-saver@^2.0.7": - version "2.0.7" - resolved "https://registry.yarnpkg.com/@types/file-saver/-/file-saver-2.0.7.tgz#8dbb2f24bdc7486c54aa854eb414940bbd056f7d" - integrity sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A== - -"@types/google-protobuf@^3.15.3": - version "3.15.12" - resolved "https://registry.yarnpkg.com/@types/google-protobuf/-/google-protobuf-3.15.12.tgz#eb2ba0eddd65712211a2b455dc6071d665ccf49b" - integrity sha512-40um9QqwHjRS92qnOaDpL7RmDK15NuZYo9HihiJRbYkMQZlWnuH8AdvbMy8/o6lgLmKbDUKa+OALCltHdbOTpQ== - -"@types/http-errors@*": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.4.tgz#7eb47726c391b7345a6ec35ad7f4de469cf5ba4f" - integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA== - -"@types/http-proxy@^1.17.8": - version "1.17.15" - resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.15.tgz#12118141ce9775a6499ecb4c01d02f90fc839d36" - integrity sha512-25g5atgiVNTIv0LBDTg1H74Hvayx0ajtJPLLcYE3whFv75J0pWNtOBzaXJQgDTmrX1bx5U9YC2w/n65BN1HwRQ== - dependencies: - "@types/node" "*" - -"@types/jasmine@*": - version "5.1.4" - resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-5.1.4.tgz#0de3f6ca753e10d1600ce1864ae42cfd47cf9924" - integrity sha512-px7OMFO/ncXxixDe1zR13V1iycqWae0MxTaw62RpFlksUi5QuNWgQJFkTQjIOvrmutJbI7Fp2Y2N1F6D2R4G6w== - -"@types/jasmine@~5.1.4": - version "5.1.7" - resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-5.1.7.tgz#b9f68cf05463717bbe36d53221839e7630a24d22" - integrity sha512-DVOfk9FaClQfNFpSfaML15jjB5cjffDMvjtph525sroR5BEAW2uKnTOYUTqTFuZFjNvH0T5XMIydvIctnUKufw== - -"@types/jasminewd2@~2.0.13": - version "2.0.13" - resolved "https://registry.yarnpkg.com/@types/jasminewd2/-/jasminewd2-2.0.13.tgz#0b60c1fcd06277ea97efbbad5a02e0c1a4a8996a" - integrity sha512-aJ3wj8tXMpBrzQ5ghIaqMisD8C3FIrcO6sDKHqFbuqAsI7yOxj0fA7MrRCPLZHIVUjERIwsMmGn/vB0UQ9u0Hg== - dependencies: - "@types/jasmine" "*" - -"@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": - version "7.0.15" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" - integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== - -"@types/jsonwebtoken@^9.0.6": - version "9.0.9" - resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-9.0.9.tgz#a4c3a446c0ebaaf467a58398382616f416345fb3" - integrity sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ== - dependencies: - "@types/ms" "*" - "@types/node" "*" - -"@types/mime@^1": - version "1.3.5" - resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690" - integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== - -"@types/ms@*": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@types/ms/-/ms-2.1.0.tgz#052aa67a48eccc4309d7f0191b7e41434b90bb78" - integrity sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA== - -"@types/node-forge@^1.3.0": - version "1.3.11" - resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-1.3.11.tgz#0972ea538ddb0f4d9c2fa0ec5db5724773a604da" - integrity sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ== - dependencies: - "@types/node" "*" - -"@types/node@*", "@types/node@^22.5.5": - version "22.13.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-22.13.13.tgz#5e7d110fb509b0d4a43fbf48fa9d6e0f83e1b1e7" - integrity sha512-ClsL5nMwKaBRwPcCvH8E7+nU4GxHVx1axNvMZTFHMEfNI7oahimt26P5zjVCRrjiIWj6YFXfE1v3dEp94wLcGQ== - dependencies: - undici-types "~6.20.0" - -"@types/node@>=10.0.0": - version "22.7.4" - resolved "https://registry.yarnpkg.com/@types/node/-/node-22.7.4.tgz#e35d6f48dca3255ce44256ddc05dee1c23353fcc" - integrity sha512-y+NPi1rFzDs1NdQHHToqeiX2TIS79SWEAw9GYhkkx8bD0ChpfqC+n2j5OXOCpzfojBEBt6DnEnnG9MY0zk1XLg== - dependencies: - undici-types "~6.19.2" - -"@types/normalize-package-data@^2.4.3": - version "2.4.4" - resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz#56e2cc26c397c038fab0e3a917a12d5c5909e901" - integrity sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA== - -"@types/opentype.js@^1.3.8": - version "1.3.8" - resolved "https://registry.yarnpkg.com/@types/opentype.js/-/opentype.js-1.3.8.tgz#741be92429d1c2d64b5fa79cf692f74b49d6007f" - integrity sha512-H6qeTp03jrknklSn4bpT1/9+1xCAEIU2CnjcWPkicJy8A1SKuthanbvoHYMiv79/2W3Xn1XE4gfSJFzt2U3JSw== - -"@types/qrcode@^1.5.2": - version "1.5.5" - resolved "https://registry.yarnpkg.com/@types/qrcode/-/qrcode-1.5.5.tgz#993ff7c6b584277eee7aac0a20861eab682f9dac" - integrity sha512-CdfBi/e3Qk+3Z/fXYShipBT13OJ2fDO2Q2w5CIP5anLTLIndQG9z6P1cnm+8zCWSpm5dnxMFd/uREtb0EXuQzg== - dependencies: - "@types/node" "*" - -"@types/qs@*": - version "6.9.16" - resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.16.tgz#52bba125a07c0482d26747d5d4947a64daf8f794" - integrity sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A== - -"@types/range-parser@*": - version "1.2.7" - resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb" - integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== - -"@types/retry@0.12.0": - version "0.12.0" - resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" - integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== - -"@types/semver@^7.3.12": - version "7.5.8" - resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.8.tgz#8268a8c57a3e4abd25c165ecd36237db7948a55e" - integrity sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ== - -"@types/send@*": - version "0.17.4" - resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.4.tgz#6619cd24e7270793702e4e6a4b958a9010cfc57a" - integrity sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA== - dependencies: - "@types/mime" "^1" - "@types/node" "*" - -"@types/serve-index@^1.9.1": - version "1.9.4" - resolved "https://registry.yarnpkg.com/@types/serve-index/-/serve-index-1.9.4.tgz#e6ae13d5053cb06ed36392110b4f9a49ac4ec898" - integrity sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug== - dependencies: - "@types/express" "*" - -"@types/serve-static@*", "@types/serve-static@^1.13.10": - version "1.15.7" - resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.7.tgz#22174bbd74fb97fe303109738e9b5c2f3064f714" - integrity sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw== - dependencies: - "@types/http-errors" "*" - "@types/node" "*" - "@types/send" "*" - -"@types/sockjs@^0.3.33": - version "0.3.36" - resolved "https://registry.yarnpkg.com/@types/sockjs/-/sockjs-0.3.36.tgz#ce322cf07bcc119d4cbf7f88954f3a3bd0f67535" - integrity sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q== - dependencies: - "@types/node" "*" - -"@types/tern@*": - version "0.23.9" - resolved "https://registry.yarnpkg.com/@types/tern/-/tern-0.23.9.tgz#6f6093a4a9af3e6bb8dde528e024924d196b367c" - integrity sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw== - dependencies: - "@types/estree" "*" - -"@types/uuid@^10.0.0": - version "10.0.0" - resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-10.0.0.tgz#e9c07fe50da0f53dc24970cca94d619ff03f6f6d" - integrity sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ== - -"@types/ws@^8.5.5": - version "8.5.12" - resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.12.tgz#619475fe98f35ccca2a2f6c137702d85ec247b7e" - integrity sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ== - dependencies: - "@types/node" "*" - -"@typescript-eslint/eslint-plugin@^5.62.0": - version "5.62.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz#aeef0328d172b9e37d9bab6dbc13b87ed88977db" - integrity sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag== - 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" - graphemer "^1.4.0" - ignore "^5.2.0" - natural-compare-lite "^1.4.0" - semver "^7.3.7" - tsutils "^3.21.0" - -"@typescript-eslint/parser@^5.62.0": - version "5.62.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.62.0.tgz#1b63d082d849a2fcae8a569248fbe2ee1b8a56c7" - integrity sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA== - dependencies: - "@typescript-eslint/scope-manager" "5.62.0" - "@typescript-eslint/types" "5.62.0" - "@typescript-eslint/typescript-estree" "5.62.0" - debug "^4.3.4" - -"@typescript-eslint/scope-manager@5.62.0": - version "5.62.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz#d9457ccc6a0b8d6b37d0eb252a23022478c5460c" - integrity sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w== - dependencies: - "@typescript-eslint/types" "5.62.0" - "@typescript-eslint/visitor-keys" "5.62.0" - -"@typescript-eslint/scope-manager@8.0.0-alpha.20": - version "8.0.0-alpha.20" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.0.0-alpha.20.tgz#2f953a8f62e87d65b7a5d19800f7c996e0fe8b11" - integrity sha512-+Ncj0Q6DT8ZHYNp8h5RndW4Siv52kiPpHEz/i8Sj2rh2y8ZCc5pKSHSslk+eZi0Bdj+/+swNOmDNcL2CrlaEnA== - dependencies: - "@typescript-eslint/types" "8.0.0-alpha.20" - "@typescript-eslint/visitor-keys" "8.0.0-alpha.20" - -"@typescript-eslint/type-utils@5.62.0": - version "5.62.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz#286f0389c41681376cdad96b309cedd17d70346a" - integrity sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew== - dependencies: - "@typescript-eslint/typescript-estree" "5.62.0" - "@typescript-eslint/utils" "5.62.0" - debug "^4.3.4" - tsutils "^3.21.0" - -"@typescript-eslint/types@5.62.0": - version "5.62.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.62.0.tgz#258607e60effa309f067608931c3df6fed41fd2f" - integrity sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ== - -"@typescript-eslint/types@8.0.0-alpha.20": - version "8.0.0-alpha.20" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.0.0-alpha.20.tgz#f6d6ed7789178934fcdc67a0796191580f505730" - integrity sha512-xpU1rMQfnnNZxpHN6YUfr18sGOMcpC9hvt54fupcU6N1qMCagEtkRt1U15x086oJAgAITJGa67454ffAoCxv/w== - -"@typescript-eslint/typescript-estree@5.62.0": - version "5.62.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz#7d17794b77fabcac615d6a48fb143330d962eb9b" - integrity sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA== - dependencies: - "@typescript-eslint/types" "5.62.0" - "@typescript-eslint/visitor-keys" "5.62.0" - debug "^4.3.4" - globby "^11.1.0" - is-glob "^4.0.3" - semver "^7.3.7" - tsutils "^3.21.0" - -"@typescript-eslint/typescript-estree@8.0.0-alpha.20": - version "8.0.0-alpha.20" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.0.0-alpha.20.tgz#f495288215150f64af97896f2c1a8cf44197d09c" - integrity sha512-VQ8Mf8upDCuf0uMTjX/Pdw3gvCZomkG43nuThUuzhK3YFwFmIDTqx0ZWSsYJkVGfll0WrXgIua+rKSP/n6NBWw== - dependencies: - "@typescript-eslint/types" "8.0.0-alpha.20" - "@typescript-eslint/visitor-keys" "8.0.0-alpha.20" - debug "^4.3.4" - globby "^11.1.0" - is-glob "^4.0.3" - minimatch "^9.0.4" - semver "^7.6.0" - ts-api-utils "^1.3.0" - -"@typescript-eslint/utils@5.62.0": - version "5.62.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.62.0.tgz#141e809c71636e4a75daa39faed2fb5f4b10df86" - integrity sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ== - 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" - -"@typescript-eslint/utils@8.0.0-alpha.20": - version "8.0.0-alpha.20" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.0.0-alpha.20.tgz#f8e7b6d282714e9e34e891eab2daf8d9b76db5a3" - integrity sha512-0aMhjDTvIrkGkHqyM0ZByAwR8BV1f2HhKdYyjtxko8S/Ca4PGjOIjub6VoF+bQwCRxEuV8viNUld78rqm9jqLA== - dependencies: - "@eslint-community/eslint-utils" "^4.4.0" - "@typescript-eslint/scope-manager" "8.0.0-alpha.20" - "@typescript-eslint/types" "8.0.0-alpha.20" - "@typescript-eslint/typescript-estree" "8.0.0-alpha.20" - -"@typescript-eslint/visitor-keys@5.62.0": - version "5.62.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz#2174011917ce582875954ffe2f6912d5931e353e" - integrity sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw== - dependencies: - "@typescript-eslint/types" "5.62.0" - eslint-visitor-keys "^3.3.0" - -"@typescript-eslint/visitor-keys@8.0.0-alpha.20": - version "8.0.0-alpha.20" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.0.0-alpha.20.tgz#bffce2fa485fd99b071a4a51fec8ed6ad7a8d1a3" - integrity sha512-ej06rfct0kalfJgIR8nTR7dF1mgfF83hkylrYas7IAElHfgw4zx99BUGa6VrnHZ1PkxdJBp5PgcO2FmmlOoaRQ== - dependencies: - "@typescript-eslint/types" "8.0.0-alpha.20" - eslint-visitor-keys "^3.4.3" - -"@ungap/structured-clone@^1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" - integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== - -"@vitejs/plugin-basic-ssl@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-1.0.1.tgz#48c46eab21e0730921986ce742563ae83fe7fe34" - integrity sha512-pcub+YbFtFhaGRTo1832FQHQSHvMrlb43974e2eS8EKleR3p1cDdkJFPci1UhwkEf1J9Bz+wKBSzqpKp7nNj2A== - -"@webassemblyjs/ast@1.12.1", "@webassemblyjs/ast@^1.12.1": - version "1.12.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.12.1.tgz#bb16a0e8b1914f979f45864c23819cc3e3f0d4bb" - integrity sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg== - dependencies: - "@webassemblyjs/helper-numbers" "1.11.6" - "@webassemblyjs/helper-wasm-bytecode" "1.11.6" - -"@webassemblyjs/floating-point-hex-parser@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz#dacbcb95aff135c8260f77fa3b4c5fea600a6431" - integrity sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw== - -"@webassemblyjs/helper-api-error@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz#6132f68c4acd59dcd141c44b18cbebbd9f2fa768" - integrity sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q== - -"@webassemblyjs/helper-buffer@1.12.1": - version "1.12.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz#6df20d272ea5439bf20ab3492b7fb70e9bfcb3f6" - integrity sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw== - -"@webassemblyjs/helper-numbers@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz#cbce5e7e0c1bd32cf4905ae444ef64cea919f1b5" - integrity sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g== - dependencies: - "@webassemblyjs/floating-point-hex-parser" "1.11.6" - "@webassemblyjs/helper-api-error" "1.11.6" - "@xtuc/long" "4.2.2" - -"@webassemblyjs/helper-wasm-bytecode@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz#bb2ebdb3b83aa26d9baad4c46d4315283acd51e9" - integrity sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA== - -"@webassemblyjs/helper-wasm-section@1.12.1": - version "1.12.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz#3da623233ae1a60409b509a52ade9bc22a37f7bf" - integrity sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g== - 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" - -"@webassemblyjs/ieee754@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz#bb665c91d0b14fffceb0e38298c329af043c6e3a" - integrity sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg== - dependencies: - "@xtuc/ieee754" "^1.2.0" - -"@webassemblyjs/leb128@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.6.tgz#70e60e5e82f9ac81118bc25381a0b283893240d7" - integrity sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ== - dependencies: - "@xtuc/long" "4.2.2" - -"@webassemblyjs/utf8@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.6.tgz#90f8bc34c561595fe156603be7253cdbcd0fab5a" - integrity sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA== - -"@webassemblyjs/wasm-edit@^1.12.1": - version "1.12.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz#9f9f3ff52a14c980939be0ef9d5df9ebc678ae3b" - integrity sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g== - 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" - -"@webassemblyjs/wasm-gen@1.12.1": - version "1.12.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz#a6520601da1b5700448273666a71ad0a45d78547" - integrity sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w== - 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" - -"@webassemblyjs/wasm-opt@1.12.1": - version "1.12.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz#9e6e81475dfcfb62dab574ac2dda38226c232bc5" - integrity sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg== - dependencies: - "@webassemblyjs/ast" "1.12.1" - "@webassemblyjs/helper-buffer" "1.12.1" - "@webassemblyjs/wasm-gen" "1.12.1" - "@webassemblyjs/wasm-parser" "1.12.1" - -"@webassemblyjs/wasm-parser@1.12.1", "@webassemblyjs/wasm-parser@^1.12.1": - version "1.12.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz#c47acb90e6f083391e3fa61d113650eea1e95937" - integrity sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ== - 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" - -"@webassemblyjs/wast-printer@1.12.1": - version "1.12.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz#bcecf661d7d1abdaf989d8341a4833e33e2b31ac" - integrity sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA== - dependencies: - "@webassemblyjs/ast" "1.12.1" - "@xtuc/long" "4.2.2" - -"@wessberg/ts-evaluator@0.0.27": - version "0.0.27" - resolved "https://registry.yarnpkg.com/@wessberg/ts-evaluator/-/ts-evaluator-0.0.27.tgz#06e8b901d5e84f11199b9f84577c6426ae761767" - integrity sha512-7gOpVm3yYojUp/Yn7F4ZybJRxyqfMNf0LXK5KJiawbPfL0XTsJV+0mgrEDjOIR6Bi0OYk2Cyg4tjFu1r8MCZaA== - dependencies: - chalk "^4.1.0" - jsdom "^16.4.0" - object-path "^0.11.5" - tslib "^2.0.3" - -"@xtuc/ieee754@^1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" - integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== - -"@xtuc/long@4.2.2": - version "4.2.2" - resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" - integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== - -"@yarnpkg/lockfile@1.1.0", "@yarnpkg/lockfile@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31" - integrity sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ== - -"@yarnpkg/parsers@3.0.0-rc.46": - version "3.0.0-rc.46" - resolved "https://registry.yarnpkg.com/@yarnpkg/parsers/-/parsers-3.0.0-rc.46.tgz#03f8363111efc0ea670e53b0282cd3ef62de4e01" - integrity sha512-aiATs7pSutzda/rq8fnuPwTglyVwjM22bNnK2ZgjrpAjQHSSl3lztd2f9evst1W/qnC58DRz7T7QndUDumAR4Q== - dependencies: - js-yaml "^3.10.0" - tslib "^2.4.0" - -"@zitadel/client@1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@zitadel/client/-/client-1.2.0.tgz#8cdc3090f75fcf3a78c4f0266d3c56a0cca6821a" - integrity sha512-Q20nXhKD7VDb8D1UxhDxubC70GFrSPckrJviPR/rAfRR5slUIRTk3AvDS6Q1WvUn4Xtt+btnq52Z5O8lZtVG0w== - dependencies: - "@bufbuild/protobuf" "^2.2.2" - "@connectrpc/connect" "^2.0.0" - "@connectrpc/connect-node" "^2.0.0" - "@connectrpc/connect-web" "^2.0.0" - "@zitadel/proto" "1.2.0" - jose "^5.3.0" - -"@zitadel/proto@1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@zitadel/proto/-/proto-1.2.0.tgz#9b9a40defcd9e8464627cc99ac3fd7bcf8994ffd" - integrity sha512-OqHgyCnD9l950xswdVNPIsLA01qSpOPf+0bYqYJWHafytIBbvGNJRnypu4X0LnaFXLM6LakkP4pWYeiGLmwxaw== - dependencies: - "@bufbuild/protobuf" "^2.2.2" - -"@zkochan/js-yaml@0.0.6": - version "0.0.6" - resolved "https://registry.yarnpkg.com/@zkochan/js-yaml/-/js-yaml-0.0.6.tgz#975f0b306e705e28b8068a07737fa46d3fc04826" - integrity sha512-nzvgl3VfhcELQ8LyVrYOru+UtAy1nrygk2+AGbTm8a5YcO6o8lSjAT+pfg3vJWxIoZKOUhrK6UU7xW/+00kQrg== - dependencies: - argparse "^2.0.1" - -abab@^2.0.3, abab@^2.0.5, abab@^2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291" - integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA== - -abbrev@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" - integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== - -accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8: - version "1.3.8" - resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" - integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== - dependencies: - mime-types "~2.1.34" - negotiator "0.6.3" - -acorn-globals@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-6.0.0.tgz#46cdd39f0f8ff08a876619b55f5ac8a6dc770b45" - integrity sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg== - dependencies: - acorn "^7.1.1" - acorn-walk "^7.1.1" - -acorn-import-attributes@^1.9.5: - version "1.9.5" - resolved "https://registry.yarnpkg.com/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz#7eb1557b1ba05ef18b5ed0ec67591bfab04688ef" - integrity sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ== - -acorn-jsx@^5.3.2: - version "5.3.2" - resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" - integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== - -acorn-walk@^7.1.1: - version "7.2.0" - resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc" - integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA== - -acorn@^7.1.1: - version "7.4.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" - integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== - -acorn@^8.2.4, acorn@^8.7.1, acorn@^8.8.2, acorn@^8.9.0: - version "8.12.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248" - integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg== - -adjust-sourcemap-loader@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz#fc4a0fd080f7d10471f30a7320f25560ade28c99" - integrity sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A== - dependencies: - loader-utils "^2.0.0" - regex-parser "^2.2.11" - -agent-base@6, agent-base@^6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" - integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== - dependencies: - debug "4" - -agentkeepalive@^4.2.1: - version "4.5.0" - resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.5.0.tgz#2673ad1389b3c418c5a20c5d7364f93ca04be923" - integrity sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew== - dependencies: - humanize-ms "^1.2.1" - -aggregate-error@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" - integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== - dependencies: - clean-stack "^2.0.0" - indent-string "^4.0.0" - -ajv-formats@2.1.1, ajv-formats@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" - integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA== - dependencies: - ajv "^8.0.0" - -ajv-keywords@^3.5.2: - version "3.5.2" - resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" - integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== - -ajv-keywords@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz#69d4d385a4733cdbeab44964a1170a88f87f0e16" - integrity sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw== - dependencies: - fast-deep-equal "^3.1.3" - -ajv@8.12.0: - version "8.12.0" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.12.0.tgz#d1a0527323e22f53562c567c00991577dfbe19d1" - integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== - dependencies: - fast-deep-equal "^3.1.1" - json-schema-traverse "^1.0.0" - require-from-string "^2.0.2" - uri-js "^4.2.2" - -ajv@^6.12.4, ajv@^6.12.5: - version "6.12.6" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" - integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== - dependencies: - fast-deep-equal "^3.1.1" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.4.1" - uri-js "^4.2.2" - -ajv@^8.0.0, ajv@^8.12.0, ajv@^8.9.0: - version "8.17.1" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6" - integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g== - dependencies: - fast-deep-equal "^3.1.3" - fast-uri "^3.0.1" - json-schema-traverse "^1.0.0" - require-from-string "^2.0.2" - -angular-oauth2-oidc@^15.0.1: - version "15.0.1" - resolved "https://registry.yarnpkg.com/angular-oauth2-oidc/-/angular-oauth2-oidc-15.0.1.tgz#ba3bcb88e565be7ea4c635a48524963c677be2fe" - integrity sha512-5gpqO9QL+qFqMItYFHe8F6H5nOIEaowcNUc9iTDs3P1bfVYnoKoVAaijob53PuPTF4YwzdfwKWZi4Mq6P7GENQ== - dependencies: - tslib "^2.0.0" - -angularx-qrcode@^16.0.2: - version "16.0.2" - resolved "https://registry.yarnpkg.com/angularx-qrcode/-/angularx-qrcode-16.0.2.tgz#86af924191546394cb93f9fb8d0c42edd0132894" - integrity sha512-FztOM7vjNu88sGxUU5jG2I+A9TxZBXXYBWINjpwIBbTL+COMgrtzXnScG7TyQeNknv5w3WFJWn59PcngRRYVXA== - dependencies: - qrcode "1.5.3" - tslib "^2.3.0" - -ansi-colors@4.1.3, ansi-colors@^4.1.1: - version "4.1.3" - resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" - integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw== - -ansi-escapes@^4.2.1: - version "4.3.2" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" - integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== - dependencies: - type-fest "^0.21.3" - -ansi-html-community@^0.0.8: - version "0.0.8" - resolved "https://registry.yarnpkg.com/ansi-html-community/-/ansi-html-community-0.0.8.tgz#69fbc4d6ccbe383f9736934ae34c3f8290f1bf41" - integrity sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw== - -ansi-regex@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" - integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== - -ansi-regex@^6.0.1: - version "6.1.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.1.0.tgz#95ec409c69619d6cb1b8b34f14b660ef28ebd654" - integrity sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA== - -ansi-styles@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" - integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== - dependencies: - color-convert "^1.9.0" - -ansi-styles@^4.0.0, ansi-styles@^4.1.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" - integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== - dependencies: - color-convert "^2.0.1" - -ansi-styles@^6.1.0: - version "6.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" - integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== - -anymatch@~3.1.2: - version "3.1.3" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" - integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== - dependencies: - normalize-path "^3.0.0" - picomatch "^2.0.4" - -"aproba@^1.0.3 || ^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc" - integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ== - -are-we-there-yet@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz#679df222b278c64f2cdba1175cdc00b0d96164bd" - integrity sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg== - dependencies: - delegates "^1.0.0" - readable-stream "^3.6.0" - -argparse@^1.0.7: - version "1.0.10" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" - integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== - dependencies: - sprintf-js "~1.0.2" - -argparse@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" - integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== - -aria-query@5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.0.tgz#650c569e41ad90b51b3d7df5e5eed1c7549c103e" - integrity sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A== - dependencies: - dequal "^2.0.3" - -array-flatten@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" - integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== - -array-union@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" - integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== - -async@^3.2.3: - version "3.2.6" - resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce" - integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA== - -asynckit@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" - integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== - -autoprefixer@10.4.14: - version "10.4.14" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.14.tgz#e28d49902f8e759dd25b153264e862df2705f79d" - integrity sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ== - dependencies: - browserslist "^4.21.5" - caniuse-lite "^1.0.30001464" - fraction.js "^4.2.0" - normalize-range "^0.1.2" - picocolors "^1.0.0" - postcss-value-parser "^4.2.0" - -axios@^1.0.0: - version "1.7.7" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.7.tgz#2f554296f9892a72ac8d8e4c5b79c14a91d0a47f" - integrity sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q== - dependencies: - follow-redirects "^1.15.6" - form-data "^4.0.0" - proxy-from-env "^1.1.0" - -axobject-query@3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-3.2.1.tgz#39c378a6e3b06ca679f29138151e45b2b32da62a" - integrity sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg== - dependencies: - dequal "^2.0.3" - -axobject-query@4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-4.0.0.tgz#04a4c90dce33cc5d606c76d6216e3b250ff70dab" - integrity sha512-+60uv1hiVFhHZeO+Lz0RYzsVHy5Wr1ayX0mwda9KPDVLNJgZ1T9Ny7VmFbLDzxsH0D87I86vgj3gFrjTJUYznw== - dependencies: - dequal "^2.0.3" - -babel-loader@9.1.3: - version "9.1.3" - resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-9.1.3.tgz#3d0e01b4e69760cc694ee306fe16d358aa1c6f9a" - integrity sha512-xG3ST4DglodGf8qSwv0MdeWLhrDsw/32QMdTO5T1ZIp9gQur0HkCyFs7Awskr10JKXFXwpAhiCuYX5oGXnRGbw== - dependencies: - find-cache-dir "^4.0.0" - schema-utils "^4.0.0" - -babel-plugin-istanbul@6.1.1: - version "6.1.1" - resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz#fa88ec59232fd9b4e36dbbc540a8ec9a9b47da73" - integrity sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA== - 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" - -babel-plugin-polyfill-corejs2@^0.4.4: - version "0.4.11" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.11.tgz#30320dfe3ffe1a336c15afdcdafd6fd615b25e33" - integrity sha512-sMEJ27L0gRHShOh5G54uAAPaiCOygY/5ratXuiyb2G46FmlSpc9eFCzYVyDiPxfNbwzA7mYahmjQc5q+CZQ09Q== - dependencies: - "@babel/compat-data" "^7.22.6" - "@babel/helper-define-polyfill-provider" "^0.6.2" - semver "^6.3.1" - -babel-plugin-polyfill-corejs3@^0.8.2: - version "0.8.7" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.7.tgz#941855aa7fdaac06ed24c730a93450d2b2b76d04" - integrity sha512-KyDvZYxAzkC0Aj2dAPyDzi2Ym15e5JKZSK+maI7NAwSqofvuFglbSsxE7wUOvTg9oFVnHMzVzBKcqEb4PJgtOA== - dependencies: - "@babel/helper-define-polyfill-provider" "^0.4.4" - core-js-compat "^3.33.1" - -babel-plugin-polyfill-regenerator@^0.5.1: - version "0.5.5" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.5.tgz#8b0c8fc6434239e5d7b8a9d1f832bb2b0310f06a" - integrity sha512-OJGYZlhLqBh2DDHeqAxWB1XIvr49CxiJ2gIt61/PU55CQK4Z58OzMqjDe1zwQdQk+rBYsRc+1rJmdajM3gimHg== - dependencies: - "@babel/helper-define-polyfill-provider" "^0.5.0" - -balanced-match@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" - integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== - -base64-js@^1.2.0, base64-js@^1.3.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" - integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== - -base64id@2.0.0, base64id@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/base64id/-/base64id-2.0.0.tgz#2770ac6bc47d312af97a8bf9a634342e0cd25cb6" - integrity sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog== - -batch@0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" - integrity sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw== - -big.js@^5.2.2: - version "5.2.2" - resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" - integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== - -binary-extensions@^2.0.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" - integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== - -bl@^4.0.3, bl@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" - integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== - dependencies: - buffer "^5.5.0" - inherits "^2.0.4" - readable-stream "^3.4.0" - -body-parser@1.20.3, body-parser@^1.19.0: - version "1.20.3" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6" - integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g== - 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" - -bonjour-service@^1.0.11: - version "1.2.1" - resolved "https://registry.yarnpkg.com/bonjour-service/-/bonjour-service-1.2.1.tgz#eb41b3085183df3321da1264719fbada12478d02" - integrity sha512-oSzCS2zV14bh2kji6vNe7vrpJYCHGvcZnlffFQ1MEoX/WOeQ/teD8SYWKR942OI3INjq8OMNJlbPK5LLLUxFDw== - dependencies: - fast-deep-equal "^3.1.3" - multicast-dns "^7.2.5" - -boolbase@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" - integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== - -brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -brace-expansion@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" - integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== - dependencies: - balanced-match "^1.0.0" - -braces@^3.0.2, braces@^3.0.3, braces@~3.0.2: - version "3.0.3" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" - integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== - dependencies: - fill-range "^7.1.1" - -browser-process-hrtime@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626" - integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow== - -browserslist@^4.21.10, browserslist@^4.21.5, browserslist@^4.23.3, browserslist@^4.24.0: - version "4.24.0" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.0.tgz#a1325fe4bc80b64fda169629fc01b3d6cecd38d4" - integrity sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A== - dependencies: - caniuse-lite "^1.0.30001663" - electron-to-chromium "^1.5.28" - node-releases "^2.0.18" - update-browserslist-db "^1.1.0" - -buffer-from@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" - integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== - -buffer@^5.5.0: - version "5.7.1" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" - integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== - dependencies: - base64-js "^1.3.1" - ieee754 "^1.1.13" - -buffer@^6.0.3: - version "6.0.3" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" - integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== - dependencies: - base64-js "^1.3.1" - ieee754 "^1.2.1" - -bytes@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" - integrity sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw== - -bytes@3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" - integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== - -cacache@^16.1.0: - version "16.1.3" - resolved "https://registry.yarnpkg.com/cacache/-/cacache-16.1.3.tgz#a02b9f34ecfaf9a78c9f4bc16fceb94d5d67a38e" - integrity sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ== - dependencies: - "@npmcli/fs" "^2.1.0" - "@npmcli/move-file" "^2.0.0" - chownr "^2.0.0" - fs-minipass "^2.1.0" - glob "^8.0.1" - infer-owner "^1.0.4" - lru-cache "^7.7.1" - minipass "^3.1.6" - minipass-collect "^1.0.2" - minipass-flush "^1.0.5" - minipass-pipeline "^1.2.4" - mkdirp "^1.0.4" - p-map "^4.0.0" - promise-inflight "^1.0.1" - rimraf "^3.0.2" - ssri "^9.0.0" - tar "^6.1.11" - unique-filename "^2.0.0" - -cacache@^17.0.0: - version "17.1.4" - resolved "https://registry.yarnpkg.com/cacache/-/cacache-17.1.4.tgz#b3ff381580b47e85c6e64f801101508e26604b35" - integrity sha512-/aJwG2l3ZMJ1xNAnqbMpA40of9dj/pIH3QfiuQSqjfPJF747VR0J/bHn+/KdNnHKc6XQcWt/AfRSBft82W1d2A== - dependencies: - "@npmcli/fs" "^3.1.0" - fs-minipass "^3.0.0" - glob "^10.2.2" - lru-cache "^7.7.1" - minipass "^7.0.3" - minipass-collect "^1.0.2" - minipass-flush "^1.0.5" - minipass-pipeline "^1.2.4" - p-map "^4.0.0" - ssri "^10.0.0" - tar "^6.1.11" - unique-filename "^3.0.0" - -call-bind@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" - integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== - dependencies: - 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" - -callsites@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" - integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== - -camelcase@^5.0.0, camelcase@^5.3.1: - version "5.3.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" - integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== - -caniuse-lite@^1.0.30001464, caniuse-lite@^1.0.30001663: - version "1.0.30001666" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001666.tgz#112d77e80f1762f62a1b71ba92164e0cb3f3dd13" - integrity sha512-gD14ICmoV5ZZM1OdzPWmpx+q4GyefaK06zi8hmfHV5xe4/2nOQX3+Dw5o+fSqOws2xVwL9j+anOPFwHzdEdV4g== - -chalk@^2.4.2: - version "2.4.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" - 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" - -chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.1: - version "4.1.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" - integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - -chardet@^0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" - integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== - -chokidar@3.5.3: - version "3.5.3" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" - integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== - dependencies: - anymatch "~3.1.2" - braces "~3.0.2" - glob-parent "~5.1.2" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.6.0" - optionalDependencies: - fsevents "~2.3.2" - -"chokidar@>=3.0.0 <4.0.0", chokidar@^3.0.0, chokidar@^3.5.1, chokidar@^3.5.3: - version "3.6.0" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" - integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== - dependencies: - anymatch "~3.1.2" - braces "~3.0.2" - glob-parent "~5.1.2" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.6.0" - optionalDependencies: - fsevents "~2.3.2" - -chownr@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" - integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== - -chrome-trace-event@^1.0.2: - version "1.0.4" - resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz#05bffd7ff928465093314708c93bdfa9bd1f0f5b" - integrity sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ== - -clean-stack@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" - integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== - -cli-cursor@3.1.0, cli-cursor@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" - integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== - dependencies: - restore-cursor "^3.1.0" - -cli-spinners@2.6.1: - version "2.6.1" - resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.6.1.tgz#adc954ebe281c37a6319bfa401e6dd2488ffb70d" - integrity sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g== - -cli-spinners@^2.5.0: - version "2.9.2" - resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.9.2.tgz#1773a8f4b9c4d6ac31563df53b3fc1d79462fe41" - integrity sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg== - -cli-width@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6" - integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw== - -cliui@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1" - integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ== - dependencies: - string-width "^4.2.0" - strip-ansi "^6.0.0" - wrap-ansi "^6.2.0" - -cliui@^7.0.2: - version "7.0.4" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" - integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== - dependencies: - string-width "^4.2.0" - strip-ansi "^6.0.0" - wrap-ansi "^7.0.0" - -cliui@^8.0.1: - version "8.0.1" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" - 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" - -clone-deep@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" - integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ== - dependencies: - is-plain-object "^2.0.4" - kind-of "^6.0.2" - shallow-clone "^3.0.0" - -clone@^1.0.2: - version "1.0.4" - resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" - integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg== - -codemirror@^5.65.19: - version "5.65.19" - resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.65.19.tgz#71016c701d6a4b6e1982b0f6e7186be65e49653d" - integrity sha512-+aFkvqhaAVr1gferNMuN8vkTSrWIFvzlMV9I2KBLCWS2WpZ2+UAkZjlMZmEuT+gcXTi6RrGQCkWq1/bDtGqhIA== - -color-convert@^1.9.0: - version "1.9.3" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" - integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== - dependencies: - color-name "1.1.3" - -color-convert@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" - integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== - dependencies: - color-name "~1.1.4" - -color-name@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" - integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== - -color-name@~1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" - integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== - -color-support@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" - integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== - -colorette@^2.0.10: - version "2.0.20" - resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" - integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== - -colors@1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" - integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== - -combined-stream@^1.0.8: - version "1.0.8" - resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" - integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== - dependencies: - delayed-stream "~1.0.0" - -commander@^2.20.0: - version "2.20.3" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" - integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== - -common-path-prefix@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/common-path-prefix/-/common-path-prefix-3.0.0.tgz#7d007a7e07c58c4b4d5f433131a19141b29f11e0" - integrity sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w== - -compressible@~2.0.16: - version "2.0.18" - resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" - integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== - dependencies: - mime-db ">= 1.43.0 < 2" - -compression@^1.7.4: - version "1.7.4" - resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f" - integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ== - 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" - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== - -connect-history-api-fallback@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz#647264845251a0daf25b97ce87834cace0f5f1c8" - integrity sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA== - -connect@^3.7.0: - version "3.7.0" - resolved "https://registry.yarnpkg.com/connect/-/connect-3.7.0.tgz#5d49348910caa5e07a01800b030d0c35f20484f8" - integrity sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ== - dependencies: - debug "2.6.9" - finalhandler "1.1.2" - parseurl "~1.3.3" - utils-merge "1.0.1" - -console-control-strings@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" - integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ== - -content-disposition@0.5.4: - version "0.5.4" - resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" - integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== - dependencies: - safe-buffer "5.2.1" - -content-type@~1.0.4, content-type@~1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" - integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== - -convert-source-map@^1.5.1, convert-source-map@^1.7.0: - version "1.9.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" - integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A== - -convert-source-map@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" - integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== - -cookie-signature@1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" - integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== - -cookie@0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051" - integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw== - -cookie@~0.4.1: - version "0.4.2" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" - integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== - -copy-anything@^2.0.1: - version "2.0.6" - resolved "https://registry.yarnpkg.com/copy-anything/-/copy-anything-2.0.6.tgz#092454ea9584a7b7ad5573062b2a87f5900fc480" - integrity sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw== - dependencies: - is-what "^3.14.1" - -copy-webpack-plugin@11.0.0: - version "11.0.0" - resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz#96d4dbdb5f73d02dd72d0528d1958721ab72e04a" - integrity sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ== - dependencies: - fast-glob "^3.2.11" - glob-parent "^6.0.1" - globby "^13.1.1" - normalize-path "^3.0.0" - schema-utils "^4.0.0" - serialize-javascript "^6.0.0" - -core-js-compat@^3.31.0, core-js-compat@^3.33.1: - version "3.38.1" - resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.38.1.tgz#2bc7a298746ca5a7bcb9c164bcb120f2ebc09a09" - integrity sha512-JRH6gfXxGmrzF3tZ57lFx97YARxCXPaMzPo6jELZhv88pBH5VXpQ+y0znKGlFnzuaihqhLbefxSJxWJMPtfDzw== - dependencies: - browserslist "^4.23.3" - -core-js@^3.38.1: - version "3.41.0" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.41.0.tgz#57714dafb8c751a6095d028a7428f1fb5834a776" - integrity sha512-SJ4/EHwS36QMJd6h/Rg+GyR4A5xE0FSI3eZ+iBVpfqf1x0eTSg1smWLHrA+2jQThZSh97fmSgFSU8B61nxosxA== - -core-util-is@~1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" - integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== - -cors@~2.8.5: - version "2.8.5" - resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" - integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== - dependencies: - object-assign "^4" - vary "^1" - -cosmiconfig@^8.2.0: - version "8.3.6" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.3.6.tgz#060a2b871d66dba6c8538ea1118ba1ac16f5fae3" - integrity sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA== - dependencies: - import-fresh "^3.3.0" - js-yaml "^4.1.0" - parse-json "^5.2.0" - path-type "^4.0.0" - -critters@0.0.20: - version "0.0.20" - resolved "https://registry.yarnpkg.com/critters/-/critters-0.0.20.tgz#08ddb961550ab7b3a59370537e4f01df208f7646" - integrity sha512-CImNRorKOl5d8TWcnAz5n5izQ6HFsvz29k327/ELy6UFcmbiZNOsinaKvzv16WZR0P6etfSWYzE47C4/56B3Uw== - dependencies: - chalk "^4.1.0" - css-select "^5.1.0" - dom-serializer "^2.0.0" - domhandler "^5.0.2" - htmlparser2 "^8.0.2" - postcss "^8.4.23" - pretty-bytes "^5.3.0" - -cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: - version "7.0.6" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" - integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== - dependencies: - path-key "^3.1.0" - shebang-command "^2.0.0" - which "^2.0.1" - -css-loader@6.8.1: - version "6.8.1" - resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.8.1.tgz#0f8f52699f60f5e679eab4ec0fcd68b8e8a50a88" - integrity sha512-xDAXtEVGlD0gJ07iclwWVkLoZOpEvAWaSyf6W18S2pOC//K8+qUDIx8IIT3D+HjnmkJPQeesOPv5aiUaJsCM2g== - dependencies: - icss-utils "^5.1.0" - postcss "^8.4.21" - postcss-modules-extract-imports "^3.0.0" - postcss-modules-local-by-default "^4.0.3" - postcss-modules-scope "^3.0.0" - postcss-modules-values "^4.0.0" - postcss-value-parser "^4.2.0" - semver "^7.3.8" - -css-select@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/css-select/-/css-select-5.1.0.tgz#b8ebd6554c3637ccc76688804ad3f6a6fdaea8a6" - integrity sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg== - dependencies: - boolbase "^1.0.0" - css-what "^6.1.0" - domhandler "^5.0.2" - domutils "^3.0.1" - nth-check "^2.0.1" - -css-what@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" - integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== - -cssesc@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" - integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== - -cssom@^0.4.4: - version "0.4.4" - resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.4.4.tgz#5a66cf93d2d0b661d80bf6a44fb65f5c2e4e0a10" - integrity sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw== - -cssom@~0.3.6: - version "0.3.8" - resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a" - integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg== - -cssstyle@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-2.3.0.tgz#ff665a0ddbdc31864b09647f34163443d90b0852" - integrity sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A== - dependencies: - cssom "~0.3.6" - -custom-event@~1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425" - integrity sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg== - -data-urls@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-2.0.0.tgz#156485a72963a970f5d5821aaf642bef2bf2db9b" - integrity sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ== - dependencies: - abab "^2.0.3" - whatwg-mimetype "^2.3.0" - whatwg-url "^8.0.0" - -date-format@^4.0.14: - version "4.0.14" - resolved "https://registry.yarnpkg.com/date-format/-/date-format-4.0.14.tgz#7a8e584434fb169a521c8b7aa481f355810d9400" - integrity sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg== - -debug@2.6.9: - version "2.6.9" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" - -debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2, debug@~4.3.4: - version "4.3.7" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" - integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== - dependencies: - ms "^2.1.3" - -decamelize@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" - integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA== - -decimal.js@^10.2.1: - version "10.4.3" - resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23" - integrity sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA== - -deep-is@^0.1.3: - version "0.1.4" - resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" - integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== - -default-gateway@^6.0.3: - version "6.0.3" - resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-6.0.3.tgz#819494c888053bdb743edbf343d6cdf7f2943a71" - integrity sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg== - dependencies: - execa "^5.0.0" - -defaults@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.4.tgz#b0b02062c1e2aa62ff5d9528f0f98baa90978d7a" - integrity sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A== - dependencies: - clone "^1.0.2" - -define-data-property@^1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" - integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== - dependencies: - es-define-property "^1.0.0" - es-errors "^1.3.0" - gopd "^1.0.1" - -define-lazy-prop@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" - integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== - -delayed-stream@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" - integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== - -delegates@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" - integrity sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ== - -depd@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" - integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== - -depd@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" - integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== - -dequal@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" - integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== - -destroy@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" - integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== - -detect-node@^2.0.4: - version "2.1.0" - resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" - integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== - -di@^0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/di/-/di-0.0.1.tgz#806649326ceaa7caa3306d75d985ea2748ba913c" - integrity sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA== - -diacritics@1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/diacritics/-/diacritics-1.3.0.tgz#3efa87323ebb863e6696cebb0082d48ff3d6f7a1" - integrity sha512-wlwEkqcsaxvPJML+rDh/2iS824jbREk6DUMUKkEaSlxdYHeS43cClJtsWglvw2RfeXGm6ohKDqsXteJ5sP5enA== - -dijkstrajs@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/dijkstrajs/-/dijkstrajs-1.0.3.tgz#4c8dbdea1f0f6478bff94d9c49c784d623e4fc23" - integrity sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA== - -dir-glob@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" - integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== - dependencies: - path-type "^4.0.0" - -dns-packet@^5.2.2: - version "5.6.1" - resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-5.6.1.tgz#ae888ad425a9d1478a0674256ab866de1012cf2f" - integrity sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw== - dependencies: - "@leichtgewicht/ip-codec" "^2.0.1" - -doctrine@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" - integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== - dependencies: - esutils "^2.0.2" - -dom-serialize@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/dom-serialize/-/dom-serialize-2.2.1.tgz#562ae8999f44be5ea3076f5419dcd59eb43ac95b" - integrity sha512-Yra4DbvoW7/Z6LBN560ZwXMjoNOSAN2wRsKFGc4iBeso+mpIA6qj1vfdf9HpMaKAqG6wXTy+1SYEzmNpKXOSsQ== - dependencies: - custom-event "~1.0.0" - ent "~2.2.0" - extend "^3.0.0" - void-elements "^2.0.0" - -dom-serializer@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53" - integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg== - dependencies: - domelementtype "^2.3.0" - domhandler "^5.0.2" - entities "^4.2.0" - -domelementtype@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" - integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== - -domexception@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/domexception/-/domexception-2.0.1.tgz#fb44aefba793e1574b0af6aed2801d057529f304" - integrity sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg== - dependencies: - webidl-conversions "^5.0.0" - -domhandler@^5.0.2, domhandler@^5.0.3: - version "5.0.3" - resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31" - integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w== - dependencies: - domelementtype "^2.3.0" - -domutils@^3.0.1: - version "3.1.0" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.1.0.tgz#c47f551278d3dc4b0b1ab8cbb42d751a6f0d824e" - integrity sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA== - dependencies: - dom-serializer "^2.0.0" - domelementtype "^2.3.0" - domhandler "^5.0.3" - -dotenv@~10.0.0: - version "10.0.0" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-10.0.0.tgz#3d4227b8fb95f81096cdd2b66653fb2c7085ba81" - integrity sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q== - -duplexer@^0.1.1: - version "0.1.2" - resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" - integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== - -eastasianwidth@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" - integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== - -ee-first@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" - integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== - -ejs@^3.1.7: - version "3.1.10" - resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.10.tgz#69ab8358b14e896f80cc39e62087b88500c3ac3b" - integrity sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA== - dependencies: - jake "^10.8.5" - -electron-to-chromium@^1.5.28: - version "1.5.31" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.31.tgz#b1478418769dec72ea70d9fdf147a81491857f10" - integrity sha512-QcDoBbQeYt0+3CWcK/rEbuHvwpbT/8SV9T3OSgs6cX1FlcUAkgrkqbg9zLnDrMM/rLamzQwal4LYFCiWk861Tg== - -emoji-regex@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" - integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== - -emoji-regex@^9.2.2: - version "9.2.2" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" - integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== - -emojis-list@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" - integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== - -encode-utf8@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/encode-utf8/-/encode-utf8-1.0.3.tgz#f30fdd31da07fb596f281beb2f6b027851994cda" - integrity sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw== - -encodeurl@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" - integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== - -encodeurl@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" - integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== - -encoding@^0.1.13: - version "0.1.13" - resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9" - integrity sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A== - dependencies: - iconv-lite "^0.6.2" - -end-of-stream@^1.4.1: - version "1.4.4" - resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" - integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== - dependencies: - once "^1.4.0" - -engine.io-parser@~5.2.1: - version "5.2.3" - resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.2.3.tgz#00dc5b97b1f233a23c9398d0209504cf5f94d92f" - integrity sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q== - -engine.io@~6.6.0: - version "6.6.1" - resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.6.1.tgz#a82b1e5511239a0e95fac14516870ee9138febc8" - integrity sha512-NEpDCw9hrvBW+hVEOK4T7v0jFJ++KgtPl4jKFwsZVfG1XhS0dCrSb3VMb9gPAd7VAdW52VT1EnaNiU2vM8C0og== - dependencies: - "@types/cookie" "^0.4.1" - "@types/cors" "^2.8.12" - "@types/node" ">=10.0.0" - accepts "~1.3.4" - base64id "2.0.0" - cookie "~0.4.1" - cors "~2.8.5" - debug "~4.3.1" - engine.io-parser "~5.2.1" - ws "~8.17.1" - -enhanced-resolve@^5.17.1: - version "5.17.1" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz#67bfbbcc2f81d511be77d686a90267ef7f898a15" - integrity sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg== - dependencies: - graceful-fs "^4.2.4" - tapable "^2.2.0" - -enquirer@~2.3.6: - version "2.3.6" - resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" - integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg== - dependencies: - ansi-colors "^4.1.1" - -ent@~2.2.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.1.tgz#68dc99a002f115792c26239baedaaea9e70c0ca2" - integrity sha512-QHuXVeZx9d+tIQAz/XztU0ZwZf2Agg9CcXcgE1rurqvdBeDBrpSwjl8/6XUqMg7tw2Y7uAdKb2sRv+bSEFqQ5A== - dependencies: - punycode "^1.4.1" - -entities@^4.2.0, entities@^4.3.0, entities@^4.4.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" - integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== - -env-paths@^2.2.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2" - integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A== - -err-code@^2.0.2: - version "2.0.3" - resolved "https://registry.yarnpkg.com/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9" - integrity sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA== - -errno@^0.1.1: - version "0.1.8" - resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.8.tgz#8bb3e9c7d463be4976ff888f76b4809ebc2e811f" - integrity sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A== - dependencies: - prr "~1.0.1" - -error-ex@^1.3.1: - version "1.3.2" - resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" - integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== - dependencies: - is-arrayish "^0.2.1" - -es-define-property@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" - integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== - dependencies: - get-intrinsic "^1.2.4" - -es-errors@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" - integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== - -es-module-lexer@^1.2.1: - version "1.5.4" - resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.5.4.tgz#a8efec3a3da991e60efa6b633a7cad6ab8d26b78" - integrity sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw== - -esbuild-wasm@0.18.17: - version "0.18.17" - resolved "https://registry.yarnpkg.com/esbuild-wasm/-/esbuild-wasm-0.18.17.tgz#d3d8827502c7714212a7b2544ee99132f07189cc" - integrity sha512-9OHGcuRzy+I8ziF9FzjfKLWAPbvi0e/metACVg9k6bK+SI4FFxeV6PcZsz8RIVaMD4YNehw+qj6UMR3+qj/EuQ== - -esbuild@0.18.17: - version "0.18.17" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.18.17.tgz#2aaf6bc6759b0c605777fdc435fea3969e091cad" - integrity sha512-1GJtYnUxsJreHYA0Y+iQz2UEykonY66HNWOb0yXYZi9/kNrORUEHVg87eQsCtqh59PEJ5YVZJO98JHznMJSWjg== - optionalDependencies: - "@esbuild/android-arm" "0.18.17" - "@esbuild/android-arm64" "0.18.17" - "@esbuild/android-x64" "0.18.17" - "@esbuild/darwin-arm64" "0.18.17" - "@esbuild/darwin-x64" "0.18.17" - "@esbuild/freebsd-arm64" "0.18.17" - "@esbuild/freebsd-x64" "0.18.17" - "@esbuild/linux-arm" "0.18.17" - "@esbuild/linux-arm64" "0.18.17" - "@esbuild/linux-ia32" "0.18.17" - "@esbuild/linux-loong64" "0.18.17" - "@esbuild/linux-mips64el" "0.18.17" - "@esbuild/linux-ppc64" "0.18.17" - "@esbuild/linux-riscv64" "0.18.17" - "@esbuild/linux-s390x" "0.18.17" - "@esbuild/linux-x64" "0.18.17" - "@esbuild/netbsd-x64" "0.18.17" - "@esbuild/openbsd-x64" "0.18.17" - "@esbuild/sunos-x64" "0.18.17" - "@esbuild/win32-arm64" "0.18.17" - "@esbuild/win32-ia32" "0.18.17" - "@esbuild/win32-x64" "0.18.17" - -esbuild@^0.18.10: - version "0.18.20" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.18.20.tgz#4709f5a34801b43b799ab7d6d82f7284a9b7a7a6" - integrity sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA== - 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" - -escalade@^3.1.1, escalade@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" - integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== - -escape-html@~1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" - integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== - -escape-string-regexp@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== - -escape-string-regexp@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" - integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== - -escodegen@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.1.0.tgz#ba93bbb7a43986d29d6041f99f5262da773e2e17" - integrity sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w== - dependencies: - esprima "^4.0.1" - estraverse "^5.2.0" - esutils "^2.0.2" - optionalDependencies: - source-map "~0.6.1" - -eslint-scope@5.1.1, eslint-scope@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" - integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== - dependencies: - esrecurse "^4.3.0" - estraverse "^4.1.1" - -eslint-scope@^7.2.2: - version "7.2.2" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f" - integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg== - dependencies: - esrecurse "^4.3.0" - estraverse "^5.2.0" - -eslint-scope@^8.0.2: - version "8.1.0" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-8.1.0.tgz#70214a174d4cbffbc3e8a26911d8bf51b9ae9d30" - integrity sha512-14dSvlhaVhKKsa9Fx1l8A17s7ah7Ef7wCakJ10LYk6+GYmP9yDti2oq2SEwcyndt6knfcZyhyxwY3i9yL78EQw== - dependencies: - esrecurse "^4.3.0" - estraverse "^5.2.0" - -eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3: - version "3.4.3" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" - integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== - -eslint@^8.57.1: - version "8.57.1" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.1.tgz#7df109654aba7e3bbe5c8eae533c5e461d3c6ca9" - integrity sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA== - dependencies: - "@eslint-community/eslint-utils" "^4.2.0" - "@eslint-community/regexpp" "^4.6.1" - "@eslint/eslintrc" "^2.1.4" - "@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" - ajv "^6.12.4" - chalk "^4.0.0" - cross-spawn "^7.0.2" - debug "^4.3.2" - doctrine "^3.0.0" - escape-string-regexp "^4.0.0" - eslint-scope "^7.2.2" - eslint-visitor-keys "^3.4.3" - espree "^9.6.1" - esquery "^1.4.2" - esutils "^2.0.2" - fast-deep-equal "^3.1.3" - file-entry-cache "^6.0.1" - find-up "^5.0.0" - glob-parent "^6.0.2" - globals "^13.19.0" - graphemer "^1.4.0" - ignore "^5.2.0" - imurmurhash "^0.1.4" - is-glob "^4.0.0" - is-path-inside "^3.0.3" - js-yaml "^4.1.0" - json-stable-stringify-without-jsonify "^1.0.1" - levn "^0.4.1" - lodash.merge "^4.6.2" - minimatch "^3.1.2" - natural-compare "^1.4.0" - optionator "^0.9.3" - strip-ansi "^6.0.1" - text-table "^0.2.0" - -espree@^9.6.0, espree@^9.6.1: - version "9.6.1" - resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" - integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== - dependencies: - acorn "^8.9.0" - acorn-jsx "^5.3.2" - eslint-visitor-keys "^3.4.1" - -esprima@^4.0.0, esprima@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" - integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== - -esquery@^1.4.2: - version "1.6.0" - resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.6.0.tgz#91419234f804d852a82dceec3e16cdc22cf9dae7" - integrity sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg== - dependencies: - estraverse "^5.1.0" - -esrecurse@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" - integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== - dependencies: - estraverse "^5.2.0" - -estraverse@^4.1.1: - version "4.3.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" - integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== - -estraverse@^5.1.0, estraverse@^5.2.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" - integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== - -esutils@^2.0.2: - version "2.0.3" - resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" - integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== - -etag@~1.8.1: - version "1.8.1" - resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" - integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== - -eventemitter-asyncresource@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/eventemitter-asyncresource/-/eventemitter-asyncresource-1.0.0.tgz#734ff2e44bf448e627f7748f905d6bdd57bdb65b" - integrity sha512-39F7TBIV0G7gTelxwbEqnwhp90eqCPON1k0NwNfwhgKn4Co4ybUbj2pECcXT0B3ztRKZ7Pw1JujUUgmQJHcVAQ== - -eventemitter3@^4.0.0: - version "4.0.7" - resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" - integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== - -events@^3.2.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" - integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== - -execa@^5.0.0: - version "5.1.1" - resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" - integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== - 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" - -exponential-backoff@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/exponential-backoff/-/exponential-backoff-3.1.1.tgz#64ac7526fe341ab18a39016cd22c787d01e00bf6" - integrity sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw== - -express@^4.17.3: - version "4.21.0" - resolved "https://registry.yarnpkg.com/express/-/express-4.21.0.tgz#d57cb706d49623d4ac27833f1cbc466b668eb915" - integrity sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng== - 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.6.0" - 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" - -extend@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" - integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== - -external-editor@^3.0.3: - version "3.1.0" - resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" - integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew== - dependencies: - chardet "^0.7.0" - iconv-lite "^0.4.24" - tmp "^0.0.33" - -fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" - integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== - -fast-glob@3.2.7: - version "3.2.7" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.7.tgz#fd6cb7a2d7e9aa7a7846111e85a196d6b2f766a1" - integrity sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q== - dependencies: - "@nodelib/fs.stat" "^2.0.2" - "@nodelib/fs.walk" "^1.2.3" - glob-parent "^5.1.2" - merge2 "^1.3.0" - micromatch "^4.0.4" - -fast-glob@3.3.1: - version "3.3.1" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.1.tgz#784b4e897340f3dbbef17413b3f11acf03c874c4" - integrity sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg== - dependencies: - "@nodelib/fs.stat" "^2.0.2" - "@nodelib/fs.walk" "^1.2.3" - glob-parent "^5.1.2" - merge2 "^1.3.0" - micromatch "^4.0.4" - -fast-glob@^3.2.11, fast-glob@^3.2.9, fast-glob@^3.3.0: - version "3.3.2" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" - integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== - dependencies: - "@nodelib/fs.stat" "^2.0.2" - "@nodelib/fs.walk" "^1.2.3" - glob-parent "^5.1.2" - merge2 "^1.3.0" - micromatch "^4.0.4" - -fast-json-stable-stringify@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" - integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== - -fast-levenshtein@^2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" - integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== - -fast-uri@^3.0.1: - version "3.0.6" - resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.6.tgz#88f130b77cfaea2378d56bf970dea21257a68748" - integrity sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw== - -fastq@^1.6.0: - version "1.17.1" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47" - integrity sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w== - dependencies: - reusify "^1.0.4" - -faye-websocket@^0.11.3: - version "0.11.4" - resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.4.tgz#7f0d9275cfdd86a1c963dc8b65fcc451edcbb1da" - integrity sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g== - dependencies: - websocket-driver ">=0.5.1" - -fflate@^0.4.8: - version "0.4.8" - resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.4.8.tgz#f90b82aefbd8ac174213abb338bd7ef848f0f5ae" - integrity sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA== - -figures@3.2.0, figures@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" - integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== - dependencies: - escape-string-regexp "^1.0.5" - -file-entry-cache@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" - integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== - dependencies: - flat-cache "^3.0.4" - -file-saver@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/file-saver/-/file-saver-2.0.5.tgz#d61cfe2ce059f414d899e9dd6d4107ee25670c38" - integrity sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA== - -filelist@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.4.tgz#f78978a1e944775ff9e62e744424f215e58352b5" - integrity sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q== - dependencies: - minimatch "^5.0.1" - -fill-range@^7.1.1: - version "7.1.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" - integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== - dependencies: - to-regex-range "^5.0.1" - -filter-obj@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/filter-obj/-/filter-obj-5.1.0.tgz#5bd89676000a713d7db2e197f660274428e524ed" - integrity sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng== - -finalhandler@1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" - integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== - dependencies: - debug "2.6.9" - encodeurl "~1.0.2" - escape-html "~1.0.3" - on-finished "~2.3.0" - parseurl "~1.3.3" - statuses "~1.5.0" - unpipe "~1.0.0" - -finalhandler@1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.3.1.tgz#0c575f1d1d324ddd1da35ad7ece3df7d19088019" - integrity sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ== - 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" - -find-cache-dir@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-4.0.0.tgz#a30ee0448f81a3990708f6453633c733e2f6eec2" - integrity sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg== - dependencies: - common-path-prefix "^3.0.0" - pkg-dir "^7.0.0" - -find-up-simple@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/find-up-simple/-/find-up-simple-1.0.1.tgz#18fb90ad49e45252c4d7fca56baade04fa3fca1e" - integrity sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ== - -find-up@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" - integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== - dependencies: - locate-path "^5.0.0" - path-exists "^4.0.0" - -find-up@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" - integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== - dependencies: - locate-path "^6.0.0" - path-exists "^4.0.0" - -find-up@^6.3.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-6.3.0.tgz#2abab3d3280b2dc7ac10199ef324c4e002c8c790" - integrity sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw== - dependencies: - locate-path "^7.1.0" - path-exists "^5.0.0" - -flag-icons@^7.3.2: - version "7.3.2" - resolved "https://registry.yarnpkg.com/flag-icons/-/flag-icons-7.3.2.tgz#c5ffaf25fb2be97498703a068fa699416bca9629" - integrity sha512-QkaZ6Zvai8LIjx+UNAHUJ5Dhz9OLZpBDwCRWxF6YErxIcR16jTkIFm3bFu54EkvKQy4+wicW+Gm7/0631wVQyQ== - -flat-cache@^3.0.4: - version "3.2.0" - resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.2.0.tgz#2c0c2d5040c99b1632771a9d105725c0115363ee" - integrity sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw== - dependencies: - flatted "^3.2.9" - keyv "^4.5.3" - rimraf "^3.0.2" - -flat@^5.0.2: - version "5.0.2" - resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" - integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== - -flatted@^3.2.7, flatted@^3.2.9: - version "3.3.1" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a" - integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw== - -follow-redirects@^1.0.0, follow-redirects@^1.15.6: - version "1.15.9" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1" - integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ== - -foreground-child@^3.1.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.0.tgz#0ac8644c06e431439f8561db8ecf29a7b5519c77" - integrity sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg== - dependencies: - cross-spawn "^7.0.0" - signal-exit "^4.0.1" - -form-data@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f" - integrity sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.8" - mime-types "^2.1.12" - -form-data@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" - integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.8" - mime-types "^2.1.12" - -forwarded@0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" - integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== - -fraction.js@^4.2.0: - version "4.3.7" - resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7" - integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew== - -fresh@0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" - integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== - -fs-constants@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" - integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== - -fs-extra@^11.1.0: - version "11.2.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.2.0.tgz#e70e17dfad64232287d01929399e0ea7c86b0e5b" - integrity sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw== - dependencies: - graceful-fs "^4.2.0" - jsonfile "^6.0.1" - universalify "^2.0.0" - -fs-extra@^8.1.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" - integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== - dependencies: - graceful-fs "^4.2.0" - jsonfile "^4.0.0" - universalify "^0.1.0" - -fs-minipass@^2.0.0, fs-minipass@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" - integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== - dependencies: - minipass "^3.0.0" - -fs-minipass@^3.0.0: - version "3.0.3" - resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-3.0.3.tgz#79a85981c4dc120065e96f62086bf6f9dc26cc54" - integrity sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw== - dependencies: - minipass "^7.0.3" - -fs-monkey@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/fs-monkey/-/fs-monkey-1.0.6.tgz#8ead082953e88d992cf3ff844faa907b26756da2" - integrity sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg== - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== - -fsevents@~2.3.2: - version "2.3.3" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" - integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== - -function-bind@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" - integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== - -gauge@^4.0.3: - version "4.0.4" - resolved "https://registry.yarnpkg.com/gauge/-/gauge-4.0.4.tgz#52ff0652f2bbf607a989793d53b751bef2328dce" - integrity sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg== - dependencies: - aproba "^1.0.3 || ^2.0.0" - color-support "^1.1.3" - console-control-strings "^1.1.0" - has-unicode "^2.0.1" - signal-exit "^3.0.7" - string-width "^4.2.3" - strip-ansi "^6.0.1" - wide-align "^1.1.5" - -gensync@^1.0.0-beta.2: - version "1.0.0-beta.2" - resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" - integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== - -get-caller-file@^2.0.1, get-caller-file@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" - integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== - -get-intrinsic@^1.1.3, get-intrinsic@^1.2.4: - version "1.2.4" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" - integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== - dependencies: - es-errors "^1.3.0" - function-bind "^1.1.2" - has-proto "^1.0.1" - has-symbols "^1.0.3" - hasown "^2.0.0" - -get-package-type@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" - integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== - -get-stream@^6.0.0: - version "6.0.1" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" - integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== - -glob-parent@^5.1.2, glob-parent@~5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" - integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== - dependencies: - is-glob "^4.0.1" - -glob-parent@^6.0.1, glob-parent@^6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" - integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== - dependencies: - is-glob "^4.0.3" - -glob-to-regexp@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" - integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== - -glob@7.1.4: - version "7.1.4" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255" - integrity sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A== - 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" - -glob@^10.2.2: - version "10.4.5" - resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956" - integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg== - 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" - -glob@^7.1.3, glob@^7.1.4, glob@^7.1.7: - version "7.2.3" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" - integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== - 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" - -glob@^8.0.1: - version "8.1.0" - resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" - integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^5.0.1" - once "^1.3.0" - -globals@^11.1.0: - version "11.12.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" - integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== - -globals@^13.19.0: - version "13.24.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171" - integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ== - dependencies: - type-fest "^0.20.2" - -globby@^11.1.0: - version "11.1.0" - resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" - integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== - 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" - -globby@^13.1.1: - version "13.2.2" - resolved "https://registry.yarnpkg.com/globby/-/globby-13.2.2.tgz#63b90b1bf68619c2135475cbd4e71e66aa090592" - integrity sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w== - dependencies: - dir-glob "^3.0.1" - fast-glob "^3.3.0" - ignore "^5.2.4" - merge2 "^1.4.1" - slash "^4.0.0" - -google-protobuf@^3.21.4: - version "3.21.4" - resolved "https://registry.yarnpkg.com/google-protobuf/-/google-protobuf-3.21.4.tgz#2f933e8b6e5e9f8edde66b7be0024b68f77da6c9" - integrity sha512-MnG7N936zcKTco4Jd2PX2U96Kf9PxygAPKBug+74LHzmHXmceN16MmRcdgZv+DGef/S9YvQAfRsNCn4cjf9yyQ== - -gopd@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" - integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== - dependencies: - get-intrinsic "^1.1.3" - -graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.6: - version "4.2.11" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" - integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== - -graphemer@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" - integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== - -grpc-web@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/grpc-web/-/grpc-web-1.5.0.tgz#154e4007ab59a94bf7726b87ef6c5bd8815ecf6e" - integrity sha512-y1tS3BBIoiVSzKTDF3Hm7E8hV2n7YY7pO0Uo7depfWJqKzWE+SKr0jvHNIJsJJYILQlpYShpi/DRJJMbosgDMQ== - -guess-parser@0.4.22: - version "0.4.22" - resolved "https://registry.yarnpkg.com/guess-parser/-/guess-parser-0.4.22.tgz#c26ab9e21b69bbc761960c5a1511476ae85428eb" - integrity sha512-KcUWZ5ACGaBM69SbqwVIuWGoSAgD+9iJnchR9j/IarVI1jHVeXv+bUXBIMeqVMSKt3zrn0Dgf9UpcOEpPBLbSg== - dependencies: - "@wessberg/ts-evaluator" "0.0.27" - -handle-thing@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e" - integrity sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg== - -has-flag@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" - integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== - -has-flag@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" - integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== - -has-property-descriptors@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" - integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== - dependencies: - es-define-property "^1.0.0" - -has-proto@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" - integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== - -has-symbols@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" - integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== - -has-unicode@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" - integrity sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ== - -hasown@^2.0.0, hasown@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" - integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== - dependencies: - function-bind "^1.1.2" - -hdr-histogram-js@^2.0.1: - version "2.0.3" - resolved "https://registry.yarnpkg.com/hdr-histogram-js/-/hdr-histogram-js-2.0.3.tgz#0b860534655722b6e3f3e7dca7b78867cf43dcb5" - integrity sha512-Hkn78wwzWHNCp2uarhzQ2SGFLU3JY8SBDDd3TAABK4fc30wm+MuPOrg5QVFVfkKOQd6Bfz3ukJEI+q9sXEkK1g== - dependencies: - "@assemblyscript/loader" "^0.10.1" - base64-js "^1.2.0" - pako "^1.0.3" - -hdr-histogram-percentiles-obj@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/hdr-histogram-percentiles-obj/-/hdr-histogram-percentiles-obj-3.0.0.tgz#9409f4de0c2dda78e61de2d9d78b1e9f3cba283c" - integrity sha512-7kIufnBqdsBGcSZLPJwqHT3yhk1QTsSlFsVD3kx5ixH/AlgBs9yM1q6DPhXZ8f8gtdqgh7N7/5btRLpQsS2gHw== - -hosted-git-info@^6.0.0: - version "6.1.1" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-6.1.1.tgz#629442c7889a69c05de604d52996b74fe6f26d58" - integrity sha512-r0EI+HBMcXadMrugk0GCQ+6BQV39PiWAZVfq7oIckeGiN7sjRGyQxPdft3nQekFTCQbYxLBH+/axZMeH8UX6+w== - dependencies: - lru-cache "^7.5.1" - -hosted-git-info@^7.0.0: - version "7.0.2" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-7.0.2.tgz#9b751acac097757667f30114607ef7b661ff4f17" - integrity sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w== - dependencies: - lru-cache "^10.0.1" - -hpack.js@^2.1.6: - version "2.1.6" - resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2" - integrity sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ== - dependencies: - inherits "^2.0.1" - obuf "^1.0.0" - readable-stream "^2.0.1" - wbuf "^1.1.0" - -html-encoding-sniffer@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz#42a6dc4fd33f00281176e8b23759ca4e4fa185f3" - integrity sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ== - dependencies: - whatwg-encoding "^1.0.5" - -html-entities@^2.3.2: - version "2.5.2" - resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.5.2.tgz#201a3cf95d3a15be7099521620d19dfb4f65359f" - integrity sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA== - -html-escaper@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" - integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== - -htmlparser2@^8.0.2: - version "8.0.2" - resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.2.tgz#f002151705b383e62433b5cf466f5b716edaec21" - integrity sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA== - dependencies: - domelementtype "^2.3.0" - domhandler "^5.0.3" - domutils "^3.0.1" - entities "^4.4.0" - -http-cache-semantics@^4.1.0, http-cache-semantics@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a" - integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ== - -http-deceiver@^1.2.7: - version "1.2.7" - resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" - integrity sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw== - -http-errors@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" - integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== - dependencies: - depd "2.0.0" - inherits "2.0.4" - setprototypeof "1.2.0" - statuses "2.0.1" - toidentifier "1.0.1" - -http-errors@~1.6.2: - version "1.6.3" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" - integrity sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A== - dependencies: - depd "~1.1.2" - inherits "2.0.3" - setprototypeof "1.1.0" - statuses ">= 1.4.0 < 2" - -http-parser-js@>=0.5.1: - version "0.5.8" - resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.8.tgz#af23090d9ac4e24573de6f6aecc9d84a48bf20e3" - integrity sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q== - -http-proxy-agent@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz#8a8c8ef7f5932ccf953c296ca8291b95aa74aa3a" - integrity sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg== - dependencies: - "@tootallnate/once" "1" - agent-base "6" - debug "4" - -http-proxy-agent@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43" - integrity sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w== - dependencies: - "@tootallnate/once" "2" - agent-base "6" - debug "4" - -http-proxy-middleware@^2.0.3: - version "2.0.7" - resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz#915f236d92ae98ef48278a95dedf17e991936ec6" - integrity sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA== - dependencies: - "@types/http-proxy" "^1.17.8" - http-proxy "^1.18.1" - is-glob "^4.0.1" - is-plain-obj "^3.0.0" - micromatch "^4.0.2" - -http-proxy@^1.18.1: - version "1.18.1" - resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" - integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== - dependencies: - eventemitter3 "^4.0.0" - follow-redirects "^1.0.0" - requires-port "^1.0.0" - -https-proxy-agent@5.0.1, https-proxy-agent@^5.0.0: - version "5.0.1" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" - integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== - dependencies: - agent-base "6" - debug "4" - -human-signals@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" - integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== - -humanize-ms@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" - integrity sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ== - dependencies: - ms "^2.0.0" - -i18n-iso-countries@^7.14.0: - version "7.14.0" - resolved "https://registry.yarnpkg.com/i18n-iso-countries/-/i18n-iso-countries-7.14.0.tgz#cd5ae098198bce1cc40cadbf0a37ce6c8e9d0edf" - integrity sha512-nXHJZYtNrfsi1UQbyRqm3Gou431elgLjKl//CYlnBGt5aTWdRPH1PiS2T/p/n8Q8LnqYqzQJik3Q7mkwvLokeg== - dependencies: - diacritics "1.3.0" - -iconv-lite@0.4.24, iconv-lite@^0.4.24: - version "0.4.24" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" - integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== - dependencies: - safer-buffer ">= 2.1.2 < 3" - -iconv-lite@^0.6.2, iconv-lite@^0.6.3: - version "0.6.3" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" - integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== - dependencies: - safer-buffer ">= 2.1.2 < 3.0.0" - -icss-utils@^5.0.0, icss-utils@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae" - integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== - -ieee754@^1.1.13, ieee754@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" - integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== - -ignore-walk@^6.0.0: - version "6.0.5" - resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-6.0.5.tgz#ef8d61eab7da169078723d1f82833b36e200b0dd" - integrity sha512-VuuG0wCnjhnylG1ABXT3dAuIpTNDs/G8jlpmwXY03fXoXy/8ZK8/T+hMzt8L4WnrLCJgdybqgPagnF/f97cg3A== - dependencies: - minimatch "^9.0.0" - -ignore@5.2.4: - version "5.2.4" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" - integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== - -ignore@^5.0.4, ignore@^5.2.0, ignore@^5.2.4: - version "5.3.2" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" - integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== - -image-size@~0.5.0: - version "0.5.5" - resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c" - integrity sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ== - -immutable@^4.0.0: - version "4.3.7" - resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.7.tgz#c70145fc90d89fb02021e65c84eb0226e4e5a381" - integrity sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw== - -import-fresh@^3.2.1, import-fresh@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" - integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== - dependencies: - parent-module "^1.0.0" - resolve-from "^4.0.0" - -imurmurhash@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" - integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== - -indent-string@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" - integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== - -index-to-position@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/index-to-position/-/index-to-position-1.0.0.tgz#baca236eb6e8c2b750b9225313c31751f84ef357" - integrity sha512-sCO7uaLVhRJ25vz1o8s9IFM3nVS4DkuQnyjMwiQPKvQuBYBDmb8H7zx8ki7nVh4HJQOdVWebyvLE0qt+clruxA== - -infer-owner@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467" - integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A== - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: - version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -inherits@2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" - integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw== - -ini@4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/ini/-/ini-4.1.1.tgz#d95b3d843b1e906e56d6747d5447904ff50ce7a1" - integrity sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g== - -inquirer@8.2.4: - version "8.2.4" - resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-8.2.4.tgz#ddbfe86ca2f67649a67daa6f1051c128f684f0b4" - integrity sha512-nn4F01dxU8VeKfq192IjLsxu0/OmMZ4Lg3xKAns148rCaXP6ntAoEkVYZThWjwON8AlzdZZi6oqnhNbxUG9hVg== - dependencies: - ansi-escapes "^4.2.1" - chalk "^4.1.1" - cli-cursor "^3.1.0" - cli-width "^3.0.0" - external-editor "^3.0.3" - figures "^3.0.0" - lodash "^4.17.21" - mute-stream "0.0.8" - ora "^5.4.1" - run-async "^2.4.0" - rxjs "^7.5.5" - string-width "^4.1.0" - strip-ansi "^6.0.0" - through "^2.3.6" - wrap-ansi "^7.0.0" - -ip-address@^9.0.5: - version "9.0.5" - resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-9.0.5.tgz#117a960819b08780c3bd1f14ef3c1cc1d3f3ea5a" - integrity sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g== - dependencies: - jsbn "1.1.0" - sprintf-js "^1.1.3" - -ipaddr.js@1.9.1: - version "1.9.1" - resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" - integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== - -ipaddr.js@^2.0.1: - version "2.2.0" - resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.2.0.tgz#d33fa7bac284f4de7af949638c9d68157c6b92e8" - integrity sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA== - -is-arrayish@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" - integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== - -is-binary-path@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" - integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== - dependencies: - binary-extensions "^2.0.0" - -is-core-module@^2.11.0, is-core-module@^2.13.0, is-core-module@^2.8.1: - version "2.15.1" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.15.1.tgz#a7363a25bee942fefab0de13bf6aa372c82dcc37" - integrity sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ== - dependencies: - hasown "^2.0.2" - -is-docker@^2.0.0, is-docker@^2.1.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" - integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== - -is-extglob@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" - integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== - -is-fullwidth-code-point@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" - integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== - -is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: - version "4.0.3" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" - integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== - dependencies: - is-extglob "^2.1.1" - -is-interactive@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e" - integrity sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w== - -is-lambda@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-lambda/-/is-lambda-1.0.1.tgz#3d9877899e6a53efc0160504cde15f82e6f061d5" - integrity sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ== - -is-number@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" - integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== - -is-path-inside@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" - integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== - -is-plain-obj@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-3.0.0.tgz#af6f2ea14ac5a646183a5bbdb5baabbc156ad9d7" - integrity sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA== - -is-plain-obj@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz#d65025edec3657ce032fd7db63c97883eaed71f0" - integrity sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg== - -is-plain-object@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" - integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== - dependencies: - isobject "^3.0.1" - -is-potential-custom-element-name@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5" - integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ== - -is-stream@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" - integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== - -is-unicode-supported@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" - integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== - -is-what@^3.14.1: - version "3.14.1" - resolved "https://registry.yarnpkg.com/is-what/-/is-what-3.14.1.tgz#e1222f46ddda85dead0fd1c9df131760e77755c1" - integrity sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA== - -is-wsl@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" - integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== - dependencies: - is-docker "^2.0.0" - -isarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== - -isbinaryfile@^4.0.8: - version "4.0.10" - resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.10.tgz#0c5b5e30c2557a2f06febd37b7322946aaee42b3" - integrity sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw== - -isexe@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== - -isobject@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" - integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== - -istanbul-lib-coverage@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz#675f0ab69503fad4b1d849f736baaca803344f49" - integrity sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA== - -istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: - version "3.2.2" - resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz#2d166c4b0644d43a39f04bf6c2edd1e585f31756" - integrity sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg== - -istanbul-lib-instrument@^5.0.4, istanbul-lib-instrument@^5.1.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz#d10c8885c2125574e1c231cacadf955675e1ce3d" - integrity sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg== - 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" - -istanbul-lib-report@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz#908305bac9a5bd175ac6a74489eafd0fc2445a7d" - integrity sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw== - dependencies: - istanbul-lib-coverage "^3.0.0" - make-dir "^4.0.0" - supports-color "^7.1.0" - -istanbul-lib-source-maps@^3.0.6: - version "3.0.6" - resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz#284997c48211752ec486253da97e3879defba8c8" - integrity sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw== - dependencies: - debug "^4.1.1" - istanbul-lib-coverage "^2.0.5" - make-dir "^2.1.0" - rimraf "^2.6.3" - source-map "^0.6.1" - -istanbul-lib-source-maps@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz#895f3a709fcfba34c6de5a42939022f3e4358551" - integrity sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw== - dependencies: - debug "^4.1.1" - istanbul-lib-coverage "^3.0.0" - source-map "^0.6.1" - -istanbul-reports@^3.0.2, istanbul-reports@^3.0.5: - version "3.1.7" - resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.1.7.tgz#daed12b9e1dca518e15c056e1e537e741280fa0b" - integrity sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g== - dependencies: - html-escaper "^2.0.0" - istanbul-lib-report "^3.0.0" - -jackspeak@^3.1.2: - version "3.4.3" - resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.3.tgz#8833a9d89ab4acde6188942bd1c53b6390ed5a8a" - integrity sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw== - dependencies: - "@isaacs/cliui" "^8.0.2" - optionalDependencies: - "@pkgjs/parseargs" "^0.11.0" - -jake@^10.8.5: - version "10.9.2" - resolved "https://registry.yarnpkg.com/jake/-/jake-10.9.2.tgz#6ae487e6a69afec3a5e167628996b59f35ae2b7f" - integrity sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA== - dependencies: - async "^3.2.3" - chalk "^4.0.2" - filelist "^1.0.4" - minimatch "^3.1.2" - -jasmine-core@^4.1.0: - version "4.6.1" - resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-4.6.1.tgz#5ebb8afa07282078f8d7b15871737a83b74e58f2" - integrity sha512-VYz/BjjmC3klLJlLwA4Kw8ytk0zDSmbbDLNs794VnWmkcCB7I9aAL/D48VNQtmITyPvea2C3jdUMfc3kAoy0PQ== - -jasmine-core@~5.6.0: - version "5.6.0" - resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-5.6.0.tgz#4b979c254e7d9b1fe8e767ab00c5d2901c00bd4f" - integrity sha512-niVlkeYVRwKFpmfWg6suo6H9CrNnydfBLEqefM5UjibYS+UoTjZdmvPJSiuyrRLGnFj1eYRhFd/ch+5hSlsFVA== - -jasmine-spec-reporter@~7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/jasmine-spec-reporter/-/jasmine-spec-reporter-7.0.0.tgz#94b939448e63d4e2bd01668142389f20f0a8ea49" - integrity sha512-OtC7JRasiTcjsaCBPtMO0Tl8glCejM4J4/dNuOJdA8lBjz4PmWjYQ6pzb0uzpBNAWJMDudYuj9OdXJWqM2QTJg== - dependencies: - colors "1.4.0" - -jest-worker@^27.4.5: - version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0" - integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg== - dependencies: - "@types/node" "*" - merge-stream "^2.0.0" - supports-color "^8.0.0" - -jiti@^1.18.2: - version "1.21.6" - resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.6.tgz#6c7f7398dd4b3142767f9a168af2f317a428d268" - integrity sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w== - -jose@^5.3.0: - version "5.9.6" - resolved "https://registry.yarnpkg.com/jose/-/jose-5.9.6.tgz#77f1f901d88ebdc405e57cce08d2a91f47521883" - integrity sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ== - -js-tokens@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" - integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== - -js-yaml@4.1.0, js-yaml@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" - integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== - dependencies: - argparse "^2.0.1" - -js-yaml@^3.10.0, js-yaml@^3.13.1: - version "3.14.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" - integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== - dependencies: - argparse "^1.0.7" - esprima "^4.0.0" - -jsbn@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-1.1.0.tgz#b01307cb29b618a1ed26ec79e911f803c4da0040" - integrity sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A== - -jsdom@^16.4.0: - version "16.7.0" - resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-16.7.0.tgz#918ae71965424b197c819f8183a754e18977b710" - integrity sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw== - dependencies: - abab "^2.0.5" - acorn "^8.2.4" - acorn-globals "^6.0.0" - cssom "^0.4.4" - cssstyle "^2.3.0" - data-urls "^2.0.0" - decimal.js "^10.2.1" - domexception "^2.0.1" - escodegen "^2.0.0" - form-data "^3.0.0" - html-encoding-sniffer "^2.0.1" - http-proxy-agent "^4.0.1" - https-proxy-agent "^5.0.0" - is-potential-custom-element-name "^1.0.1" - nwsapi "^2.2.0" - parse5 "6.0.1" - saxes "^5.0.1" - symbol-tree "^3.2.4" - tough-cookie "^4.0.0" - w3c-hr-time "^1.0.2" - w3c-xmlserializer "^2.0.0" - webidl-conversions "^6.1.0" - whatwg-encoding "^1.0.5" - whatwg-mimetype "^2.3.0" - whatwg-url "^8.5.0" - ws "^7.4.6" - xml-name-validator "^3.0.0" - -jsesc@^2.5.1: - version "2.5.2" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" - integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== - -jsesc@^3.0.2, jsesc@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.0.2.tgz#bb8b09a6597ba426425f2e4a07245c3d00b9343e" - integrity sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g== - -json-buffer@3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" - integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== - -json-parse-even-better-errors@^2.3.0, json-parse-even-better-errors@^2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" - integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== - -json-parse-even-better-errors@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.2.tgz#b43d35e89c0f3be6b5fbbe9dc6c82467b30c28da" - integrity sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ== - -json-schema-traverse@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" - integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== - -json-schema-traverse@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" - integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== - -json-stable-stringify-without-jsonify@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" - integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== - -json5@^2.1.2, json5@^2.2.2, json5@^2.2.3: - version "2.2.3" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" - integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== - -jsonc-parser@3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.2.0.tgz#31ff3f4c2b9793f89c67212627c51c6394f88e76" - integrity sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w== - -jsonfile@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" - integrity sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg== - optionalDependencies: - graceful-fs "^4.1.6" - -jsonfile@^6.0.1: - version "6.1.0" - resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" - integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== - dependencies: - universalify "^2.0.0" - optionalDependencies: - graceful-fs "^4.1.6" - -jsonparse@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" - integrity sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg== - -karma-chrome-launcher@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/karma-chrome-launcher/-/karma-chrome-launcher-3.2.0.tgz#eb9c95024f2d6dfbb3748d3415ac9b381906b9a9" - integrity sha512-rE9RkUPI7I9mAxByQWkGJFXfFD6lE4gC5nPuZdobf/QdTEJI6EU4yIay/cfU/xV4ZxlM5JiTv7zWYgA64NpS5Q== - dependencies: - which "^1.2.1" - -karma-coverage-istanbul-reporter@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/karma-coverage-istanbul-reporter/-/karma-coverage-istanbul-reporter-3.0.3.tgz#f3b5303553aadc8e681d40d360dfdc19bc7e9fe9" - integrity sha512-wE4VFhG/QZv2Y4CdAYWDbMmcAHeS926ZIji4z+FkB2aF/EposRb6DP6G5ncT/wXhqUfAb/d7kZrNKPonbvsATw== - dependencies: - istanbul-lib-coverage "^3.0.0" - istanbul-lib-report "^3.0.0" - istanbul-lib-source-maps "^3.0.6" - istanbul-reports "^3.0.2" - minimatch "^3.0.4" - -karma-coverage@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/karma-coverage/-/karma-coverage-2.2.1.tgz#e1cc074f93ace9dc4fb7e7aeca7135879c2e358c" - integrity sha512-yj7hbequkQP2qOSb20GuNSIyE//PgJWHwC2IydLE6XRtsnaflv+/OSGNssPjobYUlhVVagy99TQpqUt3vAUG7A== - dependencies: - istanbul-lib-coverage "^3.2.0" - istanbul-lib-instrument "^5.1.0" - istanbul-lib-report "^3.0.0" - istanbul-lib-source-maps "^4.0.1" - istanbul-reports "^3.0.5" - minimatch "^3.0.4" - -karma-jasmine-html-reporter@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-2.1.0.tgz#f951ad00b08d61d03595402c914d1a589c4930e3" - integrity sha512-sPQE1+nlsn6Hwb5t+HHwyy0A1FNCVKuL1192b+XNauMYWThz2kweiBVW1DqloRpVvZIJkIoHVB7XRpK78n1xbQ== - -karma-jasmine@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/karma-jasmine/-/karma-jasmine-5.1.0.tgz#3af4558a6502fa16856a0f346ec2193d4b884b2f" - integrity sha512-i/zQLFrfEpRyQoJF9fsCdTMOF5c2dK7C7OmsuKg2D0YSsuZSfQDiLuaiktbuio6F2wiCsZSnSnieIQ0ant/uzQ== - dependencies: - jasmine-core "^4.1.0" - -karma-source-map-support@1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz#58526ceccf7e8730e56effd97a4de8d712ac0d6b" - integrity sha512-RsBECncGO17KAoJCYXjv+ckIz+Ii9NCi+9enk+rq6XC81ezYkb4/RHE6CTXdA7IOJqoF3wcaLfVG0CPmE5ca6A== - dependencies: - source-map-support "^0.5.5" - -karma@^6.4.4: - version "6.4.4" - resolved "https://registry.yarnpkg.com/karma/-/karma-6.4.4.tgz#dfa5a426cf5a8b53b43cd54ef0d0d09742351492" - integrity sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w== - dependencies: - "@colors/colors" "1.5.0" - body-parser "^1.19.0" - braces "^3.0.2" - chokidar "^3.5.1" - connect "^3.7.0" - di "^0.0.1" - dom-serialize "^2.2.1" - glob "^7.1.7" - graceful-fs "^4.2.6" - http-proxy "^1.18.1" - isbinaryfile "^4.0.8" - lodash "^4.17.21" - log4js "^6.4.1" - mime "^2.5.2" - minimatch "^3.0.4" - mkdirp "^0.5.5" - qjobs "^1.2.0" - range-parser "^1.2.1" - rimraf "^3.0.2" - socket.io "^4.7.2" - source-map "^0.6.1" - tmp "^0.2.1" - ua-parser-js "^0.7.30" - yargs "^16.1.1" - -keyv@^4.5.3: - version "4.5.4" - resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" - integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== - dependencies: - json-buffer "3.0.1" - -kind-of@^6.0.2: - version "6.0.3" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" - integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== - -klona@^2.0.4: - version "2.0.6" - resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.6.tgz#85bffbf819c03b2f53270412420a4555ef882e22" - integrity sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA== - -launch-editor@^2.6.0: - version "2.9.1" - resolved "https://registry.yarnpkg.com/launch-editor/-/launch-editor-2.9.1.tgz#253f173bd441e342d4344b4dae58291abb425047" - integrity sha512-Gcnl4Bd+hRO9P9icCP/RVVT2o8SFlPXofuCxvA2SaZuH45whSvf5p8x5oih5ftLiVhEI4sp5xDY+R+b3zJBh5w== - dependencies: - picocolors "^1.0.0" - shell-quote "^1.8.1" - -less-loader@11.1.0: - version "11.1.0" - resolved "https://registry.yarnpkg.com/less-loader/-/less-loader-11.1.0.tgz#a452384259bdf8e4f6d5fdcc39543609e6313f82" - integrity sha512-C+uDBV7kS7W5fJlUjq5mPBeBVhYpTIm5gB09APT9o3n/ILeaXVsiSFTbZpTJCJwQ/Crczfn3DmfQFwxYusWFug== - dependencies: - klona "^2.0.4" - -less@4.1.3: - version "4.1.3" - resolved "https://registry.yarnpkg.com/less/-/less-4.1.3.tgz#175be9ddcbf9b250173e0a00b4d6920a5b770246" - integrity sha512-w16Xk/Ta9Hhyei0Gpz9m7VS8F28nieJaL/VyShID7cYvP6IL5oHeL6p4TXSDJqZE/lNv0oJ2pGVjJsRkfwm5FA== - dependencies: - copy-anything "^2.0.1" - parse-node-version "^1.0.1" - tslib "^2.3.0" - 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" - -levn@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" - integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== - dependencies: - prelude-ls "^1.2.1" - type-check "~0.4.0" - -libphonenumber-js@^1.12.6: - version "1.12.6" - resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.12.6.tgz#32a211b976dde3ccdf201c3c0b6e60351167c8bf" - integrity sha512-PJiS4ETaUfCOFLpmtKzAbqZQjCCKVu2OhTV4SVNNE7c2nu/dACvtCqj4L0i/KWNnIgRv7yrILvBj5Lonv5Ncxw== - -license-webpack-plugin@4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/license-webpack-plugin/-/license-webpack-plugin-4.0.2.tgz#1e18442ed20b754b82f1adeff42249b81d11aec6" - integrity sha512-771TFWFD70G1wLTC4oU2Cw4qvtmNrIw+wRvBtn+okgHl7slJVi7zfNcdmqDL72BojM30VNJ2UHylr1o77U37Jw== - dependencies: - webpack-sources "^3.0.0" - -lines-and-columns@^1.1.6: - version "1.2.4" - resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" - integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== - -lines-and-columns@~2.0.3: - version "2.0.4" - resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-2.0.4.tgz#d00318855905d2660d8c0822e3f5a4715855fc42" - integrity sha512-wM1+Z03eypVAVUCE7QdSqpVIvelbOakn1M0bPDoA4SGWPx3sNDVUiMo3L6To6WWGClB7VyXnhQ4Sn7gxiJbE6A== - -loader-runner@^4.2.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.0.tgz#c1b4a163b99f614830353b16755e7149ac2314e1" - integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg== - -loader-utils@3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-3.2.1.tgz#4fb104b599daafd82ef3e1a41fb9265f87e1f576" - integrity sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw== - -loader-utils@^2.0.0: - version "2.0.4" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.4.tgz#8b5cb38b5c34a9a018ee1fc0e6a066d1dfcc528c" - integrity sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw== - dependencies: - big.js "^5.2.2" - emojis-list "^3.0.0" - json5 "^2.1.2" - -locate-path@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" - integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== - dependencies: - p-locate "^4.1.0" - -locate-path@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" - integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== - dependencies: - p-locate "^5.0.0" - -locate-path@^7.0.0, locate-path@^7.1.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-7.2.0.tgz#69cb1779bd90b35ab1e771e1f2f89a202c2a8a8a" - integrity sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA== - dependencies: - p-locate "^6.0.0" - -lodash.debounce@^4.0.8: - version "4.0.8" - resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" - integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== - -lodash.merge@^4.6.2: - version "4.6.2" - resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" - integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== - -lodash@^4.17.21, lodash@^4.7.0: - version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" - integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== - -log-symbols@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" - integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== - dependencies: - chalk "^4.1.0" - is-unicode-supported "^0.1.0" - -log4js@^6.4.1: - version "6.9.1" - resolved "https://registry.yarnpkg.com/log4js/-/log4js-6.9.1.tgz#aba5a3ff4e7872ae34f8b4c533706753709e38b6" - integrity sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g== - dependencies: - date-format "^4.0.14" - debug "^4.3.4" - flatted "^3.2.7" - rfdc "^1.3.0" - streamroller "^3.1.5" - -lru-cache@^10.0.1, lru-cache@^10.2.0: - version "10.4.3" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" - integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== - -lru-cache@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" - integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== - dependencies: - yallist "^3.0.2" - -lru-cache@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" - integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== - dependencies: - yallist "^4.0.0" - -lru-cache@^7.4.4, lru-cache@^7.5.1, lru-cache@^7.7.1: - version "7.18.3" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89" - integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA== - -magic-string@0.30.1: - version "0.30.1" - resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.1.tgz#ce5cd4b0a81a5d032bd69aab4522299b2166284d" - integrity sha512-mbVKXPmS0z0G4XqFDCTllmDQ6coZzn94aMlb0o/A4HEHJCKcanlDZwYJgwnkmgD3jyWhUgj9VsPrfd972yPffA== - dependencies: - "@jridgewell/sourcemap-codec" "^1.4.15" - -make-dir@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" - integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA== - dependencies: - pify "^4.0.1" - semver "^5.6.0" - -make-dir@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-4.0.0.tgz#c3c2307a771277cd9638305f915c29ae741b614e" - integrity sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw== - dependencies: - semver "^7.5.3" - -make-fetch-happen@^10.0.3: - version "10.2.1" - resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz#f5e3835c5e9817b617f2770870d9492d28678164" - integrity sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w== - dependencies: - agentkeepalive "^4.2.1" - cacache "^16.1.0" - http-cache-semantics "^4.1.0" - http-proxy-agent "^5.0.0" - https-proxy-agent "^5.0.0" - is-lambda "^1.0.1" - lru-cache "^7.7.1" - minipass "^3.1.6" - minipass-collect "^1.0.2" - minipass-fetch "^2.0.3" - minipass-flush "^1.0.5" - minipass-pipeline "^1.2.4" - negotiator "^0.6.3" - promise-retry "^2.0.1" - socks-proxy-agent "^7.0.0" - ssri "^9.0.0" - -make-fetch-happen@^11.0.0, make-fetch-happen@^11.0.1, make-fetch-happen@^11.1.1: - version "11.1.1" - resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-11.1.1.tgz#85ceb98079584a9523d4bf71d32996e7e208549f" - integrity sha512-rLWS7GCSTcEujjVBs2YqG7Y4643u8ucvCJeSRqiLYhesrDuzeuFIk37xREzAsfQaqzl8b9rNCE4m6J8tvX4Q8w== - dependencies: - agentkeepalive "^4.2.1" - cacache "^17.0.0" - http-cache-semantics "^4.1.1" - http-proxy-agent "^5.0.0" - https-proxy-agent "^5.0.0" - is-lambda "^1.0.1" - lru-cache "^7.7.1" - minipass "^5.0.0" - minipass-fetch "^3.0.0" - minipass-flush "^1.0.5" - minipass-pipeline "^1.2.4" - negotiator "^0.6.3" - promise-retry "^2.0.1" - socks-proxy-agent "^7.0.0" - ssri "^10.0.0" - -material-colors@^1.2.6: - version "1.2.6" - resolved "https://registry.yarnpkg.com/material-colors/-/material-colors-1.2.6.tgz#6d1958871126992ceecc72f4bcc4d8f010865f46" - integrity sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg== - -material-design-icons-iconfont@^6.7.0: - version "6.7.0" - resolved "https://registry.yarnpkg.com/material-design-icons-iconfont/-/material-design-icons-iconfont-6.7.0.tgz#55cf0f3d7e4c76e032855b7e810b6e30535eff3c" - integrity sha512-lSj71DgVv20kO0kGbs42icDzbRot61gEDBLQACzkUuznRQBUYmbxzEkGU6dNBb5fRWHMaScYlAXX96HQ4/cJWA== - -media-typer@0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" - integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== - -memfs@^3.4.12, memfs@^3.4.3: - version "3.6.0" - resolved "https://registry.yarnpkg.com/memfs/-/memfs-3.6.0.tgz#d7a2110f86f79dd950a8b6df6d57bc984aa185f6" - integrity sha512-EGowvkkgbMcIChjMTMkESFDbZeSh8xZ7kNSF0hAiAN4Jh6jgHCRS0Ga/+C8y6Au+oqpezRHCfPsmJ2+DwAgiwQ== - dependencies: - fs-monkey "^1.0.4" - -merge-descriptors@1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5" - integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ== - -merge-stream@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" - integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== - -merge2@^1.3.0, merge2@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" - integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== - -methods@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" - integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== - -micromatch@^4.0.2, micromatch@^4.0.4: - version "4.0.8" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" - integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== - dependencies: - braces "^3.0.3" - picomatch "^2.3.1" - -mime-db@1.52.0: - version "1.52.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" - integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== - -"mime-db@>= 1.43.0 < 2": - version "1.53.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.53.0.tgz#3cb63cd820fc29896d9d4e8c32ab4fcd74ccb447" - integrity sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg== - -mime-types@^2.1.12, mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.24, mime-types@~2.1.34: - version "2.1.35" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" - integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== - dependencies: - mime-db "1.52.0" - -mime@1.6.0, mime@^1.4.1: - version "1.6.0" - resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" - integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== - -mime@^2.5.2: - version "2.6.0" - resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" - integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== - -mimic-fn@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" - integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== - -mini-css-extract-plugin@2.7.6: - version "2.7.6" - resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.6.tgz#282a3d38863fddcd2e0c220aaed5b90bc156564d" - integrity sha512-Qk7HcgaPkGG6eD77mLvZS1nmxlao3j+9PkrT9Uc7HAE1id3F41+DdBRYRYkbyfNRGzm8/YWtzhw7nVPmwhqTQw== - dependencies: - schema-utils "^4.0.0" - -minimalistic-assert@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" - integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== - -minimatch@3.0.5: - version "3.0.5" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.5.tgz#4da8f1290ee0f0f8e83d60ca69f8f134068604a3" - integrity sha512-tUpxzX0VAzJHjLu0xUfFv1gwVp9ba3IOuRAVH2EGuRW8a5emA2FlACLqiT/lDVtS1W+TGNwqz3sWaNyLgDJWuw== - dependencies: - brace-expansion "^1.1.7" - -minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" - integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== - dependencies: - brace-expansion "^1.1.7" - -minimatch@^5.0.1: - version "5.1.6" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" - integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== - dependencies: - brace-expansion "^2.0.1" - -minimatch@^9.0.0, minimatch@^9.0.4: - version "9.0.5" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" - integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== - dependencies: - brace-expansion "^2.0.1" - -minimist@^1.2.0, minimist@^1.2.6: - version "1.2.8" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" - integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== - -minipass-collect@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/minipass-collect/-/minipass-collect-1.0.2.tgz#22b813bf745dc6edba2576b940022ad6edc8c617" - integrity sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA== - dependencies: - minipass "^3.0.0" - -minipass-fetch@^2.0.3: - version "2.1.2" - resolved "https://registry.yarnpkg.com/minipass-fetch/-/minipass-fetch-2.1.2.tgz#95560b50c472d81a3bc76f20ede80eaed76d8add" - integrity sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA== - dependencies: - minipass "^3.1.6" - minipass-sized "^1.0.3" - minizlib "^2.1.2" - optionalDependencies: - encoding "^0.1.13" - -minipass-fetch@^3.0.0: - version "3.0.5" - resolved "https://registry.yarnpkg.com/minipass-fetch/-/minipass-fetch-3.0.5.tgz#f0f97e40580affc4a35cc4a1349f05ae36cb1e4c" - integrity sha512-2N8elDQAtSnFV0Dk7gt15KHsS0Fyz6CbYZ360h0WTYV1Ty46li3rAXVOQj1THMNLdmrD9Vt5pBPtWtVkpwGBqg== - dependencies: - minipass "^7.0.3" - minipass-sized "^1.0.3" - minizlib "^2.1.2" - optionalDependencies: - encoding "^0.1.13" - -minipass-flush@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/minipass-flush/-/minipass-flush-1.0.5.tgz#82e7135d7e89a50ffe64610a787953c4c4cbb373" - integrity sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw== - dependencies: - minipass "^3.0.0" - -minipass-json-stream@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/minipass-json-stream/-/minipass-json-stream-1.0.2.tgz#5121616c77a11c406c3ffa77509e0b77bb267ec3" - integrity sha512-myxeeTm57lYs8pH2nxPzmEEg8DGIgW+9mv6D4JZD2pa81I/OBjeU7PtICXV6c9eRGTA5JMDsuIPUZRCyBMYNhg== - dependencies: - jsonparse "^1.3.1" - minipass "^3.0.0" - -minipass-pipeline@^1.2.4: - version "1.2.4" - resolved "https://registry.yarnpkg.com/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz#68472f79711c084657c067c5c6ad93cddea8214c" - integrity sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A== - dependencies: - minipass "^3.0.0" - -minipass-sized@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/minipass-sized/-/minipass-sized-1.0.3.tgz#70ee5a7c5052070afacfbc22977ea79def353b70" - integrity sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g== - dependencies: - minipass "^3.0.0" - -minipass@^3.0.0, minipass@^3.1.1, minipass@^3.1.6: - version "3.3.6" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.6.tgz#7bba384db3a1520d18c9c0e5251c3444e95dd94a" - integrity sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw== - dependencies: - yallist "^4.0.0" - -minipass@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d" - integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== - -"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.0.3, minipass@^7.1.2: - version "7.1.2" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" - integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== - -minizlib@^2.1.1, minizlib@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" - integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== - dependencies: - minipass "^3.0.0" - yallist "^4.0.0" - -mkdirp@^0.5.5: - version "0.5.6" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" - integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== - dependencies: - minimist "^1.2.6" - -mkdirp@^1.0.3, mkdirp@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" - integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== - -moment@^2.30.1: - version "2.30.1" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae" - integrity sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how== - -mrmime@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/mrmime/-/mrmime-1.0.1.tgz#5f90c825fad4bdd41dc914eff5d1a8cfdaf24f27" - integrity sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw== - -ms@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== - -ms@2.1.3, ms@^2.0.0, ms@^2.1.3: - version "2.1.3" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" - integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== - -multicast-dns@^7.2.5: - version "7.2.5" - resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-7.2.5.tgz#77eb46057f4d7adbd16d9290fa7299f6fa64cced" - integrity sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg== - dependencies: - dns-packet "^5.2.2" - thunky "^1.0.2" - -mute-stream@0.0.8: - version "0.0.8" - resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" - integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== - -nanoid@^3.3.6, nanoid@^3.3.7: - version "3.3.7" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" - integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== - -natural-compare-lite@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz#17b09581988979fddafe0201e931ba933c96cbb4" - integrity sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g== - -natural-compare@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" - integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== - -needle@^3.1.0: - version "3.3.1" - resolved "https://registry.yarnpkg.com/needle/-/needle-3.3.1.tgz#63f75aec580c2e77e209f3f324e2cdf3d29bd049" - integrity sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q== - dependencies: - iconv-lite "^0.6.3" - sax "^1.2.4" - -negotiator@0.6.3, negotiator@^0.6.3: - version "0.6.3" - resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" - integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== - -neo-async@^2.6.2: - version "2.6.2" - resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" - integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== - -ngx-color@^9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/ngx-color/-/ngx-color-9.0.0.tgz#7e58da081a3563c53607fc10090a61cfa27eaffc" - integrity sha512-zyAFux+FRI4cACZ7g8DQQsBbNMhqmFkhtUPaxhkiVHhPzWU1iqXP8MqWH6By3guNOCch5oYrYNBWlHToklbdDg== - dependencies: - "@ctrl/tinycolor" "^3.6.0" - material-colors "^1.2.6" - tslib "^2.3.0" - -nice-napi@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/nice-napi/-/nice-napi-1.0.2.tgz#dc0ab5a1eac20ce548802fc5686eaa6bc654927b" - integrity sha512-px/KnJAJZf5RuBGcfD+Sp2pAKq0ytz8j+1NehvgIGFkvtvFrDM3T8E4x/JJODXK9WZow8RRGrbA9QQ3hs+pDhA== - dependencies: - node-addon-api "^3.0.0" - node-gyp-build "^4.2.2" - -node-addon-api@^3.0.0, node-addon-api@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" - integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A== - -node-forge@^1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" - integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== - -node-gyp-build@^4.2.2, node-gyp-build@^4.3.0: - version "4.8.2" - resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.2.tgz#4f802b71c1ab2ca16af830e6c1ea7dd1ad9496fa" - integrity sha512-IRUxE4BVsHWXkV/SFOut4qTlagw2aM8T5/vnTsmrHJvVoKueJHRc/JaFND7QDDc61kLYUJ6qlZM3sqTSyx2dTw== - -node-gyp@^9.0.0: - version "9.4.1" - resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-9.4.1.tgz#8a1023e0d6766ecb52764cc3a734b36ff275e185" - integrity sha512-OQkWKbjQKbGkMf/xqI1jjy3oCTgMKJac58G2+bjZb3fza6gW2YrCSdMQYaoTb70crvE//Gngr4f0AgVHmqHvBQ== - dependencies: - env-paths "^2.2.0" - exponential-backoff "^3.1.1" - glob "^7.1.4" - graceful-fs "^4.2.6" - make-fetch-happen "^10.0.3" - nopt "^6.0.0" - npmlog "^6.0.0" - rimraf "^3.0.2" - semver "^7.3.5" - tar "^6.1.2" - which "^2.0.2" - -node-releases@^2.0.18: - version "2.0.18" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.18.tgz#f010e8d35e2fe8d6b2944f03f70213ecedc4ca3f" - integrity sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g== - -nopt@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-6.0.0.tgz#245801d8ebf409c6df22ab9d95b65e1309cdb16d" - integrity sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g== - dependencies: - abbrev "^1.0.0" - -normalize-package-data@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-5.0.0.tgz#abcb8d7e724c40d88462b84982f7cbf6859b4588" - integrity sha512-h9iPVIfrVZ9wVYQnxFgtw1ugSvGEMOlyPWWtm8BMJhnwyEL/FLbYbTY3V3PpjI/BUK67n9PEWDu6eHzu1fB15Q== - dependencies: - hosted-git-info "^6.0.0" - is-core-module "^2.8.1" - semver "^7.3.5" - validate-npm-package-license "^3.0.4" - -normalize-package-data@^6.0.0: - version "6.0.2" - resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-6.0.2.tgz#a7bc22167fe24025412bcff0a9651eb768b03506" - integrity sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g== - dependencies: - hosted-git-info "^7.0.0" - semver "^7.3.5" - validate-npm-package-license "^3.0.4" - -normalize-path@^3.0.0, normalize-path@~3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" - integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== - -normalize-range@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" - integrity sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA== - -npm-bundled@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-3.0.1.tgz#cca73e15560237696254b10170d8f86dad62da25" - integrity sha512-+AvaheE/ww1JEwRHOrn4WHNzOxGtVp+adrg2AeZS/7KuxGUYFuBta98wYpfHBbJp6Tg6j1NKSEVHNcfZzJHQwQ== - dependencies: - npm-normalize-package-bin "^3.0.0" - -npm-install-checks@^6.0.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/npm-install-checks/-/npm-install-checks-6.3.0.tgz#046552d8920e801fa9f919cad569545d60e826fe" - integrity sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw== - dependencies: - semver "^7.1.1" - -npm-normalize-package-bin@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz#25447e32a9a7de1f51362c61a559233b89947832" - integrity sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ== - -npm-package-arg@10.1.0, npm-package-arg@^10.0.0: - version "10.1.0" - resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-10.1.0.tgz#827d1260a683806685d17193073cc152d3c7e9b1" - integrity sha512-uFyyCEmgBfZTtrKk/5xDfHp6+MdrqGotX/VoOyEEl3mBwiEE5FlBaePanazJSVMPT7vKepcjYBY2ztg9A3yPIA== - dependencies: - hosted-git-info "^6.0.0" - proc-log "^3.0.0" - semver "^7.3.5" - validate-npm-package-name "^5.0.0" - -npm-packlist@^7.0.0: - version "7.0.4" - resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-7.0.4.tgz#033bf74110eb74daf2910dc75144411999c5ff32" - integrity sha512-d6RGEuRrNS5/N84iglPivjaJPxhDbZmlbTwTDX2IbcRHG5bZCdtysYMhwiPvcF4GisXHGn7xsxv+GQ7T/02M5Q== - dependencies: - ignore-walk "^6.0.0" - -npm-pick-manifest@8.0.1: - version "8.0.1" - resolved "https://registry.yarnpkg.com/npm-pick-manifest/-/npm-pick-manifest-8.0.1.tgz#c6acd97d1ad4c5dbb80eac7b386b03ffeb289e5f" - integrity sha512-mRtvlBjTsJvfCCdmPtiu2bdlx8d/KXtF7yNXNWe7G0Z36qWA9Ny5zXsI2PfBZEv7SXgoxTmNaTzGSbbzDZChoA== - dependencies: - npm-install-checks "^6.0.0" - npm-normalize-package-bin "^3.0.0" - npm-package-arg "^10.0.0" - semver "^7.3.5" - -npm-pick-manifest@^8.0.0: - version "8.0.2" - resolved "https://registry.yarnpkg.com/npm-pick-manifest/-/npm-pick-manifest-8.0.2.tgz#2159778d9c7360420c925c1a2287b5a884c713aa" - integrity sha512-1dKY+86/AIiq1tkKVD3l0WI+Gd3vkknVGAggsFeBkTvbhMQ1OND/LKkYv4JtXPKUJ8bOTCyLiqEg2P6QNdK+Gg== - dependencies: - npm-install-checks "^6.0.0" - npm-normalize-package-bin "^3.0.0" - npm-package-arg "^10.0.0" - semver "^7.3.5" - -npm-registry-fetch@^14.0.0: - version "14.0.5" - resolved "https://registry.yarnpkg.com/npm-registry-fetch/-/npm-registry-fetch-14.0.5.tgz#fe7169957ba4986a4853a650278ee02e568d115d" - integrity sha512-kIDMIo4aBm6xg7jOttupWZamsZRkAqMqwqqbVXnUqstY5+tapvv6bkH/qMR76jdgV+YljEUCyWx3hRYMrJiAgA== - dependencies: - make-fetch-happen "^11.0.0" - minipass "^5.0.0" - minipass-fetch "^3.0.0" - minipass-json-stream "^1.0.1" - minizlib "^2.1.2" - npm-package-arg "^10.0.0" - proc-log "^3.0.0" - -npm-run-path@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" - integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== - dependencies: - path-key "^3.0.0" - -npmlog@^6.0.0: - version "6.0.2" - resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-6.0.2.tgz#c8166017a42f2dea92d6453168dd865186a70830" - integrity sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg== - dependencies: - are-we-there-yet "^3.0.0" - console-control-strings "^1.1.0" - gauge "^4.0.3" - set-blocking "^2.0.0" - -nth-check@^2.0.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d" - integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w== - dependencies: - boolbase "^1.0.0" - -nwsapi@^2.2.0: - version "2.2.13" - resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.13.tgz#e56b4e98960e7a040e5474536587e599c4ff4655" - integrity sha512-cTGB9ptp9dY9A5VbMSe7fQBcl/tt22Vcqdq8+eN93rblOuE0aCFu4aZ2vMwct/2t+lFnosm8RkQW1I0Omb1UtQ== - -nx@16.5.1: - version "16.5.1" - resolved "https://registry.yarnpkg.com/nx/-/nx-16.5.1.tgz#fc0d19090d8faae5f431f9fec199adf95881150c" - integrity sha512-I3hJRE4hG7JWAtncWwDEO3GVeGPpN0TtM8xH5ArZXyDuVeTth/i3TtJzdDzqXO1HHtIoAQN0xeq4n9cLuMil5g== - dependencies: - "@nrwl/tao" "16.5.1" - "@parcel/watcher" "2.0.4" - "@yarnpkg/lockfile" "^1.1.0" - "@yarnpkg/parsers" "3.0.0-rc.46" - "@zkochan/js-yaml" "0.0.6" - axios "^1.0.0" - chalk "^4.1.0" - cli-cursor "3.1.0" - cli-spinners "2.6.1" - cliui "^7.0.2" - dotenv "~10.0.0" - enquirer "~2.3.6" - fast-glob "3.2.7" - figures "3.2.0" - flat "^5.0.2" - fs-extra "^11.1.0" - glob "7.1.4" - ignore "^5.0.4" - js-yaml "4.1.0" - jsonc-parser "3.2.0" - lines-and-columns "~2.0.3" - minimatch "3.0.5" - npm-run-path "^4.0.1" - open "^8.4.0" - semver "7.5.3" - string-width "^4.2.3" - strong-log-transformer "^2.1.0" - tar-stream "~2.2.0" - tmp "~0.2.1" - tsconfig-paths "^4.1.2" - tslib "^2.3.0" - v8-compile-cache "2.3.0" - yargs "^17.6.2" - yargs-parser "21.1.1" - optionalDependencies: - "@nx/nx-darwin-arm64" "16.5.1" - "@nx/nx-darwin-x64" "16.5.1" - "@nx/nx-freebsd-x64" "16.5.1" - "@nx/nx-linux-arm-gnueabihf" "16.5.1" - "@nx/nx-linux-arm64-gnu" "16.5.1" - "@nx/nx-linux-arm64-musl" "16.5.1" - "@nx/nx-linux-x64-gnu" "16.5.1" - "@nx/nx-linux-x64-musl" "16.5.1" - "@nx/nx-win32-arm64-msvc" "16.5.1" - "@nx/nx-win32-x64-msvc" "16.5.1" - -object-assign@^4: - version "4.1.1" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" - integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== - -object-inspect@^1.13.1: - version "1.13.2" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.2.tgz#dea0088467fb991e67af4058147a24824a3043ff" - integrity sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g== - -object-path@^0.11.5: - version "0.11.8" - resolved "https://registry.yarnpkg.com/object-path/-/object-path-0.11.8.tgz#ed002c02bbdd0070b78a27455e8ae01fc14d4742" - integrity sha512-YJjNZrlXJFM42wTBn6zgOJVar9KFJvzx6sTWDte8sWZF//cnjl0BxHNpfZx+ZffXX63A9q0b1zsFiBX4g4X5KA== - -obuf@^1.0.0, obuf@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" - integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== - -on-finished@2.4.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" - integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== - dependencies: - ee-first "1.1.1" - -on-finished@~2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" - integrity sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww== - dependencies: - ee-first "1.1.1" - -on-headers@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" - integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== - -once@^1.3.0, once@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== - dependencies: - wrappy "1" - -onetime@^5.1.0, onetime@^5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" - integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== - dependencies: - mimic-fn "^2.1.0" - -open@8.4.2, open@^8.0.9, open@^8.4.0: - version "8.4.2" - resolved "https://registry.yarnpkg.com/open/-/open-8.4.2.tgz#5b5ffe2a8f793dcd2aad73e550cb87b59cb084f9" - integrity sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ== - dependencies: - define-lazy-prop "^2.0.0" - is-docker "^2.1.1" - is-wsl "^2.2.0" - -opentype.js@^1.3.4: - version "1.3.4" - resolved "https://registry.yarnpkg.com/opentype.js/-/opentype.js-1.3.4.tgz#1c0e72e46288473cc4a4c6a2dc60fd7fe6020d77" - integrity sha512-d2JE9RP/6uagpQAVtJoF0pJJA/fgai89Cc50Yp0EJHk+eLp6QQ7gBoblsnubRULNY132I0J1QKMJ+JTbMqz4sw== - dependencies: - string.prototype.codepointat "^0.2.1" - tiny-inflate "^1.0.3" - -optionator@^0.9.3: - version "0.9.4" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734" - integrity sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g== - dependencies: - deep-is "^0.1.3" - fast-levenshtein "^2.0.6" - levn "^0.4.1" - prelude-ls "^1.2.1" - type-check "^0.4.0" - word-wrap "^1.2.5" - -ora@5.4.1, ora@^5.4.1: - version "5.4.1" - resolved "https://registry.yarnpkg.com/ora/-/ora-5.4.1.tgz#1b2678426af4ac4a509008e5e4ac9e9959db9e18" - integrity sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ== - 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" - -os-tmpdir@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" - integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g== - -p-filter@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/p-filter/-/p-filter-4.1.0.tgz#fe0aa794e2dfad8ecf595a39a245484fcd09c6e4" - integrity sha512-37/tPdZ3oJwHaS3gNJdenCDB3Tz26i9sjhnguBtvN0vYlRIiDNnvTWkuh+0hETV9rLPdJ3rlL3yVOYPIAnM8rw== - dependencies: - p-map "^7.0.1" - -p-limit@^2.2.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" - integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== - dependencies: - p-try "^2.0.0" - -p-limit@^3.0.2: - version "3.1.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" - integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== - dependencies: - yocto-queue "^0.1.0" - -p-limit@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-4.0.0.tgz#914af6544ed32bfa54670b061cafcbd04984b644" - integrity sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ== - dependencies: - yocto-queue "^1.0.0" - -p-locate@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" - integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== - dependencies: - p-limit "^2.2.0" - -p-locate@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" - integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== - dependencies: - p-limit "^3.0.2" - -p-locate@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-6.0.0.tgz#3da9a49d4934b901089dca3302fa65dc5a05c04f" - integrity sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw== - dependencies: - p-limit "^4.0.0" - -p-map@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" - integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== - dependencies: - aggregate-error "^3.0.0" - -p-map@^7.0.1: - version "7.0.3" - resolved "https://registry.yarnpkg.com/p-map/-/p-map-7.0.3.tgz#7ac210a2d36f81ec28b736134810f7ba4418cdb6" - integrity sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA== - -p-retry@^4.5.0: - version "4.6.2" - resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-4.6.2.tgz#9baae7184057edd4e17231cee04264106e092a16" - integrity sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ== - dependencies: - "@types/retry" "0.12.0" - retry "^0.13.1" - -p-try@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" - integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== - -package-json-from-dist@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz#4f1471a010827a86f94cfd9b0727e36d267de505" - integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw== - -pacote@15.2.0: - version "15.2.0" - resolved "https://registry.yarnpkg.com/pacote/-/pacote-15.2.0.tgz#0f0dfcc3e60c7b39121b2ac612bf8596e95344d3" - integrity sha512-rJVZeIwHTUta23sIZgEIM62WYwbmGbThdbnkt81ravBplQv+HjyroqnLRNH2+sLJHcGZmLRmhPwACqhfTcOmnA== - dependencies: - "@npmcli/git" "^4.0.0" - "@npmcli/installed-package-contents" "^2.0.1" - "@npmcli/promise-spawn" "^6.0.1" - "@npmcli/run-script" "^6.0.0" - cacache "^17.0.0" - fs-minipass "^3.0.0" - minipass "^5.0.0" - npm-package-arg "^10.0.0" - npm-packlist "^7.0.0" - npm-pick-manifest "^8.0.0" - npm-registry-fetch "^14.0.0" - proc-log "^3.0.0" - promise-retry "^2.0.1" - read-package-json "^6.0.0" - read-package-json-fast "^3.0.0" - sigstore "^1.3.0" - ssri "^10.0.0" - tar "^6.1.11" - -pako@^1.0.3: - version "1.0.11" - resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" - integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== - -parent-module@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" - integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== - dependencies: - callsites "^3.0.0" - -parse-json@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" - integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== - dependencies: - "@babel/code-frame" "^7.0.0" - error-ex "^1.3.1" - json-parse-even-better-errors "^2.3.0" - lines-and-columns "^1.1.6" - -parse-json@^8.0.0: - version "8.2.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-8.2.0.tgz#794a590dcf54588ec2282ce6065f15121fa348a0" - integrity sha512-eONBZy4hm2AgxjNFd8a4nyDJnzUAH0g34xSQAwWEVGCjdZ4ZL7dKZBfq267GWP/JaS9zW62Xs2FeAdDvpHHJGQ== - dependencies: - "@babel/code-frame" "^7.26.2" - index-to-position "^1.0.0" - type-fest "^4.37.0" - -parse-node-version@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/parse-node-version/-/parse-node-version-1.0.1.tgz#e2b5dbede00e7fa9bc363607f53327e8b073189b" - integrity sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA== - -parse5-html-rewriting-stream@7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-7.0.0.tgz#e376d3e762d2950ccbb6bb59823fc1d7e9fdac36" - integrity sha512-mazCyGWkmCRWDI15Zp+UiCqMp/0dgEmkZRvhlsqqKYr4SsVm/TvnSpD9fCvqCA2zoWJcfRym846ejWBBHRiYEg== - dependencies: - entities "^4.3.0" - parse5 "^7.0.0" - parse5-sax-parser "^7.0.0" - -parse5-sax-parser@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/parse5-sax-parser/-/parse5-sax-parser-7.0.0.tgz#4c05064254f0488676aca75fb39ca069ec96dee5" - integrity sha512-5A+v2SNsq8T6/mG3ahcz8ZtQ0OUFTatxPbeidoMB7tkJSGDY3tdfl4MHovtLQHkEn5CGxijNWRQHhRQ6IRpXKg== - dependencies: - parse5 "^7.0.0" - -parse5@6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" - integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== - -parse5@^7.0.0, parse5@^7.1.2: - version "7.1.2" - resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32" - integrity sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw== - dependencies: - entities "^4.4.0" - -parseurl@~1.3.2, parseurl@~1.3.3: - version "1.3.3" - resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" - integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== - -path-exists@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" - integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== - -path-exists@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-5.0.0.tgz#a6aad9489200b21fab31e49cf09277e5116fb9e7" - integrity sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ== - -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== - -path-key@^3.0.0, path-key@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" - integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== - -path-parse@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" - integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== - -path-scurry@^1.11.1: - version "1.11.1" - resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2" - integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== - dependencies: - lru-cache "^10.2.0" - minipass "^5.0.0 || ^6.0.2 || ^7.0.0" - -path-to-regexp@0.1.10: - version "0.1.10" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.10.tgz#67e9108c5c0551b9e5326064387de4763c4d5f8b" - integrity sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w== - -path-type@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" - integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== - -picocolors@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" - integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== - -picocolors@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.0.tgz#5358b76a78cde483ba5cef6a9dc9671440b27d59" - integrity sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw== - -picomatch@2.3.1, picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" - integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== - -pify@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" - integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== - -piscina@4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/piscina/-/piscina-4.0.0.tgz#f8913d52b2000606d51aaa242f0813a0c77ca3b1" - integrity sha512-641nAmJS4k4iqpNUqfggqUBUMmlw0ZoM5VZKdQkV2e970Inn3Tk9kroCc1wpsYLD07vCwpys5iY0d3xI/9WkTg== - dependencies: - eventemitter-asyncresource "^1.0.0" - hdr-histogram-js "^2.0.1" - hdr-histogram-percentiles-obj "^3.0.0" - optionalDependencies: - nice-napi "^1.0.2" - -pkg-dir@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-7.0.0.tgz#8f0c08d6df4476756c5ff29b3282d0bab7517d11" - integrity sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA== - dependencies: - find-up "^6.3.0" - -pngjs@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-5.0.0.tgz#e79dd2b215767fd9c04561c01236df960bce7fbb" - integrity sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw== - -postcss-loader@7.3.3: - version "7.3.3" - resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-7.3.3.tgz#6da03e71a918ef49df1bb4be4c80401df8e249dd" - integrity sha512-YgO/yhtevGO/vJePCQmTxiaEwER94LABZN0ZMT4A0vsak9TpO+RvKRs7EmJ8peIlB9xfXCsS7M8LjqncsUZ5HA== - dependencies: - cosmiconfig "^8.2.0" - jiti "^1.18.2" - semver "^7.3.8" - -postcss-modules-extract-imports@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz#b4497cb85a9c0c4b5aabeb759bb25e8d89f15002" - integrity sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q== - -postcss-modules-local-by-default@^4.0.3: - version "4.0.5" - resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.5.tgz#f1b9bd757a8edf4d8556e8d0f4f894260e3df78f" - integrity sha512-6MieY7sIfTK0hYfafw1OMEG+2bg8Q1ocHCpoWLqOKj3JXlKu4G7btkmM/B7lFubYkYWmRSPLZi5chid63ZaZYw== - dependencies: - icss-utils "^5.0.0" - postcss-selector-parser "^6.0.2" - postcss-value-parser "^4.1.0" - -postcss-modules-scope@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.2.0.tgz#a43d28289a169ce2c15c00c4e64c0858e43457d5" - integrity sha512-oq+g1ssrsZOsx9M96c5w8laRmvEu9C3adDSjI8oTcbfkrTE8hx/zfyobUoWIxaKPO8bt6S62kxpw5GqypEw1QQ== - dependencies: - postcss-selector-parser "^6.0.4" - -postcss-modules-values@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz#d7c5e7e68c3bb3c9b27cbf48ca0bb3ffb4602c9c" - integrity sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ== - dependencies: - icss-utils "^5.0.0" - -postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4: - version "6.1.2" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz#27ecb41fb0e3b6ba7a1ec84fff347f734c7929de" - integrity sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg== - dependencies: - cssesc "^3.0.0" - util-deprecate "^1.0.2" - -postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" - integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== - -postcss@8.4.31: - version "8.4.31" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d" - integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ== - dependencies: - nanoid "^3.3.6" - picocolors "^1.0.0" - source-map-js "^1.0.2" - -postcss@^8.2.14, postcss@^8.4.21, postcss@^8.4.23, postcss@^8.4.27: - version "8.4.47" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.47.tgz#5bf6c9a010f3e724c503bf03ef7947dcb0fea365" - integrity sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ== - dependencies: - nanoid "^3.3.7" - picocolors "^1.1.0" - source-map-js "^1.2.1" - -posthog-js@^1.232.7: - version "1.232.7" - resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.232.7.tgz#76c0bb565d82f1567c47118b164fc8dbb795c48c" - integrity sha512-9Hqdam80bf41mj30O3ZLgdfDtZqR9TXNpTh7EkJo6C/emerFQEnW7X+E3yyE81KIPON4amSqTj5BL0gg+qZ28w== - dependencies: - core-js "^3.38.1" - fflate "^0.4.8" - preact "^10.19.3" - web-vitals "^4.2.4" - -preact@^10.19.3: - version "10.26.4" - resolved "https://registry.yarnpkg.com/preact/-/preact-10.26.4.tgz#b514f4249453a4247c82ff6d1267d59b7d78f9f9" - integrity sha512-KJhO7LBFTjP71d83trW+Ilnjbo+ySsaAgCfXOXUlmGzJ4ygYPWmysm77yg4emwfmoz3b22yvH5IsVFHbhUaH5w== - -prelude-ls@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" - integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== - -prettier-plugin-organize-imports@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-4.1.0.tgz#f3d3764046a8e7ba6491431158b9be6ffd83b90f" - integrity sha512-5aWRdCgv645xaa58X8lOxzZoiHAldAPChljr/MT0crXVOWTZ+Svl4hIWlz+niYSlO6ikE5UXkN1JrRvIP2ut0A== - -prettier@^3.5.3: - version "3.5.3" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.5.3.tgz#4fc2ce0d657e7a02e602549f053b239cb7dfe1b5" - integrity sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw== - -pretty-bytes@^5.3.0: - version "5.6.0" - resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" - integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg== - -proc-log@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/proc-log/-/proc-log-3.0.0.tgz#fb05ef83ccd64fd7b20bbe9c8c1070fc08338dd8" - integrity sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A== - -process-nextick-args@~2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" - integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== - -promise-inflight@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" - integrity sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g== - -promise-retry@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/promise-retry/-/promise-retry-2.0.1.tgz#ff747a13620ab57ba688f5fc67855410c370da22" - integrity sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g== - dependencies: - err-code "^2.0.2" - retry "^0.12.0" - -proxy-addr@~2.0.7: - version "2.0.7" - resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" - integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== - dependencies: - forwarded "0.2.0" - ipaddr.js "1.9.1" - -proxy-from-env@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" - integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== - -prr@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" - integrity sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw== - -psl@^1.1.33: - version "1.9.0" - resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" - integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag== - -punycode@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" - integrity sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ== - -punycode@^2.1.0, punycode@^2.1.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" - integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== - -qjobs@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.2.0.tgz#c45e9c61800bd087ef88d7e256423bdd49e5d071" - integrity sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg== - -qrcode@1.5.3: - version "1.5.3" - resolved "https://registry.yarnpkg.com/qrcode/-/qrcode-1.5.3.tgz#03afa80912c0dccf12bc93f615a535aad1066170" - integrity sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg== - dependencies: - dijkstrajs "^1.0.1" - encode-utf8 "^1.0.3" - pngjs "^5.0.0" - yargs "^15.3.1" - -qs@6.13.0: - version "6.13.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906" - integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg== - dependencies: - side-channel "^1.0.6" - -querystringify@^2.1.1: - version "2.2.0" - resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" - integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== - -queue-microtask@^1.2.2: - version "1.2.3" - resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" - integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== - -randombytes@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" - integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== - dependencies: - safe-buffer "^5.1.0" - -range-parser@^1.2.1, range-parser@~1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" - integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== - -raw-body@2.5.2: - version "2.5.2" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" - integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== - dependencies: - bytes "3.1.2" - http-errors "2.0.0" - iconv-lite "0.4.24" - unpipe "1.0.0" - -read-package-json-fast@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/read-package-json-fast/-/read-package-json-fast-3.0.2.tgz#394908a9725dc7a5f14e70c8e7556dff1d2b1049" - integrity sha512-0J+Msgym3vrLOUB3hzQCuZHII0xkNGCtz/HJH9xZshwv9DbDwkw1KaE3gx/e2J5rpEY5rtOy6cyhKOPrkP7FZw== - dependencies: - json-parse-even-better-errors "^3.0.0" - npm-normalize-package-bin "^3.0.0" - -read-package-json@^6.0.0: - version "6.0.4" - resolved "https://registry.yarnpkg.com/read-package-json/-/read-package-json-6.0.4.tgz#90318824ec456c287437ea79595f4c2854708836" - integrity sha512-AEtWXYfopBj2z5N5PbkAOeNHRPUg5q+Nen7QLxV8M2zJq1ym6/lCz3fYNTCXe19puu2d06jfHhrP7v/S2PtMMw== - dependencies: - glob "^10.2.2" - json-parse-even-better-errors "^3.0.0" - normalize-package-data "^5.0.0" - npm-normalize-package-bin "^3.0.0" - -read-package-up@^11.0.0: - version "11.0.0" - resolved "https://registry.yarnpkg.com/read-package-up/-/read-package-up-11.0.0.tgz#71fb879fdaac0e16891e6e666df22de24a48d5ba" - integrity sha512-MbgfoNPANMdb4oRBNg5eqLbB2t2r+o5Ua1pNt8BqGp4I0FJZhuVSOj3PaBPni4azWuSzEdNn2evevzVmEk1ohQ== - dependencies: - find-up-simple "^1.0.0" - read-pkg "^9.0.0" - type-fest "^4.6.0" - -read-pkg@^9.0.0: - version "9.0.1" - resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-9.0.1.tgz#b1b81fb15104f5dbb121b6bbdee9bbc9739f569b" - integrity sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA== - dependencies: - "@types/normalize-package-data" "^2.4.3" - normalize-package-data "^6.0.0" - parse-json "^8.0.0" - type-fest "^4.6.0" - unicorn-magic "^0.1.0" - -readable-stream@^2.0.1: - version "2.3.8" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" - 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" - -readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: - version "3.6.2" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" - integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - -readdirp@~3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" - integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== - dependencies: - picomatch "^2.2.1" - -reflect-metadata@^0.1.2: - version "0.1.14" - resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.14.tgz#24cf721fe60677146bb77eeb0e1f9dece3d65859" - integrity sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A== - -regenerate-unicode-properties@^10.2.0: - version "10.2.0" - resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz#626e39df8c372338ea9b8028d1f99dc3fd9c3db0" - integrity sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA== - dependencies: - regenerate "^1.4.2" - -regenerate@^1.4.2: - version "1.4.2" - resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" - integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== - -regenerator-runtime@^0.13.11: - version "0.13.11" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" - integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== - -regenerator-runtime@^0.14.0: - version "0.14.1" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" - integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== - -regenerator-transform@^0.15.2: - version "0.15.2" - resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.15.2.tgz#5bbae58b522098ebdf09bca2f83838929001c7a4" - integrity sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg== - dependencies: - "@babel/runtime" "^7.8.4" - -regex-parser@^2.2.11: - version "2.3.0" - resolved "https://registry.yarnpkg.com/regex-parser/-/regex-parser-2.3.0.tgz#4bb61461b1a19b8b913f3960364bb57887f920ee" - integrity sha512-TVILVSz2jY5D47F4mA4MppkBrafEaiUWJO/TcZHEIuI13AqoZMkK1WMA4Om1YkYbTx+9Ki1/tSUXbceyr9saRg== - -regexpu-core@^6.1.1: - version "6.1.1" - resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-6.1.1.tgz#b469b245594cb2d088ceebc6369dceb8c00becac" - integrity sha512-k67Nb9jvwJcJmVpw0jPttR1/zVfnKf8Km0IPatrU/zJ5XeG3+Slx0xLXs9HByJSzXzrlz5EDvN6yLNMDc2qdnw== - dependencies: - regenerate "^1.4.2" - regenerate-unicode-properties "^10.2.0" - regjsgen "^0.8.0" - regjsparser "^0.11.0" - unicode-match-property-ecmascript "^2.0.0" - unicode-match-property-value-ecmascript "^2.1.0" - -regjsgen@^0.8.0: - version "0.8.0" - resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.8.0.tgz#df23ff26e0c5b300a6470cad160a9d090c3a37ab" - integrity sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q== - -regjsparser@^0.11.0: - version "0.11.0" - resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.11.0.tgz#f01e6ccaba36d384fb0d00a06b78b372c8b681e8" - integrity sha512-vTbzVAjQDzwQdKuvj7qEq6OlAprCjE656khuGQ4QaBLg7abQ9I9ISpmLuc6inWe7zP75AECjqUa4g4sdQvOXhg== - dependencies: - jsesc "~3.0.2" - -require-directory@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" - integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== - -require-from-string@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" - integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== - -require-main-filename@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" - integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== - -requires-port@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" - integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== - -resolve-from@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" - integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== - -resolve-from@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" - integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== - -resolve-url-loader@5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/resolve-url-loader/-/resolve-url-loader-5.0.0.tgz#ee3142fb1f1e0d9db9524d539cfa166e9314f795" - integrity sha512-uZtduh8/8srhBoMx//5bwqjQ+rfYOUq8zC9NrMUGtjBiGTtFJM42s58/36+hTqeqINcnYe08Nj3LkK9lW4N8Xg== - 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" - -resolve@1.22.2: - version "1.22.2" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.2.tgz#0ed0943d4e301867955766c9f3e1ae6d01c6845f" - integrity sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g== - dependencies: - is-core-module "^2.11.0" - path-parse "^1.0.7" - supports-preserve-symlinks-flag "^1.0.0" - -resolve@^1.14.2: - version "1.22.8" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" - integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== - dependencies: - is-core-module "^2.13.0" - path-parse "^1.0.7" - supports-preserve-symlinks-flag "^1.0.0" - -restore-cursor@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" - integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== - dependencies: - onetime "^5.1.0" - signal-exit "^3.0.2" - -retry@^0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" - integrity sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow== - -retry@^0.13.1: - version "0.13.1" - resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" - integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== - -reusify@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" - integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== - -rfdc@^1.3.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.4.1.tgz#778f76c4fb731d93414e8f925fbecf64cce7f6ca" - integrity sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA== - -rimraf@^2.6.3: - version "2.7.1" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" - integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== - dependencies: - glob "^7.1.3" - -rimraf@^3.0.0, rimraf@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" - integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== - dependencies: - glob "^7.1.3" - -rollup@^3.27.1: - version "3.29.5" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.29.5.tgz#8a2e477a758b520fb78daf04bca4c522c1da8a54" - integrity sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w== - optionalDependencies: - fsevents "~2.3.2" - -run-async@^2.4.0: - version "2.4.1" - resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" - integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ== - -run-parallel@^1.1.9: - version "1.2.0" - resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" - integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== - dependencies: - queue-microtask "^1.2.2" - -rxjs@7.8.1, rxjs@^7.5.5: - version "7.8.1" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543" - integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg== - dependencies: - tslib "^2.1.0" - -rxjs@^7.8.2: - version "7.8.2" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.2.tgz#955bc473ed8af11a002a2be52071bf475638607b" - integrity sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA== - dependencies: - tslib "^2.1.0" - -safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - -safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.1.0, safe-buffer@~5.2.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== - -"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": - version "2.1.2" - resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" - integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== - -safevalues@^0.3.4: - version "0.3.4" - resolved "https://registry.yarnpkg.com/safevalues/-/safevalues-0.3.4.tgz#82e846a02b6956d7d40bf9f41e92e13fce0186db" - integrity sha512-LRneZZRXNgjzwG4bDQdOTSbze3fHm1EAKN/8bePxnlEZiBmkYEDggaHbuvHI9/hoqHbGfsEA7tWS9GhYHZBBsw== - -sass-loader@13.3.2: - version "13.3.2" - resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-13.3.2.tgz#460022de27aec772480f03de17f5ba88fa7e18c6" - integrity sha512-CQbKl57kdEv+KDLquhC+gE3pXt74LEAzm+tzywcA0/aHZuub8wTErbjAoNI57rPUWRYRNC5WUnNl8eGJNbDdwg== - dependencies: - neo-async "^2.6.2" - -sass@1.64.1: - version "1.64.1" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.64.1.tgz#6a46f6d68e0fa5ad90aa59ce025673ddaa8441cf" - integrity sha512-16rRACSOFEE8VN7SCgBu1MpYCyN7urj9At898tyzdXFhC+a+yOX5dXwAR7L8/IdPJ1NB8OYoXmD55DM30B2kEQ== - dependencies: - chokidar ">=3.0.0 <4.0.0" - immutable "^4.0.0" - source-map-js ">=0.6.2 <2.0.0" - -sax@^1.2.4: - version "1.4.1" - resolved "https://registry.yarnpkg.com/sax/-/sax-1.4.1.tgz#44cc8988377f126304d3b3fc1010c733b929ef0f" - integrity sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg== - -saxes@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/saxes/-/saxes-5.0.1.tgz#eebab953fa3b7608dbe94e5dadb15c888fa6696d" - integrity sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw== - dependencies: - xmlchars "^2.2.0" - -schema-utils@^3.1.1, schema-utils@^3.2.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.3.0.tgz#f50a88877c3c01652a15b622ae9e9795df7a60fe" - integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg== - dependencies: - "@types/json-schema" "^7.0.8" - ajv "^6.12.5" - ajv-keywords "^3.5.2" - -schema-utils@^4.0.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.2.0.tgz#70d7c93e153a273a805801882ebd3bff20d89c8b" - integrity sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw== - dependencies: - "@types/json-schema" "^7.0.9" - ajv "^8.9.0" - ajv-formats "^2.1.1" - ajv-keywords "^5.1.0" - -select-hose@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" - integrity sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg== - -selfsigned@^2.1.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-2.4.1.tgz#560d90565442a3ed35b674034cec4e95dceb4ae0" - integrity sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q== - dependencies: - "@types/node-forge" "^1.3.0" - node-forge "^1" - -semver@7.5.3: - version "7.5.3" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.3.tgz#161ce8c2c6b4b3bdca6caadc9fa3317a4c4fe88e" - integrity sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ== - dependencies: - lru-cache "^6.0.0" - -semver@7.5.4: - version "7.5.4" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" - integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== - dependencies: - lru-cache "^6.0.0" - -semver@^5.6.0: - version "5.7.2" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" - integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== - -semver@^6.3.0, semver@^6.3.1: - version "6.3.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" - integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== - -semver@^7.0.0, semver@^7.1.1, semver@^7.3.5, semver@^7.3.7, semver@^7.5.3, semver@^7.6.0: - version "7.6.3" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" - integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== - -semver@^7.3.8: - version "7.7.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.1.tgz#abd5098d82b18c6c81f6074ff2647fd3e7220c9f" - integrity sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA== - -send@0.19.0: - version "0.19.0" - resolved "https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8" - integrity sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw== - 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" - -serialize-javascript@^6.0.0, serialize-javascript@^6.0.1: - version "6.0.2" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" - integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== - dependencies: - randombytes "^2.1.0" - -serve-index@^1.9.1: - version "1.9.1" - resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239" - integrity sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw== - dependencies: - accepts "~1.3.4" - batch "0.6.1" - debug "2.6.9" - escape-html "~1.0.3" - http-errors "~1.6.2" - mime-types "~2.1.17" - parseurl "~1.3.2" - -serve-static@1.16.2: - version "1.16.2" - resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.2.tgz#b6a5343da47f6bdd2673848bf45754941e803296" - integrity sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw== - dependencies: - encodeurl "~2.0.0" - escape-html "~1.0.3" - parseurl "~1.3.3" - send "0.19.0" - -set-blocking@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" - integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== - -set-function-length@^1.2.1: - version "1.2.2" - resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" - integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== - dependencies: - define-data-property "^1.1.4" - es-errors "^1.3.0" - function-bind "^1.1.2" - get-intrinsic "^1.2.4" - gopd "^1.0.1" - has-property-descriptors "^1.0.2" - -setprototypeof@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" - integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== - -setprototypeof@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" - integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== - -shallow-clone@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" - integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== - dependencies: - kind-of "^6.0.2" - -shebang-command@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" - integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== - dependencies: - shebang-regex "^3.0.0" - -shebang-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" - integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== - -shell-quote@^1.8.1: - version "1.8.1" - resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680" - integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA== - -side-channel@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" - integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== - dependencies: - call-bind "^1.0.7" - es-errors "^1.3.0" - get-intrinsic "^1.2.4" - object-inspect "^1.13.1" - -signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: - version "3.0.7" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" - integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== - -signal-exit@^4.0.1: - version "4.1.0" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" - integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== - -sigstore@^1.3.0: - version "1.9.0" - resolved "https://registry.yarnpkg.com/sigstore/-/sigstore-1.9.0.tgz#1e7ad8933aa99b75c6898ddd0eeebc3eb0d59875" - integrity sha512-0Zjz0oe37d08VeOtBIuB6cRriqXse2e8w+7yIy2XSXjshRKxbc2KkhXjL229jXSxEm7UbcjS76wcJDGQddVI9A== - dependencies: - "@sigstore/bundle" "^1.1.0" - "@sigstore/protobuf-specs" "^0.2.0" - "@sigstore/sign" "^1.0.0" - "@sigstore/tuf" "^1.0.3" - make-fetch-happen "^11.0.1" - -slash@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" - integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== - -slash@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-4.0.0.tgz#2422372176c4c6c5addb5e2ada885af984b396a7" - integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew== - -smart-buffer@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" - integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== - -socket.io-adapter@~2.5.2: - version "2.5.5" - resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz#c7a1f9c703d7756844751b6ff9abfc1780664082" - integrity sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg== - dependencies: - debug "~4.3.4" - ws "~8.17.1" - -socket.io-parser@~4.2.4: - version "4.2.4" - resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.4.tgz#c806966cf7270601e47469ddeec30fbdfda44c83" - integrity sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew== - dependencies: - "@socket.io/component-emitter" "~3.1.0" - debug "~4.3.1" - -socket.io@^4.7.2: - version "4.8.0" - resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.8.0.tgz#33d05ae0915fad1670bd0c4efcc07ccfabebe3b1" - integrity sha512-8U6BEgGjQOfGz3HHTYaC/L1GaxDCJ/KM0XTkJly0EhZ5U/du9uNEZy4ZgYzEzIqlx2CMm25CrCqr1ck899eLNA== - dependencies: - accepts "~1.3.4" - base64id "~2.0.0" - cors "~2.8.5" - debug "~4.3.2" - engine.io "~6.6.0" - socket.io-adapter "~2.5.2" - socket.io-parser "~4.2.4" - -sockjs@^0.3.24: - version "0.3.24" - resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.24.tgz#c9bc8995f33a111bea0395ec30aa3206bdb5ccce" - integrity sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ== - dependencies: - faye-websocket "^0.11.3" - uuid "^8.3.2" - websocket-driver "^0.7.4" - -socks-proxy-agent@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz#dc069ecf34436621acb41e3efa66ca1b5fed15b6" - integrity sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww== - dependencies: - agent-base "^6.0.2" - debug "^4.3.3" - socks "^2.6.2" - -socks@^2.6.2: - version "2.8.3" - resolved "https://registry.yarnpkg.com/socks/-/socks-2.8.3.tgz#1ebd0f09c52ba95a09750afe3f3f9f724a800cb5" - integrity sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw== - dependencies: - ip-address "^9.0.5" - smart-buffer "^4.2.0" - -"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.2, source-map-js@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" - integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== - -source-map-loader@4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-4.0.1.tgz#72f00d05f5d1f90f80974eda781cbd7107c125f2" - integrity sha512-oqXpzDIByKONVY8g1NUPOTQhe0UTU5bWUl32GSkqK2LjJj0HmwTMVKxcUip0RgAYhY1mqgOxjbQM48a0mmeNfA== - dependencies: - abab "^2.0.6" - iconv-lite "^0.6.3" - source-map-js "^1.0.2" - -source-map-support@0.5.21, source-map-support@^0.5.5, source-map-support@~0.5.20: - version "0.5.21" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" - integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - -source-map@0.6.1, source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" - integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== - -source-map@0.7.4: - version "0.7.4" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656" - integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== - -spdx-correct@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.2.0.tgz#4f5ab0668f0059e34f9c00dce331784a12de4e9c" - integrity sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA== - dependencies: - spdx-expression-parse "^3.0.0" - spdx-license-ids "^3.0.0" - -spdx-exceptions@^2.1.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz#5d607d27fc806f66d7b64a766650fa890f04ed66" - integrity sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w== - -spdx-expression-parse@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679" - integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q== - dependencies: - spdx-exceptions "^2.1.0" - spdx-license-ids "^3.0.0" - -spdx-license-ids@^3.0.0: - version "3.0.21" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz#6d6e980c9df2b6fc905343a3b2d702a6239536c3" - integrity sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg== - -spdy-transport@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-3.0.0.tgz#00d4863a6400ad75df93361a1608605e5dcdcf31" - integrity sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw== - dependencies: - debug "^4.1.0" - detect-node "^2.0.4" - hpack.js "^2.1.6" - obuf "^1.1.2" - readable-stream "^3.0.6" - wbuf "^1.7.3" - -spdy@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/spdy/-/spdy-4.0.2.tgz#b74f466203a3eda452c02492b91fb9e84a27677b" - integrity sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA== - dependencies: - debug "^4.1.0" - handle-thing "^2.0.0" - http-deceiver "^1.2.7" - select-hose "^2.0.0" - spdy-transport "^3.0.0" - -sprintf-js@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.3.tgz#4914b903a2f8b685d17fdf78a70e917e872e444a" - integrity sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA== - -sprintf-js@~1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" - integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== - -ssri@^10.0.0: - version "10.0.6" - resolved "https://registry.yarnpkg.com/ssri/-/ssri-10.0.6.tgz#a8aade2de60ba2bce8688e3fa349bad05c7dc1e5" - integrity sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ== - dependencies: - minipass "^7.0.3" - -ssri@^9.0.0: - version "9.0.1" - resolved "https://registry.yarnpkg.com/ssri/-/ssri-9.0.1.tgz#544d4c357a8d7b71a19700074b6883fcb4eae057" - integrity sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q== - dependencies: - minipass "^3.1.1" - -statuses@2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" - integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== - -"statuses@>= 1.4.0 < 2", statuses@~1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" - integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== - -streamroller@^3.1.5: - version "3.1.5" - resolved "https://registry.yarnpkg.com/streamroller/-/streamroller-3.1.5.tgz#1263182329a45def1ffaef58d31b15d13d2ee7ff" - integrity sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw== - dependencies: - date-format "^4.0.14" - debug "^4.3.4" - fs-extra "^8.1.0" - -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - 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" - -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - 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" - -string-width@^5.0.1, string-width@^5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" - integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== - dependencies: - eastasianwidth "^0.2.0" - emoji-regex "^9.2.2" - strip-ansi "^7.0.1" - -string.prototype.codepointat@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz#004ad44c8afc727527b108cd462b4d971cd469bc" - integrity sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg== - -string_decoder@^1.1.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.0" - -string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" - integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== - dependencies: - safe-buffer "~5.1.0" - -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@^7.0.1: - version "7.1.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" - integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== - dependencies: - ansi-regex "^6.0.1" - -strip-bom@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" - integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== - -strip-final-newline@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" - integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== - -strip-json-comments@3.1.1, strip-json-comments@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" - integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== - -strong-log-transformer@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/strong-log-transformer/-/strong-log-transformer-2.1.0.tgz#0f5ed78d325e0421ac6f90f7f10e691d6ae3ae10" - integrity sha512-B3Hgul+z0L9a236FAUC9iZsL+nVHgoCJnqCbN588DjYxvGXaXaaFbfmQ/JhvKjZwsOukuR72XbHv71Qkug0HxA== - dependencies: - duplexer "^0.1.1" - minimist "^1.2.0" - through "^2.3.4" - -supports-color@^5.3.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" - integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== - dependencies: - has-flag "^3.0.0" - -supports-color@^7.1.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" - integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== - dependencies: - has-flag "^4.0.0" - -supports-color@^8.0.0: - version "8.1.1" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" - integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== - dependencies: - has-flag "^4.0.0" - -supports-preserve-symlinks-flag@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" - integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== - -symbol-observable@4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-4.0.0.tgz#5b425f192279e87f2f9b937ac8540d1984b39205" - integrity sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ== - -symbol-tree@^3.2.4: - version "3.2.4" - resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" - integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== - -tapable@^2.1.1, tapable@^2.2.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" - integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== - -tar-stream@~2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287" - integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== - 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" - -tar@^6.1.11, tar@^6.1.2: - version "6.2.1" - resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a" - integrity sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A== - 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" - -terser-webpack-plugin@^5.3.10: - version "5.3.10" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz#904f4c9193c6fd2a03f693a2150c62a92f40d199" - integrity sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w== - 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" - -terser@5.19.2: - version "5.19.2" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.19.2.tgz#bdb8017a9a4a8de4663a7983f45c506534f9234e" - integrity sha512-qC5+dmecKJA4cpYxRa5aVkKehYsQKc+AHeKl0Oe62aYjBL8ZA33tTljktDHJSaxxMnbI5ZYw+o/S2DxxLu8OfA== - dependencies: - "@jridgewell/source-map" "^0.3.3" - acorn "^8.8.2" - commander "^2.20.0" - source-map-support "~0.5.20" - -terser@^5.26.0: - version "5.34.1" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.34.1.tgz#af40386bdbe54af0d063e0670afd55c3105abeb6" - integrity sha512-FsJZ7iZLd/BXkz+4xrRTGJ26o/6VTjQytUk8b8OxkwcD2I+79VPJlz7qss1+zE7h8GNIScFqXcDyJ/KqBYZFVA== - dependencies: - "@jridgewell/source-map" "^0.3.3" - acorn "^8.8.2" - commander "^2.20.0" - source-map-support "~0.5.20" - -test-exclude@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e" - integrity sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w== - dependencies: - "@istanbuljs/schema" "^0.1.2" - glob "^7.1.4" - minimatch "^3.0.4" - -text-table@0.2.0, text-table@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" - integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== - -through@^2.3.4, through@^2.3.6: - version "2.3.8" - resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" - integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== - -thunky@^1.0.2: - version "1.1.0" - resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" - integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== - -tiny-inflate@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/tiny-inflate/-/tiny-inflate-1.0.3.tgz#122715494913a1805166aaf7c93467933eea26c4" - integrity sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw== - -tinycolor2@^1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.6.0.tgz#f98007460169b0263b97072c5ae92484ce02d09e" - integrity sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw== - -tmp@0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" - integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ== - dependencies: - rimraf "^3.0.0" - -tmp@^0.0.33: - version "0.0.33" - resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" - integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== - dependencies: - os-tmpdir "~1.0.2" - -tmp@^0.2.1, tmp@~0.2.1: - version "0.2.3" - resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.3.tgz#eb783cc22bc1e8bebd0671476d46ea4eb32a79ae" - integrity sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w== - -to-fast-properties@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" - integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== - -to-regex-range@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" - integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== - dependencies: - is-number "^7.0.0" - -toidentifier@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" - integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== - -tough-cookie@^4.0.0: - version "4.1.4" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.4.tgz#945f1461b45b5a8c76821c33ea49c3ac192c1b36" - integrity sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag== - dependencies: - psl "^1.1.33" - punycode "^2.1.1" - universalify "^0.2.0" - url-parse "^1.5.3" - -tr46@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/tr46/-/tr46-2.1.0.tgz#fa87aa81ca5d5941da8cbf1f9b749dc969a4e240" - integrity sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw== - dependencies: - punycode "^2.1.1" - -tree-kill@1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" - integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== - -ts-api-utils@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.3.0.tgz#4b490e27129f1e8e686b45cc4ab63714dc60eea1" - integrity sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ== - -tsconfig-paths@^4.1.2: - version "4.2.0" - resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz#ef78e19039133446d244beac0fd6a1632e2d107c" - integrity sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg== - dependencies: - json5 "^2.2.2" - minimist "^1.2.6" - strip-bom "^3.0.0" - -tslib@2.6.1: - version "2.6.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.1.tgz#fd8c9a0ff42590b25703c0acb3de3d3f4ede0410" - integrity sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig== - -tslib@^1.8.1: - version "1.14.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" - integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== - -tslib@^2.0.0, tslib@^2.0.3, tslib@^2.3.0, tslib@^2.4.0, tslib@^2.4.1: - version "2.7.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.7.0.tgz#d9b40c5c40ab59e8738f297df3087bf1a2690c01" - integrity sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA== - -tslib@^2.1.0, tslib@^2.7.0: - version "2.8.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" - integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== - -tsutils@^3.21.0: - version "3.21.0" - resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" - integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== - dependencies: - tslib "^1.8.1" - -tuf-js@^1.1.7: - version "1.1.7" - resolved "https://registry.yarnpkg.com/tuf-js/-/tuf-js-1.1.7.tgz#21b7ae92a9373015be77dfe0cb282a80ec3bbe43" - integrity sha512-i3P9Kgw3ytjELUfpuKVDNBJvk4u5bXL6gskv572mcevPbSKCV3zt3djhmlEQ65yERjIbOSncy7U4cQJaB1CBCg== - dependencies: - "@tufjs/models" "1.0.4" - debug "^4.3.4" - make-fetch-happen "^11.1.1" - -type-check@^0.4.0, type-check@~0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" - integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== - dependencies: - prelude-ls "^1.2.1" - -type-fest@^0.20.2: - version "0.20.2" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" - integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== - -type-fest@^0.21.3: - version "0.21.3" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" - integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== - -type-fest@^4.37.0, type-fest@^4.6.0: - version "4.38.0" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.38.0.tgz#659fa14d1a71c2811400aa3b5272627e0c1e6b96" - integrity sha512-2dBz5D5ycHIoliLYLi0Q2V7KRaDlH0uWIvmk7TYlAg5slqwiPv1ezJdZm1QEM0xgk29oYWMCbIG7E6gHpvChlg== - -type-is@~1.6.18: - version "1.6.18" - resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" - integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== - dependencies: - media-typer "0.3.0" - mime-types "~2.1.24" - -typed-assert@^1.0.8: - version "1.0.9" - resolved "https://registry.yarnpkg.com/typed-assert/-/typed-assert-1.0.9.tgz#8af9d4f93432c4970ec717e3006f33f135b06213" - integrity sha512-KNNZtayBCtmnNmbo5mG47p1XsCyrx6iVqomjcZnec/1Y5GGARaxPs6r49RnSPeUP3YjNYiU9sQHAtY4BBvnZwg== - -typescript@5.1: - version "5.1.6" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.1.6.tgz#02f8ac202b6dad2c0dd5e0913745b47a37998274" - integrity sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA== - -ua-parser-js@^0.7.30: - version "0.7.39" - resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.39.tgz#c71efb46ebeabc461c4612d22d54f88880fabe7e" - integrity sha512-IZ6acm6RhQHNibSt7+c09hhvsKy9WUr4DVbeq9U8o71qxyYtJpQeDxQnMrVqnIFMLcQjHO0I9wgfO2vIahht4w== - -undici-types@~6.19.2: - version "6.19.8" - resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" - integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== - -undici-types@~6.20.0: - version "6.20.0" - resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.20.0.tgz#8171bf22c1f588d1554d55bf204bc624af388433" - integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg== - -unicode-canonical-property-names-ecmascript@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz#cb3173fe47ca743e228216e4a3ddc4c84d628cc2" - integrity sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg== - -unicode-match-property-ecmascript@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz#54fd16e0ecb167cf04cf1f756bdcc92eba7976c3" - integrity sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q== - dependencies: - unicode-canonical-property-names-ecmascript "^2.0.0" - unicode-property-aliases-ecmascript "^2.0.0" - -unicode-match-property-value-ecmascript@^2.1.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz#a0401aee72714598f739b68b104e4fe3a0cb3c71" - integrity sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg== - -unicode-property-aliases-ecmascript@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz#43d41e3be698bd493ef911077c9b131f827e8ccd" - integrity sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w== - -unicorn-magic@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/unicorn-magic/-/unicorn-magic-0.1.0.tgz#1bb9a51c823aaf9d73a8bfcd3d1a23dde94b0ce4" - integrity sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ== - -unique-filename@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-2.0.1.tgz#e785f8675a9a7589e0ac77e0b5c34d2eaeac6da2" - integrity sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A== - dependencies: - unique-slug "^3.0.0" - -unique-filename@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-3.0.0.tgz#48ba7a5a16849f5080d26c760c86cf5cf05770ea" - integrity sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g== - dependencies: - unique-slug "^4.0.0" - -unique-slug@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-3.0.0.tgz#6d347cf57c8a7a7a6044aabd0e2d74e4d76dc7c9" - integrity sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w== - dependencies: - imurmurhash "^0.1.4" - -unique-slug@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-4.0.0.tgz#6bae6bb16be91351badd24cdce741f892a6532e3" - integrity sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ== - dependencies: - imurmurhash "^0.1.4" - -universalify@^0.1.0: - version "0.1.2" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" - integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== - -universalify@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0" - integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg== - -universalify@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" - integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== - -unpipe@1.0.0, unpipe@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" - integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== - -update-browserslist-db@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz#80846fba1d79e82547fb661f8d141e0945755fe5" - integrity sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A== - dependencies: - escalade "^3.2.0" - picocolors "^1.1.0" - -uri-js@^4.2.2: - version "4.4.1" - resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" - integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== - dependencies: - punycode "^2.1.0" - -url-parse@^1.5.3: - version "1.5.10" - resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1" - integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ== - dependencies: - querystringify "^2.1.1" - requires-port "^1.0.0" - -util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== - -utils-merge@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" - integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== - -uuid@^10.0.0: - version "10.0.0" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-10.0.0.tgz#5a95aa454e6e002725c79055fd42aaba30ca6294" - integrity sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ== - -uuid@^8.3.2: - version "8.3.2" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" - integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== - -v8-compile-cache@2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" - integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== - -validate-npm-package-license@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" - integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== - dependencies: - spdx-correct "^3.0.0" - spdx-expression-parse "^3.0.0" - -validate-npm-package-name@^5.0.0: - version "5.0.1" - resolved "https://registry.yarnpkg.com/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz#a316573e9b49f3ccd90dbb6eb52b3f06c6d604e8" - integrity sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ== - -vary@^1, vary@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" - integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== - -vite@4.5.5: - version "4.5.5" - resolved "https://registry.yarnpkg.com/vite/-/vite-4.5.5.tgz#639b9feca5c0a3bfe3c60cb630ef28bf219d742e" - integrity sha512-ifW3Lb2sMdX+WU91s3R0FyQlAyLxOzCSCP37ujw0+r5POeHPwe6udWVIElKQq8gk3t7b8rkmvqC6IHBpCff4GQ== - dependencies: - esbuild "^0.18.10" - postcss "^8.4.27" - rollup "^3.27.1" - optionalDependencies: - fsevents "~2.3.2" - -void-elements@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec" - integrity sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung== - -w3c-hr-time@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd" - integrity sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ== - dependencies: - browser-process-hrtime "^1.0.0" - -w3c-xmlserializer@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz#3e7104a05b75146cc60f564380b7f683acf1020a" - integrity sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA== - dependencies: - xml-name-validator "^3.0.0" - -watchpack@^2.4.1: - version "2.4.2" - resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.2.tgz#2feeaed67412e7c33184e5a79ca738fbd38564da" - integrity sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw== - dependencies: - glob-to-regexp "^0.4.1" - graceful-fs "^4.1.2" - -wbuf@^1.1.0, wbuf@^1.7.3: - version "1.7.3" - resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df" - integrity sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA== - dependencies: - minimalistic-assert "^1.0.0" - -wcwidth@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8" - integrity sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg== - dependencies: - defaults "^1.0.3" - -web-vitals@^4.2.4: - version "4.2.4" - resolved "https://registry.yarnpkg.com/web-vitals/-/web-vitals-4.2.4.tgz#1d20bc8590a37769bd0902b289550936069184b7" - integrity sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw== - -webidl-conversions@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-5.0.0.tgz#ae59c8a00b121543a2acc65c0434f57b0fc11aff" - integrity sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA== - -webidl-conversions@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-6.1.0.tgz#9111b4d7ea80acd40f5270d666621afa78b69514" - integrity sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w== - -webpack-dev-middleware@6.1.2: - version "6.1.2" - resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-6.1.2.tgz#0463232e59b7d7330fa154121528d484d36eb973" - integrity sha512-Wu+EHmX326YPYUpQLKmKbTyZZJIB8/n6R09pTmB03kJmnMsVPTo9COzHZFr01txwaCAuZvfBJE4ZCHRcKs5JaQ== - dependencies: - colorette "^2.0.10" - memfs "^3.4.12" - mime-types "^2.1.31" - range-parser "^1.2.1" - schema-utils "^4.0.0" - -webpack-dev-middleware@^5.3.1: - version "5.3.4" - resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz#eb7b39281cbce10e104eb2b8bf2b63fce49a3517" - integrity sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q== - dependencies: - colorette "^2.0.10" - memfs "^3.4.3" - mime-types "^2.1.31" - range-parser "^1.2.1" - schema-utils "^4.0.0" - -webpack-dev-server@4.15.1: - version "4.15.1" - resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.15.1.tgz#8944b29c12760b3a45bdaa70799b17cb91b03df7" - integrity sha512-5hbAst3h3C3L8w6W4P96L5vaV0PxSmJhxZvWKYIdgxOQm8pNZ5dEOmmSLBVpP85ReeyRt6AS1QJNyo/oFFPeVA== - dependencies: - "@types/bonjour" "^3.5.9" - "@types/connect-history-api-fallback" "^1.3.5" - "@types/express" "^4.17.13" - "@types/serve-index" "^1.9.1" - "@types/serve-static" "^1.13.10" - "@types/sockjs" "^0.3.33" - "@types/ws" "^8.5.5" - ansi-html-community "^0.0.8" - bonjour-service "^1.0.11" - chokidar "^3.5.3" - colorette "^2.0.10" - compression "^1.7.4" - connect-history-api-fallback "^2.0.0" - default-gateway "^6.0.3" - express "^4.17.3" - graceful-fs "^4.2.6" - html-entities "^2.3.2" - http-proxy-middleware "^2.0.3" - ipaddr.js "^2.0.1" - launch-editor "^2.6.0" - open "^8.0.9" - p-retry "^4.5.0" - rimraf "^3.0.2" - schema-utils "^4.0.0" - selfsigned "^2.1.1" - serve-index "^1.9.1" - sockjs "^0.3.24" - spdy "^4.0.2" - webpack-dev-middleware "^5.3.1" - ws "^8.13.0" - -webpack-merge@5.9.0: - version "5.9.0" - resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.9.0.tgz#dc160a1c4cf512ceca515cc231669e9ddb133826" - integrity sha512-6NbRQw4+Sy50vYNTw7EyOn41OZItPiXB8GNv3INSoe3PSFaHJEz3SHTrYVaRm2LilNGnFUzh0FAwqPEmU/CwDg== - dependencies: - clone-deep "^4.0.1" - wildcard "^2.0.0" - -webpack-sources@^3.0.0, webpack-sources@^3.2.3: - version "3.2.3" - resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" - integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== - -webpack-subresource-integrity@5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/webpack-subresource-integrity/-/webpack-subresource-integrity-5.1.0.tgz#8b7606b033c6ccac14e684267cb7fb1f5c2a132a" - integrity sha512-sacXoX+xd8r4WKsy9MvH/q/vBtEHr86cpImXwyg74pFIpERKt6FmB8cXpeuh0ZLgclOlHI4Wcll7+R5L02xk9Q== - dependencies: - typed-assert "^1.0.8" - -webpack@5.94.0: - version "5.94.0" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.94.0.tgz#77a6089c716e7ab90c1c67574a28da518a20970f" - integrity sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg== - 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" - -websocket-driver@>=0.5.1, websocket-driver@^0.7.4: - version "0.7.4" - resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760" - integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== - dependencies: - http-parser-js ">=0.5.1" - safe-buffer ">=5.1.0" - websocket-extensions ">=0.1.1" - -websocket-extensions@>=0.1.1: - version "0.1.4" - resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" - integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== - -whatwg-encoding@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0" - integrity sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw== - dependencies: - iconv-lite "0.4.24" - -whatwg-mimetype@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf" - integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g== - -whatwg-url@^8.0.0, whatwg-url@^8.5.0: - version "8.7.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-8.7.0.tgz#656a78e510ff8f3937bc0bcbe9f5c0ac35941b77" - integrity sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg== - dependencies: - lodash "^4.7.0" - tr46 "^2.1.0" - webidl-conversions "^6.1.0" - -which-module@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.1.tgz#776b1fe35d90aebe99e8ac15eb24093389a4a409" - integrity sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ== - -which@^1.2.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" - integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== - dependencies: - isexe "^2.0.0" - -which@^2.0.1, which@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" - integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== - dependencies: - isexe "^2.0.0" - -which@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/which/-/which-3.0.1.tgz#89f1cd0c23f629a8105ffe69b8172791c87b4be1" - integrity sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg== - dependencies: - isexe "^2.0.0" - -wide-align@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3" - integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg== - dependencies: - string-width "^1.0.2 || 2 || 3 || 4" - -wildcard@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.1.tgz#5ab10d02487198954836b6349f74fff961e10f67" - integrity sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ== - -word-wrap@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" - integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== - -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrap-ansi@^6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" - integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrap-ansi@^8.1.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" - integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== - dependencies: - ansi-styles "^6.1.0" - string-width "^5.0.1" - strip-ansi "^7.0.1" - -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== - -ws@^7.4.6: - version "7.5.10" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9" - integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ== - -ws@^8.13.0: - version "8.18.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" - integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== - -ws@~8.17.1: - version "8.17.1" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b" - integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ== - -xml-name-validator@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" - integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== - -xmlchars@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" - integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== - -y18n@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf" - integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ== - -y18n@^5.0.5: - version "5.0.8" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" - integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== - -yallist@^3.0.2: - version "3.1.1" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" - integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== - -yallist@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" - integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== - -yargs-parser@21.1.1, yargs-parser@^21.1.1: - version "21.1.1" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" - integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== - -yargs-parser@^18.1.2: - version "18.1.3" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" - integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== - dependencies: - camelcase "^5.0.0" - decamelize "^1.2.0" - -yargs-parser@^20.2.2: - version "20.2.9" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" - integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== - -yargs@17.7.2, yargs@^17.2.1, yargs@^17.6.2: - version "17.7.2" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" - 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" - -yargs@^15.3.1: - version "15.4.1" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" - integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A== - dependencies: - 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" - -yargs@^16.1.1: - version "16.2.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" - integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== - dependencies: - cliui "^7.0.2" - escalade "^3.1.1" - get-caller-file "^2.0.5" - require-directory "^2.1.1" - string-width "^4.2.0" - y18n "^5.0.5" - yargs-parser "^20.2.2" - -yocto-queue@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" - integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== - -yocto-queue@^1.0.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.2.1.tgz#36d7c4739f775b3cbc28e6136e21aa057adec418" - integrity sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg== - -zone.js@~0.13.3: - version "0.13.3" - resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.13.3.tgz#344c24098fa047eda6427a4c7ed486e391fd67b5" - integrity sha512-MKPbmZie6fASC/ps4dkmIhaT5eonHkEt6eAy80K42tAm0G2W+AahLJjbfi6X9NPdciOE9GRFTTM8u2IiF6O3ww== - dependencies: - tslib "^2.3.0" diff --git a/docker-bake.hcl b/docker-bake.hcl new file mode 100644 index 0000000000..2c36c66141 --- /dev/null +++ b/docker-bake.hcl @@ -0,0 +1,13 @@ +# login-standalone should be extended by the login-standalone target in apps/login/docker-bake.hcl +target "login-standalone" { + dockerfile = "build/login/Dockerfile" +} + +target "login-standalone-out" { + inherits = ["login-standalone"] + target = "build-out" + output = [ + "type=local,dest=.artifacts/login" + ] +} + \ No newline at end of file diff --git a/docs/.gitignore b/docs/.gitignore index bd99d98c6f..9698cc30bf 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -24,6 +24,6 @@ docs/apis/resources package-lock.json npm-debug.log* -yarn-debug.log* -yarn-error.log* +pnpm-debug.log* .vercel +/protoc-gen-connect-openapi* diff --git a/docs/README.md b/docs/README.md index 92d3f8f279..248053a640 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,43 +2,84 @@ This website is built using [Docusaurus 2](https://v2.docusaurus.io/), a modern static website generator. +The documentation is part of the ZITADEL monorepo and uses **pnpm** and **Turbo** for development and build processes. + +## Quick Start + +```bash +# From the repository root +pnpm install + +# Start development server (with Turbo) +pnpm turbo dev --filter=zitadel-docs + +# Or start directly from docs directory +cd docs && pnpm start +``` + +The site will be available at http://localhost:3000 + +## Available Scripts + +All scripts can be run from the repository root using Turbo: + +```bash +# Development server with live reload +pnpm turbo dev --filter=zitadel-docs + +# Build for production +pnpm turbo build --filter=zitadel-docs + +# Generate API documentation and configuration docs +pnpm turbo generate --filter=zitadel-docs + +# Lint and fix code +pnpm turbo lint --filter=zitadel-docs + +# Serve production build locally +cd docs && pnpm serve +``` + ## Add new Sites to existing Topics To add a new site to the already existing structure simply save the `md` file into the corresponding folder and append the sites id int the file `sidebars.js`. -## Installation +If you are introducing new APIs (gRPC), you need to add a new entry to `docusaurus.config.js` under the `plugins` section. -Install dependencies with +## Build Process -``` -yarn install -``` - -then run - -``` -yarn generate -``` +The documentation build process automatically: +1. **Downloads required protoc plugins** - Ensures `protoc-gen-connect-openapi` is available +2. **Generates gRPC documentation** - Creates API docs from proto files +3. **Generates API documentation** - Creates OpenAPI specification docs +4. **Copies configuration files** - Includes configuration examples +5. **Builds the Docusaurus site** - Generates the final static site ## Local Development -Start a local development server with +### Standard Development -``` -yarn start +```bash +# Install dependencies +pnpm install + +# Start development server +pnpm start ``` -When working on the API docs, run a local development server with +### API Documentation Development -``` -yarn start:api +When working on the API docs, run a local development server with: + +```bash +pnpm start:api ``` ## Container Image If you just want to start docusaurus locally without installing node you can fallback to our container image. -Execute the following commands from the repository root to build and start a local version of ZITADEL +Execute the following commands from the repository root to build and start a local version of ZITADEL ```shell docker build -f docs/Dockerfile . -t zitadel-docs diff --git a/docs/babel.config.js b/docs/babel.config.js deleted file mode 100644 index 279a0ff91c..0000000000 --- a/docs/babel.config.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - presets: [require.resolve("@docusaurus/core/lib/babel/preset")], - compact: auto -}; diff --git a/docs/base.yaml b/docs/base.yaml new file mode 100644 index 0000000000..dc5b9aa0f9 --- /dev/null +++ b/docs/base.yaml @@ -0,0 +1,3 @@ +openapi: 3.1.0 +info: + version: v2 \ No newline at end of file diff --git a/docs/buf.gen.yaml b/docs/buf.gen.yaml index a628f6e748..d23040f416 100644 --- a/docs/buf.gen.yaml +++ b/docs/buf.gen.yaml @@ -1,11 +1,18 @@ # buf.gen.yaml -version: v1 +version: v2 managed: enabled: true plugins: - - plugin: buf.build/grpc-ecosystem/openapiv2 + - remote: buf.build/grpc-ecosystem/openapiv2 out: .artifacts/openapi opt: - allow_delete_body - remove_internal_comments=true - preserve_rpc_order=true + - local: ./protoc-gen-connect-openapi/protoc-gen-connect-openapi + out: .artifacts/openapi3 + strategy: all + opt: + - short-service-tags + - ignore-googleapi-http + - base=base.yaml diff --git a/docs/docs/apis/actions/external-authentication.md b/docs/docs/apis/actions/external-authentication.md index 114185871b..33afb3d344 100644 --- a/docs/docs/apis/actions/external-authentication.md +++ b/docs/docs/apis/actions/external-authentication.md @@ -27,7 +27,7 @@ The first parameter contains the following fields - `idToken` *string* The id token provided by the identity provider. - `v1` - - `externalUser()` [*externalUser*](./objects#external-user) + - `externalUser` [*externalUser*](./objects#external-user) - `authError` *string* This is a verification errors string representation. If the verification succeeds, this is "none" - `authRequest` [*auth request*](/docs/apis/actions/objects#auth-request) diff --git a/docs/docs/apis/benchmarks/_template.mdx b/docs/docs/apis/benchmarks/_template.mdx index f015d20768..578ebcd842 100644 --- a/docs/docs/apis/benchmarks/_template.mdx +++ b/docs/docs/apis/benchmarks/_template.mdx @@ -63,7 +63,7 @@ TODO: describe the outcome of the test? ## Endpoint latencies -import OutputSource from "!!raw-loader!./output.json"; +import OutputSource from "./output.json"; import { BenchmarkChart } from '/src/components/benchmark_chart'; diff --git a/docs/docs/apis/benchmarks/v2.65.0/machine_jwt_profile_grant/index.mdx b/docs/docs/apis/benchmarks/v2.65.0/machine_jwt_profile_grant/index.mdx index 4c2809feb4..a8c10780ad 100644 --- a/docs/docs/apis/benchmarks/v2.65.0/machine_jwt_profile_grant/index.mdx +++ b/docs/docs/apis/benchmarks/v2.65.0/machine_jwt_profile_grant/index.mdx @@ -32,7 +32,7 @@ Tests are halted after this test run because of too many [client read events](ht ## /token endpoint latencies -import OutputSource from "!!raw-loader!./output.json"; +import OutputSource from "./output.json"; import { BenchmarkChart } from '/src/components/benchmark_chart'; diff --git a/docs/docs/apis/benchmarks/v2.66.0/machine_jwt_profile_grant/index.mdx b/docs/docs/apis/benchmarks/v2.66.0/machine_jwt_profile_grant/index.mdx index 881e7a38ee..6ab40eb4d4 100644 --- a/docs/docs/apis/benchmarks/v2.66.0/machine_jwt_profile_grant/index.mdx +++ b/docs/docs/apis/benchmarks/v2.66.0/machine_jwt_profile_grant/index.mdx @@ -33,7 +33,7 @@ The tests showed heavy database load by time by the first two database queries. ## Endpoint latencies -import OutputSource from "!!raw-loader!./output.json"; +import OutputSource from "./output.json"; import { BenchmarkChart } from '/src/components/benchmark_chart'; diff --git a/docs/docs/apis/benchmarks/v2.70.0/machine_jwt_profile_grant/index.mdx b/docs/docs/apis/benchmarks/v2.70.0/machine_jwt_profile_grant/index.mdx index fa8c84bc7e..d4fd2708a8 100644 --- a/docs/docs/apis/benchmarks/v2.70.0/machine_jwt_profile_grant/index.mdx +++ b/docs/docs/apis/benchmarks/v2.70.0/machine_jwt_profile_grant/index.mdx @@ -33,7 +33,7 @@ The performance goals of [this issue](https://github.com/zitadel/zitadel/issues/ ## Endpoint latencies -import OutputSource from "!!raw-loader!./output.json"; +import OutputSource from "./output.json"; import { BenchmarkChart } from '/src/components/benchmark_chart'; diff --git a/docs/docs/apis/benchmarks/v2.70.0/oidc_session/index.mdx b/docs/docs/apis/benchmarks/v2.70.0/oidc_session/index.mdx index 4615413d2b..94fd83f119 100644 --- a/docs/docs/apis/benchmarks/v2.70.0/oidc_session/index.mdx +++ b/docs/docs/apis/benchmarks/v2.70.0/oidc_session/index.mdx @@ -35,7 +35,7 @@ The tests showed that querying the user takes too much time because Zitadel ensu ## Endpoint latencies -import OutputSource from "!!raw-loader!./output.json"; +import OutputSource from "./output.json"; import { BenchmarkChart } from '/src/components/benchmark_chart'; diff --git a/docs/docs/apis/introduction.mdx b/docs/docs/apis/introduction.mdx index e05a7e84b3..905adfc0fb 100644 --- a/docs/docs/apis/introduction.mdx +++ b/docs/docs/apis/introduction.mdx @@ -45,7 +45,7 @@ The [OIDC Playground](https://zitadel.com/playgrounds/oidc) is for testing OpenI ### Custom -ZITADEL allows to authenticate users by creating a session with the [Session API](/docs/apis/resources/session_service_v2), get OIDC authentication request details with the [OIDC service API](/docs/apis/resources/oidc_service) or get SAML request details with the [SAML service API](/docs/apis/resources/saml_service). +ZITADEL allows to authenticate users by creating a session with the [Session API](/docs/apis/resources/session_service_v2), get OIDC authentication request details with the [OIDC service API](/docs/apis/resources/oidc_service_v2) or get SAML request details with the [SAML service API](/docs/apis/resources/saml_service_v2). User authorizations can be [retrieved as roles from our APIs](/docs/guides/integrate/retrieve-user-roles). Refer to our guide to learn how to [build your own login UI](/docs/guides/integrate/login-ui) diff --git a/docs/docs/apis/migration_v1_to_v2.mdx b/docs/docs/apis/migration_v1_to_v2.mdx new file mode 100644 index 0000000000..6258b99fd9 --- /dev/null +++ b/docs/docs/apis/migration_v1_to_v2.mdx @@ -0,0 +1,173 @@ +--- +title: Migrate from v1 APIs to v2 APIs +--- + +This guide gives you an overview for migrating from our v1 API to the new and improved v2 API. +This upgrade introduces some significant architectural changes designed to make your development experience smoother, more intuitive, and efficient. + +## The v1 Approach: Use-Case Based APIs +Our v1 API was structured around use cases, meaning we provided distinct APIs tailored to different user roles: + +- Auth API: For authenticated users. +- Management API: For administrators of an organization. +- Admin API: For administrators of an instance. +- System API: For managing multiple instances. + +While this approach served its initial purpose, it presented a few challenges. +Developers often found it difficult to determine which specific API endpoint to use for their needs. +Additionally, this model sometimes led to redundant implementations of similar functionalities across different APIs – for example, listing users might have existed in slightly different forms for an instance context versus an organization context. +This often required more extensive reading of documentation and further explanation to ensure correct usage. + +## The v2 Approach: A Resource-Based API +With our v2 API, we introduce a resource-based architecture. +This means instead of organizing by user type, we now structure our API around logical resources, such as: +- Users API +- Instance API +- Organization API +- And more... + +A key improvement in v2 is how context and permissions are handled. +The data you receive from an endpoint will now automatically be scoped based on the role and permissions of the authenticated user. + +For example: +- An instance administrator calling a GET /users endpoint will receive a list of all users within that instance. +- An organization administrator calling the exact same GET /users endpoint will receive a list of users belonging only to their specific organization. + +## Why the Change +The primary goals behind this architectural shift are to make our API: +- More Intuitive: Finding the right endpoint should feel natural. If you want to interact with users, you look at the Users API. +- Self-Explanatory: The structure itself guides you, reducing the need to sift through extensive documentation to understand which API "hat" you need to wear. +- Developer-Friendly: A cleaner, more consistent API surface means faster integration and less room for confusion. + +We're confident that these changes will significantly enhance your experience working with our platform. +The following sections will detail the specific resources that have been migrated and outline any changes you'll need to be aware of. + +## Resource Migration + +This section details the migrated resources, including any breaking changes and other important considerations for your transition from v1 to v2. + +### General Changes + +#### Sunsetting OpenAPI/REST Support in Favor of Connect RPC +While our v1 API already offered gRPC, it also provided a parallel REST/OpenAPI interface for clients who preferred making traditional HTTP calls. + +In our v2 API, we are consolidating our efforts to provide a more streamlined and efficient development experience. +The primary change is the removal of the OpenAPI/REST interface. +We will now exclusively support interaction with our gRPC services directly or through [Connect RPC](https://connectrpc.com/). + +Connect RPC is being introduced as the new, official way to interact with our gRPC services using familiar, plain HTTP/1.1. +It effectively replaces the previous REST gateway. + +For teams already using gRPC, your transition will be minimal. +For teams who were using the v1 REST API, migrating to v2 will involve adopting one of the following methods: + +- Native gRPC: For the highest performance and to leverage features like bi-directional streaming. +- Connect RPC: For making CRUD-like (Create, Read, Update, Delete) calls over HTTP. This is the recommended path for most clients migrating from our v1 REST API. + +A significant advantage of this new architecture remains the automatic generation of client libraries. +Based on our .proto service definitions, you can generate type-safe clients for your specific programming language, whether you use native gRPC or Connect RPC. +This eliminates the need to write boilerplate code for handling HTTP requests and parsing responses, leading to a more streamlined and less error-prone development process. + +#### Contextual Information in the Request Body + +A key change in v2 is that contextual data, like organization_id, must now be sent in the request body. +Previously, this was sent in the request headers. + +**v1 (Header)** +``` +x-zitadel-orgid: 1234567890 +``` + +**v2 (Request Body)** +``` +{ + "organization_id": "1234567890" +} +``` + +### Instances + +No major changes have been made to the organization requests. + +### Organizations + +No major changes have been made to the organization requests. + +### Users + +When migrating your user management from v1 to v2, the most significant updates involve user states and the initial onboarding process: + +- **Unified User Creation Endpoint**: + - A significant simplification in v2 is the consolidation of user creation. There is now one primary endpoint for creating users, regardless of whether they are human or machine users. + - You can use this single endpoint to provision both human users (individuals interacting with your application) and machine users (e.g., service accounts, API clients), typically by specifying the user type in the request payload. +- **No More "Initial" State**: + - In v1, new users without a password or verified email were automatically assigned an initial state. This default assumption wasn't always ideal. + - In v2, this initial state has been removed. All newly created users are now active by default, regardless of their initial attributes. +- **New Onboarding Process**: + - To enable users to set up their accounts, you can now send them an invitation code. This allows them to securely add their authentication methods. +- **Flexible Email Verification**: + - v2 provides more control over email verification: + - You can choose at user creation whether an email verification code should be sent automatically. + - Alternatively, the API can return the verification code directly to you, empowering you to send a customized verification email. + +[Users API v2 Documentation](/docs/apis/resources/user_service_v2) + +### Projects + +We've simplified how you interact with projects by unifying projects and granted_projects into a single resource. +From a consumer's perspective, it no longer matters if you own a project or if it was granted to you by another organization; it's all just a project. + +The main difference now is your level of permission. +Your permissions determine whether you have administrative rights (like updating the project's details) or if you can only view the project and manage authorizations for your users. + +This change significantly streamlines API calls. +For example, where you previously had to make two separate requests to see all projects, you now make one. + +**v1 (Separate Requests):** +``` +- ListProjects +- ListGrantedProjects +``` + +**v2 (Single Request with Filter):** +``` +- ListProjects (returns all projects you have access to) +``` + +You can now use filters within the single ListProjects request if you need to differentiate between project types, such as filtering by projects you own versus those that have been granted to you. +Update your code to use this new unified ListProjects endpoint. + +### Applications + +We have streamlined the creation and management of applications. +In v1, each application type had its own unique endpoints. +In v2, we have unified these into a single set of endpoints for all application types. + +The biggest change is in how you create/update applications. +Instead of calling a specific endpoint for each type (e.g., CreateOidcApp, CreateSamlApp), you will now use a single CreateApp endpoint. + +To specify the type of application, you will include its specific configuration object within the request body. +For example, to create a OIDC app, you will provide a oidc object in the request. +All properties that are common to every application, such as name, are now top-level fields in the request body, consistent across all types. + +This approach simplifies client-side logic, as you no longer need to route requests to different endpoints. + +**v1 (Multiple, Type-Specific Endpoints):** + +``` +- AddOIDCApp +- AddSAMLApp +- AddAPIApp +``` + +**v2 (Single Endpoint with Type-Specific Body):** + +``` +- CreateApplication + - ProjectID + - Name + - Type + - OIDC + - SAML + - API +``` \ No newline at end of file diff --git a/docs/docs/apis/openidoauth/claims.md b/docs/docs/apis/openidoauth/claims.md index 4129806aef..b7424aaf1d 100644 --- a/docs/docs/apis/openidoauth/claims.md +++ b/docs/docs/apis/openidoauth/claims.md @@ -110,7 +110,6 @@ ZITADEL reserves some claims to assert certain data. Please check out the [reser | urn:zitadel:iam:org:domain:primary:\{domainname} | `{"urn:zitadel:iam:org:domain:primary": "acme.ch"}` | This claim represents the primary domain of the organization the user belongs to. | | urn:zitadel:iam:org:project:roles | `{"urn:zitadel:iam:org:project:roles": [ {"user": {"id1": "acme.zitade.ch", "id2": "caos.ch"} } ] }` | When roles are asserted, ZITADEL does this by providing the `id` and `primaryDomain` below the role. This gives you the option to check in which organization a user has the role on the current project (where your client belongs to). | | urn:zitadel:iam:org:project:\{projectid}:roles | `{"urn:zitadel:iam:org:project:id3:roles": [ {"user": {"id1": "acme.zitade.ch", "id2": "caos.ch"} } ] }` | When roles are asserted, ZITADEL does this by providing the `id` and `primaryDomain` below the role. This gives you the option to check in which organization a user has the role on a specific project. | -| urn:zitadel:iam:roles:\{rolename} | TBA | TBA | | urn:zitadel:iam:user:metadata | `{"urn:zitadel:iam:user:metadata": [ {"key": "VmFsdWU=" } ] }` | The metadata claim will include all metadata of a user. The values are base64 encoded. | | urn:zitadel:iam:user:resourceowner:id | `{"urn:zitadel:iam:user:resourceowner:id": "orgid"}` | This claim represents the id of the resource owner organisation of the user. | | urn:zitadel:iam:user:resourceowner:name | `{"urn:zitadel:iam:user:resourceowner:name": "ACME"}` | This claim represents the name of the resource owner organisation of the user. | diff --git a/docs/docs/apis/openidoauth/endpoints.mdx b/docs/docs/apis/openidoauth/endpoints.mdx index 13745eaeb1..79d533ab3a 100644 --- a/docs/docs/apis/openidoauth/endpoints.mdx +++ b/docs/docs/apis/openidoauth/endpoints.mdx @@ -656,12 +656,14 @@ The endpoint has to be opened in the user agent (browser) to terminate the user No parameters are needed apart from the user agent cookie, but you can provide the following to customize the behavior: -| Parameter | Description | -| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------- | -| id_token_hint | the id_token that was previously issued to the client | -| client_id | client_id of the application | -| post_logout_redirect_uri | Callback uri of the logout where the user (agent) will be redirected to. Must match exactly one of the preregistered in Console. | -| state | Opaque value used to maintain state between the request and the callback | +| Parameter | Description | +| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| id_token_hint | the id_token that was previously issued to the client | +| client_id | client_id of the application | +| post_logout_redirect_uri | Callback uri of the logout where the user (agent) will be redirected to. Must match exactly one of the preregistered in Console. | +| state | Opaque value used to maintain state between the request and the callback | +| logout_hint | A valid login name of a user. Will be used to select the user to logout. Only supported when using the login UI V2. | +| ui_locales | Spaces delimited list of preferred locales for the login UI, e.g. `de-CH de en`. If none is provided or matches the possible locales provided by the login UI, the `accept-language` header of the browser will be taken into account. | The `post_logout_redirect_uri` will be checked against the previously registered uris of the client provided by the `azp` claim of the `id_token_hint` or the `client_id` parameter. If both parameters are provided, they must be equal. diff --git a/docs/docs/apis/openidoauth/scopes.md b/docs/docs/apis/openidoauth/scopes.md index 263d888f31..d1fe9c7c5b 100644 --- a/docs/docs/apis/openidoauth/scopes.md +++ b/docs/docs/apis/openidoauth/scopes.md @@ -8,7 +8,7 @@ ZITADEL supports the usage of scopes as way of requesting information from the I ## Standard Scopes | Scopes | Description | -|:---------------|--------------------------------------------------------------------------------| +| :------------- | ------------------------------------------------------------------------------ | | openid | When using openid connect this is a mandatory scope | | profile | Optional scope to request the profile of the subject | | email | Optional scope to request the email of the subject | @@ -30,12 +30,9 @@ In addition to the standard compliant scopes we utilize the following scopes. | `urn:zitadel:iam:org:projects:roles` | `urn:zitadel:iam:org:projects:roles` | By using this scope a client can request the claim `urn:zitadel:iam:org:project:{projectid}:roles` to be asserted for each requested project. All projects of the token audience, requested by the `urn:zitadel:iam:org:project:id:{projectid}:aud` scopes will be used. | | `urn:zitadel:iam:org:id:{id}` | `urn:zitadel:iam:org:id:178204173316174381` | When requesting this scope **ZITADEL** will enforce that the user is a member of the selected organization. If the organization does not exist a failure is displayed. It will assert the `urn:zitadel:iam:user:resourceowner` claims. | | `urn:zitadel:iam:org:domain:primary:{domainname}` | `urn:zitadel:iam:org:domain:primary:acme.ch` | When requesting this scope **ZITADEL** will enforce that the user is a member of the selected organization and the username is suffixed by the provided domain. If the organization does not exist a failure is displayed | -| `urn:zitadel:iam:role:{rolename}` | | | -| `urn:zitadel:iam:org:roles:id:{orgID}` | `urn:zitadel:iam:org:roles:id:178204173316174381` | This scope can be used one or more times to limit the granted organization IDs in the returned roles. Unknown organization IDs are ignored. When this scope is not used, all granted organizations are returned inside the roles.[^1] | +| `urn:zitadel:iam:org:roles:id:{orgID}` | `urn:zitadel:iam:org:roles:id:178204173316174381` | This scope can be used one or more times to limit the granted organization IDs in the returned roles. Unknown organization IDs are ignored. When this scope is not used, all granted organizations are returned inside the roles. | | `urn:zitadel:iam:org:project:id:{projectid}:aud` | `urn:zitadel:iam:org:project:id:69234237810729019:aud` | By adding this scope, the requested projectid will be added to the audience of the access token | | `urn:zitadel:iam:org:project:id:zitadel:aud` | `urn:zitadel:iam:org:project:id:zitadel:aud` | By adding this scope, the ZITADEL project ID will be added to the audience of the access token | | `urn:zitadel:iam:user:metadata` | `urn:zitadel:iam:user:metadata` | By adding this scope, the metadata of the user will be included in the token. The values are base64 encoded. | -| `urn:zitadel:iam:user:resourceowner` | `urn:zitadel:iam:user:resourceowner` | By adding this scope: id, name and primary_domain of the resource owner (the users organization) will be included in the token. | +| `urn:zitadel:iam:user:resourceowner` | `urn:zitadel:iam:user:resourceowner` | By adding this scope: id, name and primary_domain of the resource owner (the users organization) will be included in the token. | | `urn:zitadel:iam:org:idp:id:{idp_id}` | `urn:zitadel:iam:org:idp:id:76625965177954913` | By adding this scope the user will directly be redirected to the identity provider to authenticate. Make sure you also send the primary domain scope if a custom login policy is configured. Otherwise the system will not be able to identify the identity provider. | - -[^1]: `urn:zitadel:iam:org:roles:id:{orgID}` is not supported when the `oidcLegacyIntrospection` [feature flag](/docs/apis/resources/feature_service_v2/feature-service-set-instance-features) is enabled. diff --git a/docs/docs/apis/scim2.md b/docs/docs/apis/scim2.md index 10afbb2c5c..d342142cf0 100644 --- a/docs/docs/apis/scim2.md +++ b/docs/docs/apis/scim2.md @@ -2,15 +2,6 @@ title: SCIM v2.0 (Preview) --- -:::info -The SCIM v2 interface of Zitadel is currently in a [preview stage](/support/software-release-cycles-support#preview). -It is not yet feature-complete, may contain bugs, and is not generally available. - -Do not use it for production yet. - -As long as the feature is in a preview state, it will be available for free, it will be put behind a commercial license once it is fully available. -::: - The Zitadel [SCIM v2](https://scim.cloud/) service provider interface enables seamless integration of identity and access management (IAM) systems with Zitadel, following the System for Cross-domain Identity Management (SCIM) v2.0 specification. diff --git a/docs/docs/concepts/architecture/software.md b/docs/docs/concepts/architecture/software.md index dc6f2b56c7..01bacfe12d 100644 --- a/docs/docs/concepts/architecture/software.md +++ b/docs/docs/concepts/architecture/software.md @@ -147,5 +147,5 @@ Zitadel currently supports PostgreSQL. Make sure to read our [Production Guide](/docs/self-hosting/manage/production#prefer-postgresql) before you decide on using one of them. :::info -Zitadel v2 supported CockroachDB and PostgreSQL. Zitadel v3 only supports PostgreSQL. Please refer to [the mirror guide](cli/mirror) to migrate to PostgreSQL. +Zitadel v2 supported CockroachDB and PostgreSQL. Zitadel v3 only supports PostgreSQL. Please refer to [the mirror guide](/docs/self-hosting/manage/cli/mirror) to migrate to PostgreSQL. ::: \ No newline at end of file diff --git a/docs/docs/examples/login/go.md b/docs/docs/examples/login/go.md index d78b41f77f..ae24148952 100644 --- a/docs/docs/examples/login/go.md +++ b/docs/docs/examples/login/go.md @@ -3,12 +3,16 @@ title: ZITADEL with Go sidebar_label: Go --- -This integration guide demonstrates the recommended way to incorporate ZITADEL into your Go web application. +This integration guide demonstrates the recommended way to incorporate ZITADEL into your Go web application. It explains how to enable user login in your application and how to fetch data from the user info endpoint. +> ℹ️ These examples and guides are based on our official [Go SDK](https://github.com/zitadel/zitadel-go). +> +> The SDK is a convenient wrapper around our low-level [OIDC library](https://github.com/zitadel/oidc). For most use cases, using the helpers provided in our [Go SDK](https://github.com/zitadel/zitadel-go) is the recommended approach for implementing authentication. + By the end of this guide, your application will have login functionality and will be able to access the current user's profile. -> This documentation references our [example](https://github.com/zitadel/zitadel-go) on GitHub. +> This documentation references our [example](https://github.com/zitadel/zitadel-go) on GitHub. > You can either create your own application or directly run the example by providing the necessary arguments. ## Set up application @@ -86,7 +90,7 @@ To ensure the user is authenticated before they are able to use your application ```go mw.CheckAuthentication()(handler) ``` - + ***Authentication context*** If you used either of the authentication checks above, you can then access context information in your handler: @@ -119,7 +123,7 @@ https://github.com/zitadel/zitadel-go/blob/next/example/app/templates/profile.ht You will need to provide some values for the program to run: - `domain`: Your ZITADEL instance domain, e.g. my-domain.zitadel.cloud -- `key`: Random secret string. Used for symmetric encryption of state parameters, cookies and PCKE. +- `key`: Random secret string. Used for symmetric encryption of state parameters, cookies and PCKE. - `clientID`: The clientID provided by ZITADEL - `redirectURI`: The redirectURI registered at ZITADEL - `port`: The port on which the API will be accessible, default it 8089 @@ -146,7 +150,7 @@ By clicking on `Login` you will be redirected to your ZITADEL instance. After lo Congratulations! You have successfully integrated your Go application with ZITADEL! -If you get stuck, consider checking out our [example](https://github.com/zitadel/zitadel-go) application. -This application includes all the functionalities mentioned in this quickstart. +If you get stuck, consider checking out our [example](https://github.com/zitadel/zitadel-go) application. +This application includes all the functionalities mentioned in this quickstart. You can directly start it with your own configuration. If you face issues, contact us or raise an issue on [GitHub](https://github.com/zitadel/zitadel-go/issues). diff --git a/docs/docs/examples/secure-api/go.md b/docs/docs/examples/secure-api/go.md index ab45e2a6b7..7c1db14cf2 100644 --- a/docs/docs/examples/secure-api/go.md +++ b/docs/docs/examples/secure-api/go.md @@ -6,30 +6,71 @@ sidebar_label: Go This integration guide shows you how to integrate **ZITADEL** into your Go API. It demonstrates how to secure your API using OAuth 2 Token Introspection. +> ℹ️ These examples and guides are based on our official [Go SDK](https://github.com/zitadel/zitadel-go). +> +> The SDK is a convenient wrapper around our low-level [OIDC library](https://github.com/zitadel/oidc). For most use cases, using the helpers provided in our [Go SDK](https://github.com/zitadel/zitadel-go) is the recommended approach for implementing authentication. + At the end of the guide you should have an API with a protected endpoint. > This documentation references our HTTP example. There's also one for GRPC. Check them out on [GitHub](https://github.com/zitadel/zitadel-go/blob/next/example/api/http/main.go). -## Set up application and obtain keys - -Before we begin developing our API, we need to perform a few configuration steps in the ZITADEL Console. -You'll need to provide some information about your app. We recommend creating a new app to start from scratch. Navigate to your Project, then add a new application at the top of the page. -Select the **API** application type and continue. - -![Create app in console](/img/go/api-create.png) - -We recommend that you use JWT Profile for authenticating at the Introspection Endpoint. - -![Create app in console](/img/go/api-create-auth.png) - -Then create a new key with your desired expiration date. Be sure to download it, as you won't be able to retrieve it again. - -![Create api key in console](/img/go/api-create-key.png) - ## Prerequisites -This will handle the OAuth 2.0 introspection request including authentication using JWT with Private Key using our [OIDC client library](https://github.com/zitadel/oidc). -All that is required, is to create your API and download the private key file later called `Key JSON` for the service user. +This will handle the OAuth 2.0 introspection request including authentication using JWT with Private Key using our [Go SDK](https://github.com/zitadel/zitadel-go). +All that is required, is to create your API, create a private key and a personal access token for a service user. + +### Set up application and obtain keys + +Before we begin developing our API, we need to perform a few configuration steps in the ZITADEL Console. +You'll need to provide some information about your app. We recommend creating a new app to start from scratch. + +Starting from the homepage of your console, click on Create Application + +![Create app in homepage](/img/go/api-create_application.png) + +Select a project from the dropdown and select *Other* as framework, then continue. + +![Framework Selection](/img/go/api-select_framework.png) + +Add your app name and select *API* as application type, then continue. + +![Application Type](/img/go/api-app_details.png) + +We recommend that you use JWT Profile for authenticating at the Introspection Endpoint. So select *JWT* as authentication method + +![JWT authentication method](/img/go/api-select_jwt.png) + +You then need to create a new JSON key. + +![New JSON key](/img/go/api-new_key.png) + +Select an expiration date that suits you. + +![Key expiration date](/img/go/api-expiration_date.png) + +And make sure to download it, as you won't be able to retrieve it again. + +![Key download](/img/go/api-download_key.png) + +Now we need to create a *Personal Access Token* to authenticate the client requests. + +On the user view, switch to *Service Users* and create a new one. + +![Service User Panel](/img/go/api-service_user_panel.png) + +Give the service user a name and a user name. Select `Bearer` as *Access Token Type*. + +![Service User Creation](/img/go/api-create_service_user.png) + +### Create service user and personal access token (PAT) + +Once done, from the left panel of the user management, click on Personal Access Token and create a new one. + +![Personal Access Token View](/img/go/api-PAT_view.png) + +Set an expiration date and then copy the PAT generated to somewhere safe. We will need it later. + +![PAT creation](/img/go/api-PAT_creation.png) ## Go Setup @@ -88,7 +129,7 @@ Now you can call the API by browser or curl. Try the healthz endpoint first: curl -i http://localhost:8089/api/healthz ``` -it should return something like: +it should return something like: ``` HTTP/1.1 200 OK @@ -119,8 +160,7 @@ Content-Length: 44 unauthorized: authorization header is empty ``` -Get a valid access_token for the API. You can either achieve this by getting an access token with the project_id in the audience -or use a PAT of a service account. +We need to use the personal access token generated previously. If you provide a valid Bearer Token: diff --git a/docs/docs/guides/integrate/actions/testing-event.md b/docs/docs/guides/integrate/actions/testing-event.md index 8b4502703b..7c8bdd8c2b 100644 --- a/docs/docs/guides/integrate/actions/testing-event.md +++ b/docs/docs/guides/integrate/actions/testing-event.md @@ -103,9 +103,7 @@ curl -L -X PUT 'https://$CUSTOM-DOMAIN/v2beta/actions/executions' \ } }, "targets": [ - { - "target": "" - } + "" ] }' ``` diff --git a/docs/docs/guides/integrate/actions/testing-function-manipulation.md b/docs/docs/guides/integrate/actions/testing-function-manipulation.md index 8f5e2fc968..e82f0b8c18 100644 --- a/docs/docs/guides/integrate/actions/testing-function-manipulation.md +++ b/docs/docs/guides/integrate/actions/testing-function-manipulation.md @@ -129,9 +129,7 @@ curl -L -X PUT 'https://$CUSTOM-DOMAIN/v2beta/actions/executions' \ } }, "targets": [ - { - "target": "" - } + "" ] }' ``` diff --git a/docs/docs/guides/integrate/actions/testing-function.md b/docs/docs/guides/integrate/actions/testing-function.md index f14f20b69d..a2faa5e709 100644 --- a/docs/docs/guides/integrate/actions/testing-function.md +++ b/docs/docs/guides/integrate/actions/testing-function.md @@ -107,9 +107,7 @@ curl -L -X PUT 'https://$CUSTOM-DOMAIN/v2beta/actions/executions' \ } }, "targets": [ - { - "target": "" - } + "" ] }' ``` diff --git a/docs/docs/guides/integrate/actions/testing-request-manipulation.md b/docs/docs/guides/integrate/actions/testing-request-manipulation.md index 1cb4f1776a..b10c32d372 100644 --- a/docs/docs/guides/integrate/actions/testing-request-manipulation.md +++ b/docs/docs/guides/integrate/actions/testing-request-manipulation.md @@ -154,9 +154,7 @@ curl -L -X PUT 'https://$CUSTOM-DOMAIN/v2beta/actions/executions' \ } }, "targets": [ - { - "target": "" - } + "" ] }' ``` diff --git a/docs/docs/guides/integrate/actions/testing-request-signature.md b/docs/docs/guides/integrate/actions/testing-request-signature.md index c1932a7d5b..b3a9f0fa5d 100644 --- a/docs/docs/guides/integrate/actions/testing-request-signature.md +++ b/docs/docs/guides/integrate/actions/testing-request-signature.md @@ -114,9 +114,7 @@ curl -L -X PUT 'https://$CUSTOM-DOMAIN/v2beta/actions/executions' \ } }, "targets": [ - { - "target": "" - } + "" ] }' ``` diff --git a/docs/docs/guides/integrate/actions/testing-request.md b/docs/docs/guides/integrate/actions/testing-request.md index b2413e606e..c99e16cd2f 100644 --- a/docs/docs/guides/integrate/actions/testing-request.md +++ b/docs/docs/guides/integrate/actions/testing-request.md @@ -107,9 +107,7 @@ curl -L -X PUT 'https://$CUSTOM-DOMAIN/v2beta/actions/executions' \ } }, "targets": [ - { - "target": "" - } + "" ] }' ``` diff --git a/docs/docs/guides/integrate/actions/testing-response-manipulation.md b/docs/docs/guides/integrate/actions/testing-response-manipulation.md index 9d95479b05..2bec3e0acd 100644 --- a/docs/docs/guides/integrate/actions/testing-response-manipulation.md +++ b/docs/docs/guides/integrate/actions/testing-response-manipulation.md @@ -173,9 +173,7 @@ curl -L -X PUT 'https://$CUSTOM-DOMAIN/v2beta/actions/executions' \ } }, "targets": [ - { - "target": "" - } + "" ] }' ``` diff --git a/docs/docs/guides/integrate/actions/testing-response.md b/docs/docs/guides/integrate/actions/testing-response.md index a2ab736505..aea6ac732f 100644 --- a/docs/docs/guides/integrate/actions/testing-response.md +++ b/docs/docs/guides/integrate/actions/testing-response.md @@ -107,9 +107,7 @@ curl -L -X PUT 'https://$CUSTOM-DOMAIN/v2beta/actions/executions' \ } }, "targets": [ - { - "target": "" - } + "" ] }' ``` diff --git a/docs/docs/guides/integrate/actions/usage.md b/docs/docs/guides/integrate/actions/usage.md index ba512ae549..643c9a3995 100644 --- a/docs/docs/guides/integrate/actions/usage.md +++ b/docs/docs/guides/integrate/actions/usage.md @@ -371,7 +371,7 @@ The API documentation to create a target can be found [here](/apis/resources/act To ensure the integrity of request content, each call includes a 'ZITADEL-Signature' in the headers. This header contains an HMAC value computed from the request content and a timestamp, which can be used to time out requests. The logic for this process is provided in 'pkg/actions/signing.go'. The goal is to verify that the HMAC value in the header matches the HMAC value computed by the Target, ensuring that the sent and received requests are identical. Each Target resource now contains also a Signing Key, which gets generated and returned when a Target is [created](/apis/resources/action_service_v2/action-service-create-target), -and can also be newly generated when a Target is [patched](/apis/resources/action_service_v2/action-service-patch-target). +and can also be newly generated when a Target is [patched](/apis/resources/action_service_v2/action-service-update-target). For an example on how to check the signature, [refer to the example](/guides/integrate/actions/testing-request-signature). @@ -406,17 +406,11 @@ If you then have a call on `/zitadel.user.v2.UserService/UpdateHumanUser` the fo And if you use a different service, for example `zitadel.session.v2.SessionService`, then the `all` Execution would still be used. -### Targets and Includes +### Targets -:::info -Includes are limited to 3 levels, which mean that include1->include2->include3 is the maximum for now. -If you have feedback to the include logic, or a reason why 3 levels are not enough, please open [an issue on github](https://github.com/zitadel/zitadel/issues) or [start a discussion on github](https://github.com/zitadel/zitadel/discussions)/[start a topic on discord](https://zitadel.com/chat) -::: +An execution can contain only a list of Targets, and Targets are comma separated string values. -An execution can not only contain a list of Targets, but also Includes. -The Includes can be defined in the Execution directly, which means you include all defined Targets by a before set Execution. - -If you define 2 Executions as follows: +Here's an example of a Target defined on a service (e.g. `zitadel.user.v2.UserService`) ```json { @@ -426,13 +420,12 @@ If you define 2 Executions as follows: } }, "targets": [ - { - "target": "" - } + "" ] } ``` +Here's an example of a Target defined on a method (e.g. `/zitadel.user.v2.UserService/AddHumanUser`) ```json { "condition": { @@ -441,21 +434,13 @@ If you define 2 Executions as follows: } }, "targets": [ - { - "target": "" - }, - { - "include": { - "request": { - "service": "zitadel.user.v2.UserService" - } - } - } + "", + "" ] } ``` -The called Targets on "/zitadel.user.v2.UserService/AddHumanUser" would be, in order: +The called Targets on `/zitadel.user.v2.UserService/AddHumanUser` would be, in order: 1. `` 2. `` diff --git a/docs/docs/guides/integrate/login-ui/device-auth.mdx b/docs/docs/guides/integrate/login-ui/device-auth.mdx index f60fad1310..32984a17ff 100644 --- a/docs/docs/guides/integrate/login-ui/device-auth.mdx +++ b/docs/docs/guides/integrate/login-ui/device-auth.mdx @@ -102,7 +102,7 @@ Present the user with the information of the device authorization request and al ### Perform Login After you have initialized the OIDC flow you can implement the login. -Implement all the steps you like the user the go trough by [creating](/docs/apis/resources/session_service_v2/session-service-create-session) and [updating](/docs/apis/resources/session_service/session-service-set-session) the user-session. +Implement all the steps you like the user the go trough by [creating](/docs/apis/resources/session_service_v2/session-service-create-session) and [updating](/docs/apis/resources/session_service_v2/session-service-set-session) the user-session. Read the following resources for more information about the different checks: - [Username and Password](./username-password) @@ -117,7 +117,7 @@ On the create and update user session request you will always get a session toke The latest session token has to be sent to the following request: -Read more about the [Authorize or Deny Device Authorization Request Documentation](/docs/apis/resources/oidc_service_v2/oidc-service-authorize-device-authorization) +Read more about the [Authorize or Deny Device Authorization Request Documentation](/docs/apis/resources/oidc_service_v2/oidc-service-authorize-or-deny-device-authorization) Make sure that the authorization header is from an account which is permitted to finalize the Auth Request through the `IAM_LOGIN_CLIENT` role. ```bash diff --git a/docs/docs/guides/integrate/login-ui/external-login.mdx b/docs/docs/guides/integrate/login-ui/external-login.mdx index 3b3c47cf18..6775d2cb3b 100644 --- a/docs/docs/guides/integrate/login-ui/external-login.mdx +++ b/docs/docs/guides/integrate/login-ui/external-login.mdx @@ -16,6 +16,7 @@ ZITADEL will handle as much as possible from the authentication flow with the ex This requires you to initiate the flow with your desired provider. Send the following two URLs in the request body: + 1. SuccessURL: Page that should be shown when the login was successful 2. ErrorURL: Page that should be shown when an error happens during the authentication @@ -63,6 +64,10 @@ https://accounts.google.com/o/oauth2/v2/auth?client_id=Test&prompt=select_accoun After the user has successfully authenticated, a redirect to the ZITADEL backend /idps/callback will automatically be performed. +:::warning +Note that the redirect URL is `https://{YOUR-DOMAIN}/idps/callback` when using the new V2 hosted login compared to the V1 hosted login, which was `https://{YOUR-DOMAIN}/ui/login/login/externalidp/callback`. +::: + ## Get Provider Information ZITADEL will take the information of the provider. After this, a redirect will be made to either the success page in case of a successful login or to the error page in case of a failure will be performed. In the parameters, you will provide the IDP intentID, a token, and optionally, if a user could be found, a user ID. @@ -71,6 +76,7 @@ To get the information of the provider, make a request to ZITADEL. [Retrieve Identity Provider Intent Documentation](/docs/apis/resources/user_service_v2/user-service-retrieve-identity-provider-intent) ### Request + ```bash curl --request POST \ --url https://$ZITADEL_DOMAIN/v2/idp_intents/$INTENT_ID \ @@ -115,7 +121,9 @@ curl --request POST \ ``` ## Handle Provider Information + After successfully authenticating using your identity provider, you have three possible options. + 1. Login 2. Register user 3. Add social login to existing user @@ -127,6 +135,7 @@ Create a new session and include the IDP intent ID and the token in the checks. This check requires that the previous step ended on the successful page and didn't’t result in an error. #### Request + ```bash curl --request POST \ --url https://$ZITADEL_DOMAIN/v2/sessions \ @@ -158,6 +167,7 @@ The display name is used to list the linkings on the users. [Create User API Documentation](/docs/apis/resources/user_service_v2/user-service-add-human-user) #### Request + ```bash curl --request POST \ --url https://$ZITADEL_DOMAIN/v2/users/human \ @@ -196,6 +206,7 @@ If you want to link/connect to an existing account you can perform the add ident [Add IDP Link to existing user documentation](/docs/apis/resources/user_service_v2/user-service-add-idp-link) #### Request + ```bash curl --request POST \ --url https://$ZITADEL_DOMAIN/v2/users/users/218385419895570689/links \ diff --git a/docs/docs/guides/integrate/login-ui/oidc-standard.mdx b/docs/docs/guides/integrate/login-ui/oidc-standard.mdx index c96338fbf0..92068e5116 100644 --- a/docs/docs/guides/integrate/login-ui/oidc-standard.mdx +++ b/docs/docs/guides/integrate/login-ui/oidc-standard.mdx @@ -80,7 +80,7 @@ Response Example: ### Perform Login After you have initialized the OIDC flow you can implement the login. -Implement all the steps you like the user the go trough by [creating](/docs/apis/resources/session_service_v2/session-service-create-session) and [updating](/docs/apis/resources/session_service/session-service-set-session) the user-session. +Implement all the steps you like the user the go trough by [creating](/docs/apis/resources/session_service_v2/session-service-create-session) and [updating](/docs/apis/resources/session_service_v2/session-service-set-session) the user-session. Read the following resources for more information about the different checks: - [Username and Password](./username-password) diff --git a/docs/docs/guides/integrate/login-ui/saml-standard.mdx b/docs/docs/guides/integrate/login-ui/saml-standard.mdx index 8114350d5d..5196f6c81a 100644 --- a/docs/docs/guides/integrate/login-ui/saml-standard.mdx +++ b/docs/docs/guides/integrate/login-ui/saml-standard.mdx @@ -77,7 +77,7 @@ Response Example: ### Perform Login After you have initialized the SAML flow you can implement the login. -Implement all the steps you like the user to go trough by [creating](/docs/apis/resources/session_service_v2/session-service-create-session) and [updating](/docs/apis/resources/session_service/session-service-set-session) the user-session. +Implement all the steps you like the user to go trough by [creating](/docs/apis/resources/session_service_v2/session-service-create-session) and [updating](/docs/apis/resources/session_service_v2/session-service-set-session) the user-session. Read the following resources for more information about the different checks: - [Username and Password](./username-password) diff --git a/docs/docs/guides/integrate/login/login-users.mdx b/docs/docs/guides/integrate/login/login-users.mdx index de1dacfe24..13439ac1db 100644 --- a/docs/docs/guides/integrate/login/login-users.mdx +++ b/docs/docs/guides/integrate/login/login-users.mdx @@ -25,7 +25,7 @@ The identity provider is not part of the original application, but a standalone The user will authenticate using their credentials. After successful authentication, the user will be redirected back to the original application. -If you want to read more about authenticating with OIDC, head over to our comprehensive [OpenID Connect Guide](/docs/integrate/login/oidc). +If you want to read more about authenticating with OIDC, head over to our comprehensive [OpenID Connect Guide](/docs/guides/integrate/login/oidc). ### Authenticate users with SAML @@ -54,7 +54,7 @@ Note that SAML might not be suitable for mobile applications. In case you want to integrate a mobile application, use OpenID Connect or our Session API. There are more [differences between SAML and OIDC](https://zitadel.com/blog/saml-vs-oidc) that you might want to consider. -If you want to read more about authenticating with SAML, head over to our comprehensive [SAML Guide](/docs/integrate/login/saml). +If you want to read more about authenticating with SAML, head over to our comprehensive [SAML Guide](/docs/guides/integrate/login/saml). ## ZITADEL's Session API diff --git a/docs/docs/guides/integrate/login/oidc/webkeys.md b/docs/docs/guides/integrate/login/oidc/webkeys.md index a66cae61a9..288284fefc 100644 --- a/docs/docs/guides/integrate/login/oidc/webkeys.md +++ b/docs/docs/guides/integrate/login/oidc/webkeys.md @@ -20,13 +20,6 @@ JWT access tokens, instead of [introspection](/docs/apis/openidoauth/endpoints#i ZITADEL uses public key verification when API calls are made or when the userInfo or introspection endpoints are called with a JWT access token. -:::info -Web keys are an [experimental](/docs/support/software-release-cycles-support#beta) feature. Be sure to enable the `web_key` [feature](/docs/apis/resources/feature_service_v2/feature-service-set-instance-features) before using it. - -The documentation describes the state of the feature in ZITADEL V3. -Test the feature and add improvement or bug reports directly to the [github repository](https://github.com/zitadel/zitadel) or let us know your general feedback in the [discord thread](https://discord.com/channels/927474939156643850/1329100936127320175/threads/1332344892629717075)! -::: - ### JSON Web Key ZITADEL implements the [RFC7517 - JSON Web Key (JWK)](https://www.rfc-editor.org/rfc/rfc7517) format for storage and distribution of public keys. @@ -85,7 +78,7 @@ The same counts for [zitadel/oidc](https://github.com/zitadel/oidc) Go library. ## Web Key management -ZITADEL provides a resource based [web keys API](/docs/apis/resources/webkey_service_v3). +ZITADEL provides a resource based [web keys API](/docs/apis/resources/webkey_service_v2). The API allows the creation, activation, deletion and listing of web keys. All public keys that are stored for an instance are served on the [JWKS endpoint](#json-web-key-set). Applications need public keys for token verification and not all applications are capable of on-demand diff --git a/docs/docs/guides/integrate/zitadel-apis/example-zitadel-api-with-go.md b/docs/docs/guides/integrate/zitadel-apis/example-zitadel-api-with-go.md index 3fdc3e8490..ef3f1739e5 100644 --- a/docs/docs/guides/integrate/zitadel-apis/example-zitadel-api-with-go.md +++ b/docs/docs/guides/integrate/zitadel-apis/example-zitadel-api-with-go.md @@ -6,13 +6,17 @@ sidebar_label: Example Go This integration guide shows you how to integrate **ZITADEL** into your Go application. It demonstrates how to fetch some data from the ZITADEL management API. +> ℹ️ These examples and guides are based on our official [Go SDK](https://github.com/zitadel/zitadel-go). +> +> The SDK is a convenient wrapper around our low-level [OIDC library](https://github.com/zitadel/oidc). For most use cases, using the helpers provided in our [Go SDK](https://github.com/zitadel/zitadel-go) is the recommended approach for implementing authentication. + At the end of the guide you should have an application able to read the details of your organization. > This documentation references our [CLI example](https://github.com/zitadel/zitadel-go/blob/next/example/client/cli/cli.go). ## Prerequisites -The client [SDK](https://github.com/zitadel/zitadel-go) will handle all necessary OAuth 2.0 requests and send the required headers to the ZITADEL API using our [OIDC client library](https://github.com/zitadel/oidc). +The client [SDK](https://github.com/zitadel/zitadel-go) will handle all necessary OAuth 2.0 requests and send the required headers to the ZITADEL API using our [Go SDK](https://github.com/zitadel/zitadel-go). All that is required, is a service account with an Org Owner (or another role, depending on the needed api requests) role assigned and its key JSON. However, we recommend you read the guide on [how to access ZITADEL API](/docs/guides/integrate/zitadel-apis/access-zitadel-apis)) and the associated guides for a basic knowledge of : @@ -64,7 +68,7 @@ This will output something similar to: You have successfully used the ZITADEL Go SDK to call the management API! -If you encountered an error (e.g. `code = PermissionDenied desc = No matching permissions found`), +If you encountered an error (e.g. `code = PermissionDenied desc = No matching permissions found`), ensure your service user has the required permissions by assigning the `ORG_OWNER` or `ORG_OWNER_VIEWER` role and check the mentioned [guides](#prerequisites) at the beginning. diff --git a/docs/docs/guides/manage/console/default-settings.mdx b/docs/docs/guides/manage/console/default-settings.mdx index e8e36956a1..f255d15d93 100644 --- a/docs/docs/guides/manage/console/default-settings.mdx +++ b/docs/docs/guides/manage/console/default-settings.mdx @@ -17,7 +17,7 @@ When you configure your default settings, you can set the following: - **Organizations**: A list of your organizations - [**Features**](#features): Feature Settings let you try out new features before they become generally available. You can also disable features you are not interested in. -- [**Notification settings**](#notification-providers-and-smtp): Setup Notification and Email Server settings for initialization-, verification- and other mails. Setup Twilio as SMS notification provider. +- [**Notification settings**](#notification-settings): Setup Notification and Email Server settings for initialization-, verification- and other mails. Setup Twilio as SMS notification provider. - [**Login Behavior and Access**](#login-behavior-and-access): Multifactor Authentication Options and Enforcement, Define whether Passwordless authentication methods are allowed or not, Set Login Lifetimes and advanced behavour for the login interface. - [**Identity Providers**](#identity-providers): Define IDPs which are available for all organizations - [**Password Complexity**](#password-complexity): Requirements for Passwords ex. Symbols, Numbers, min length and more. diff --git a/docs/docs/guides/manage/customize/branding.md b/docs/docs/guides/manage/customize/branding.md index 14c18705f6..c5ec0a8838 100644 --- a/docs/docs/guides/manage/customize/branding.md +++ b/docs/docs/guides/manage/customize/branding.md @@ -46,7 +46,7 @@ If you like to trigger your settings for your applications you have different po Send a [reserved scope](/apis/openidoauth/scopes) with your [authorization request](../../integrate/login/oidc/login-users#auth-request) to trigger your organization. The primary domain scope will restrict the login to your organization, so only users of your own organization will be able to login. -You can use our [OpenID Authentication Request Playground](/oidc-playground) to learn more about how to trigger an [organization's policies and branding](/oidc-playground#organization-policies-and-branding). +You can use our [OpenID Authentication Request Playground](https://zitadel.com/playgrounds/oidc) to learn more about how to trigger an [organization's policies and branding](https://zitadel.com/playgrounds/oidc#organization-policies-and-branding). ### 2. Setting on your Project diff --git a/docs/docs/guides/manage/customize/notification-providers.mdx b/docs/docs/guides/manage/customize/notification-providers.mdx index b94ce7542d..3bd1b358e5 100644 --- a/docs/docs/guides/manage/customize/notification-providers.mdx +++ b/docs/docs/guides/manage/customize/notification-providers.mdx @@ -74,7 +74,7 @@ A provider with HTTP type will send the messages and the data to a pre-defined w - First [add a new Email Provider of type HTTP](/apis/resources/admin/admin-service-add-email-provider-http) to create a new HTTP provider that can be used to send SMS messages: + First [add a new Email Provider of type HTTP](/apis/resources/admin/admin-service-add-email-provider-http) to create a new HTTP provider that can be used to send Email messages: ```bash curl -L 'https://$CUSTOM-DOMAIN/admin/v1/email/http' \ diff --git a/docs/docs/guides/manage/customize/texts.md b/docs/docs/guides/manage/customize/texts.md index bd3cb6ea90..80e1db1308 100644 --- a/docs/docs/guides/manage/customize/texts.md +++ b/docs/docs/guides/manage/customize/texts.md @@ -53,6 +53,7 @@ ZITADEL is available in the following languages - Hungarian (hu) - 한국어 (ko) - Romanian (ro) +- Turkish (tr) A language is displayed based on your agent's language header. If a users language header doesn't match any of the supported or [restricted](#restrict-languages) languages, the instances default language will be used. diff --git a/docs/docs/guides/manage/user/scim2.md b/docs/docs/guides/manage/user/scim2.md index edf5e7bd10..2d9b90c681 100644 --- a/docs/docs/guides/manage/user/scim2.md +++ b/docs/docs/guides/manage/user/scim2.md @@ -2,15 +2,6 @@ title: SCIM v2.0 (Preview) --- -:::info -The SCIM v2 interface of Zitadel is currently in a [preview stage](/support/software-release-cycles-support#preview). -It is not yet feature-complete, may contain bugs, and is not generally available. - -Do not use it for production yet. - -As long as the feature is in a preview state, it will be available for free, it will be put behind a commercial license once it is fully available. -::: - The Zitadel [SCIM v2](https://scim.cloud/) service provider interface enables seamless integration of identity and access management (IAM) systems with Zitadel, following the System for Cross-domain Identity Management (SCIM) v2.0 specification. diff --git a/docs/docs/guides/migrate/sources/auth0-guide.md b/docs/docs/guides/migrate/sources/auth0-guide.md new file mode 100644 index 0000000000..f8a62b75fa --- /dev/null +++ b/docs/docs/guides/migrate/sources/auth0-guide.md @@ -0,0 +1,185 @@ +--- +title: Migrating Users from Auth0 to ZITADEL (Including Password Hashes) +sidebar_label: Auth0 Migration Guide +--- + +## 1. Introduction + +This guide will walk you through the steps to migrate users from Auth0 to ZITADEL, including password hashes (which requires Auth0's support assistance), so users don't need to reset their passwords. + +**What you'll learn with this guide** +- How to prepare your data from Auth0 +- Use of the ZITADEL migration tooling +- Performing the user import via ZITADEL's API +- Troubleshooting and validating the migration + +--- + +## 2. Prerequisites + +### 2.1. Install Go +The migration tool is written in Go. Download and install the latest version of Go from the [official Go website](https://go.dev/doc/install). + +### 2.2. Create a ZITADEL Instance and Organization +You'll need a target organization in ZITADEL to import your users. You can create a new organization or use an existing one. + +If you don't have a ZITADEL instance, you can [sign up for free here](https://zitadel.com) to create a new one for you. +See: [Managing Organizations in ZITADEL](https://zitadel.com/docs/guides/manage/console/organizations). + +> **Note:** Copy your Organization ID (Resource ID) since you will use the id in the later steps. + +--- + +## 3. Preparing Auth0 Data + +### 3.1. Export User Profiles and Password Hashes from Auth0 +You cannot bulk export user data from the Auth0 Dashboard. Instead, use the [Auth0 Management API](https://auth0.com/docs/manage-users/user-migration#bulk-user-exports) or the [User Import/Export extension](https://auth0.com/docs/manage-users/user-migration/user-import-export-extension). + +> **Important:** Password hashes cannot be obtained in a self-service way. +> You must open a **support ticket** with Auth0 and request a password hash export. +> If approved, Auth0 will provide an export containing the password hashes. + +Reference: [Export hashed passwords from Auth0](https://zitadel.com/docs/guides/migrate/sources/auth0#export-hashed-passwords) + +--- + +## 4. Running the ZITADEL Migration Tool + +### 4.1. Install the Migration Tool +Follow the installation instructions to set up the ZITADEL migration tool from [ZITADEL Tools](https://github.com/zitadel/zitadel-tools?tab=readme-ov-file#installation). + +### 4.2. Generate Import JSON +Use the migration tool to convert the Auth0 export file to a ZITADEL-compatible JSON. +Step-by-step instructions: [Migration Tool for Auth0](https://github.com/zitadel/zitadel-tools/blob/main/cmd/migration/auth0/readme.md) + +Typical steps: +- Run the migration tool with your exported Auth0 files as input. +- The tool generates a JSON file ready for import into ZITADEL. + +Example: +After obtaining the 2 required input files (passwords and profile) in JSON lines format, you can run the following command: + +Sample `passwords.ndjson` content, as obtained from the Auth0 Support team: +```json +{"_id":{"$oid":"emxdpVxozXeFb1HeEn5ThAK8"},"email_verified":true,"email":"tommie_krajcik85@hotmail.com","passwordHash":"$2b$10$d.GvZhGwTllA7OdAmsA75uGGzqr/mhdQoU88M3zD.fX3Vb8Rcf33.","password_set_date":{"$date":"2025-06-30T00:00:00.000Z"},"tenant":"test","connection":"Username-Password-Authentication","_tmp_is_unique":true} +``` + +Sample `profiles.json` content, as obtained from the Auth0 Management API: +```json +{"user_id":"auth0|emxdpVxozXeFb1HeEn5ThAK8","email_verified":true,"name":"Tommie Krajcik","email":"tommie_krajcik85@hotmail.com"} +``` + +Run the following command in your terminal (replace ORG_ID with your own organization ID): +```bash +zitadel-tools migrate auth0 --org= --users=./profiles.json --passwords=./passwords.ndjson --multiline --email-verified --output=./importBody.json --timeout=5m0s +``` + +The tool will merge both objects into a single one in the importBody.json output, this will be used in the next step to complete the import process. + +## 5. Importing Users into ZITADEL + +### 5.1. Obtain Access Token (or PAT) for API Access + +To call the ZITADEL Admin API, you need to authenticate using a **Service User** with the `IAM_OWNER` Manager permissions. + +There are two recommended authentication methods: + +- **Client Credentials Flow** + [Learn how to authenticate with client credentials.](https://zitadel.com/docs/guides/integrate/service-users/client-credentials) + +- **Personal Access Token (PAT)** + [Learn how to create and use a PAT.](https://zitadel.com/docs/guides/integrate/service-users/authenticate-service-users#personal-access-token) + +**Reference:** [Service Users & API Authentication](https://zitadel.com/docs/guides/integrate/service-users/authenticate-service-users#authentication-methods) + +--- + +### 5.2. Import Data with the ZITADEL API + +- Use your **access token** or **PAT** to authenticate. +- Call the [Admin API – Import Data](https://zitadel.com/docs/apis/resources/admin/admin-service-import-data) endpoint, passing your generated JSON file. +- Verify that the users were imported successfully in the ZITADEL console. + +**Import Endpoint:** + +- `POST /admin/v1/import` +- `Authorization: Bearer ` +- **Body:** Generated in step 4.2 + +#### Example cURL request + +```bash +curl --location 'https:///admin/v1/import' \ +--header 'Content-Type: application/json' \ +--header 'Accept: application/json' \ +--header 'Authorization: Bearer ' \ +--data-raw '{ + "dataOrgs": { + "orgs": [ + { + "orgId": "", + "humanUsers": [ + { + "userId": "auth0|emxdpVxozXeFb1HeEn5ThAK8", + "user": { + "userName": "tommie_krajcik85@hotmail.com", + "profile": { + "firstName": "Tommie Krajcik", + "lastName": "Tommie Krajcik" + }, + "email": { + "email": "tommie_krajcik85@hotmail.com", + "isEmailVerified": true + }, + "hashedPassword": { + "value": "$2b$10$d.GvZhGwTllA7OdAmsA75uGGzqr/mhdQoU88M3zD.fX3Vb8Rcf33." + } + } + } + ] + } + ] + }, + "timeout": "5m0s" +}' +``` + +## 6. Testing the Migration + +### 6.1. Test User Login + +Use the **ZITADEL login page** or your integrated app to test logging in with one of the imported users. + +> **Password for the sample user:** `Password1!` + +Confirm that the migrated password works as expected. + +--- + +### 6.2. Troubleshooting + +**Common issues:** + +- Missing password hashes +- Malformed JSON +- Invalid or incomplete user data + +The import endpoint returns an `errors` array which can help you identify any issues with the import. + +#### Where to check logs and get help + +You can also verify that a user was imported by calling the **events endpoint** and checking for the following event type: + +```json +"user.human.added" +``` + +## 7. Q&A and Further Resources + +### Real-World Scenarios & Common Questions + +**Q:** What is the maximum number of users that can be imported in a single batch? +**A:** There is no hard limit on the number of users. However, there is a **timeout**. +For **ZITADEL Cloud deployments**, the timeout is **5 minutes**, which typically allows for importing around **5,000 users per batch**. + +--- \ No newline at end of file diff --git a/docs/docs/guides/migrate/sources/keycloak-guide.md b/docs/docs/guides/migrate/sources/keycloak-guide.md new file mode 100644 index 0000000000..597fbf8ae7 --- /dev/null +++ b/docs/docs/guides/migrate/sources/keycloak-guide.md @@ -0,0 +1,253 @@ +--- + +title: Migrating Users from Keycloak to ZITADEL (Including Password Hashes) +sidebar_label: Keycloak Migration Guide +--- + +## 1. Introduction + +This guide will walk you through the steps to migrate users from **Keycloak** to **ZITADEL**, including password hashes, using the `zitadel-tools` CLI and the user import APIs. + +**What you'll learn with this guide** + +* How to export users from Keycloak +* Use of the ZITADEL migration tooling +* Performing the user import via ZITADEL's API +* Troubleshooting and validating the migration + +--- + +## 2. Prerequisites + +### 2.1. Install Go + +The migration tool is written in Go. Download and install the latest version of Go from the [official Go website](https://go.dev/doc/install). + +### 2.2. Create a ZITADEL Instance and Organization + +You'll need a target organization in ZITADEL to import your users. You can create a new organization or use an existing one. + +If you don't have a ZITADEL instance, you can [sign up for free here](https://zitadel.com) to create a new one for you. +See: [Managing Organizations in ZITADEL](https://zitadel.com/docs/guides/manage/console/organizations). + +> **Note:** Copy your Organization ID (Resource ID) since you will use the id in the later steps. + +--- + +## 3. Exporting User Data from Keycloak + +### 3.1. Set up Keycloak Locally (Optional) + +To run a local development Keycloak instance, use the official Docker image: + +```bash +docker run -d -p 8081:8080 \ + -e KEYCLOAK_ADMIN=admin \ + -e KEYCLOAK_ADMIN_PASSWORD=admin \ + quay.io/keycloak/keycloak:22.0.1 start-dev +``` + +### 3.2. Export Users from Keycloak + +Run the following command inside the Keycloak container to export your realm and users: + +```bash +docker exec \ + /opt/keycloak/bin/kc.sh export \ + --dir /tmp/export \ + --realm \ + --users realm_file +``` + +Then copy the exported file to your host machine: + +```bash +docker cp :/tmp/export/-realm.json . +``` + +This creates a file such as: + +``` +-realm.json +``` + +--- + +## 4. Running the ZITADEL Migration Tool + +### 4.1. Install the Migration Tool + +Follow the installation instructions to set up the ZITADEL migration tool from [ZITADEL Tools](https://github.com/zitadel/zitadel-tools?tab=readme-ov-file#installation). + +### 4.2. Generate Import JSON + +Use the migration tool to convert the Keycloak realm export into a ZITADEL-compatible JSON file: + +```bash +zitadel-tools migrate keycloak \ + --org= \ + --realm=-realm.json \ + --output=./importBody.json \ + --timeout=5m0s \ + --multiline +``` + +The tool will generate `importBody.json`, which is ready for importing into ZITADEL. + +--- + +## 5. Importing Users into ZITADEL + +### 5.1. Obtain Access Token (or PAT) for API Access + +To call the ZITADEL Management API, you need to authenticate using a **Service User** with the `IAM_OWNER` Manager permissions. + +There are two recommended authentication methods: + +* **Client Credentials Flow** + [Learn how to authenticate with client credentials.](https://zitadel.com/docs/guides/integrate/service-users/client-credentials) + +* **Personal Access Token (PAT)** + [Learn how to create and use a PAT.](https://zitadel.com/docs/guides/integrate/service-users/personal-access-token) + +**Reference:** [Service Users & API Authentication](https://zitadel.com/docs/guides/integrate/service-users/authenticate-service-users#authentication-methods) + +--- + +### 5.2. Import Data with the ZITADEL API + +Use your **access token** or **PAT** to authenticate, then call the [Management API – Human User Import](https://zitadel.com/docs/apis/resources/admin/admin-service-import-data) endpoint. + +**Import Endpoint:** + +* `POST /admin/v1/import` +* `Authorization: Bearer ` +* **Body:** Generated in step 4.2 + +#### Example cURL request + +```bash +curl --request POST \ + --url https:///admin/v1/import \ + --header 'Content-Type: application/json' \ + --header 'Authorization: Bearer ' \ + --data @importBody.json +``` + +Successful Response: +```bash +{ + "success": { + "orgs": [ + { + "orgId": "318900732864567390", + "humanUserIds": [ + "da72ac13-6994-4498-8b27-3ff9555661b2", + "4e987a01-34db-4393-b61c-1ce753baf69c", + "1041d710-8a89-48f8-85b5-1ab9656190f3", + "7b23b799-4f0f-4964-bc6d-95c534787d2c", + "6f2f1b2f-b292-4431-932b-620124e065ec", + "2c65045a-9de8-4d28-b686-b27bf3a70fc3", + "aca2dd3e-689c-4ab6-b446-0990127b1e0d", + "18a23e01-f0fe-443f-9f1c-2a8135cd22c2", + "c49af4bf-0dbb-4994-b453-b8dd0d5006ea" + ] + } + ] + }, + "errors": [ + { + "type": "org", + "id": "318900732864567390", + "message": "ID=ORG-lapo2m Message=Errors.Org.AlreadyExisting" + } + ] +} +``` + +ℹ️ Note: The above response indicates that the organization already existed, and users were successfully added. This is not an error, and you can consider the import successful as long as the HTTP status code is **200**. + +--- + +## 6. Testing the Migration + +### 6.1. Test User Login + +Use the **ZITADEL login page** or your integrated app to test logging in with one of the imported users. + +Confirm that the migrated password works as expected. + +--- + +### 6.2. Troubleshooting + +**Common issues:** + +* Invalid Keycloak export format +* Malformed JSON +* Missing `orgId` or access token +* Timeout exceeded during import + +The import API returns a detailed response with any errors encountered during the process. + +#### Where to check logs and get help + +You can verify that users were imported successfully by querying the **events API** and looking for the `user.human.added` event type. + +Use the following request: + +```bash +curl --location 'https:///admin/v1/events/_search' \ +--header 'Authorization: Bearer ' \ +--header 'Content-Type: application/json' \ +--data '{ + "asc": true, + "limit": 1000, + "event_types": [ + "user.human.added" + ] +}' +``` + +This will return a list of user creation events including details such as email, username, and hashed password to help you confirm the imported data. + +Successful Response +```bash +{ + "events": [ + { + "type": { + "type": "user.human.added", + "localized": { + "key": "EventTypes.user.human.added", + "localizedMessage": "Person added" + } + }, + "payload": { + "displayName": "test user", + "email": "testuser@gmail.com", + "userName": "testuser" + }, + "aggregate": { + "id": "da72ac13-6994-4498-8b27-3ff9555661b2", + "resourceOwner": "318900732864567390" + }, + "creationDate": "2025-07-22T15:16:06.364302Z" + } + ] +} +``` + +ℹ️ Note: If you see entries with "type": "user.human.added" and correct payload data, the import was successful. + +--- + +## 7. Q&A and Further Resources + +### Real-World Scenarios & Common Questions + +**Q:** What is the maximum number of users that can be imported in a single batch? +**A:** There is no hard limit on the number of users. However, there is a **timeout**. +For **ZITADEL Cloud deployments**, the timeout is **5 minutes**, which typically allows for importing around **5,000 users per batch**. + +--- diff --git a/docs/docs/guides/migrate/sources/zitadel.md b/docs/docs/guides/migrate/sources/zitadel.md index 8a8dd60a3d..99b6e1d64e 100644 --- a/docs/docs/guides/migrate/sources/zitadel.md +++ b/docs/docs/guides/migrate/sources/zitadel.md @@ -40,7 +40,7 @@ You need a PAT from a service user with IAM Owner permissions in both the source 4. Go to the Default settings 5. Add the import_user as [manager](/docs/guides/manage/console/managers) with the role "IAM Owner" -Save the PAT to the environment variabel `PAT_EXPORT_TOKEN` and the source domain as `ZITADEL_EXPORT_DOMAIN` to run the following scripts. +Save the PAT to the environment variable `PAT_EXPORT_TOKEN` and the source domain as `ZITADEL_EXPORT_DOMAIN` to run the following scripts. ### Target system @@ -50,7 +50,7 @@ Save the PAT to the environment variabel `PAT_EXPORT_TOKEN` and the source domai 4. Go to the Default settings 5. Add the export_user as [manager](/docs/guides/manage/console/managers) with the role "IAM Owner" -Save the PAT to the environment variabel `PAT_IMPORT_TOKEN` and the source domain as `ZITADEL_IMPORT_DOMAIN` to run the following scripts. +Save the PAT to the environment variable `PAT_IMPORT_TOKEN` and the source domain as `ZITADEL_IMPORT_DOMAIN` to run the following scripts. :::warning Clean-up You should let the PAT expire as soon as possible. diff --git a/docs/docs/legal/data-processing-agreement.mdx b/docs/docs/legal/data-processing-agreement.mdx index 63a393605f..78f14aa730 100644 --- a/docs/docs/legal/data-processing-agreement.mdx +++ b/docs/docs/legal/data-processing-agreement.mdx @@ -1,113 +1,277 @@ --- title: Data Processing Agreement custom_edit_url: null -custom: - created_at: 2022-07-15 - updated_at: 2023-11-16 --- -import PiidTable from './_piid-table.mdx'; -Last updated on November 15, 2023 +Last updated on May 8, 2025 -Within the scope of the [**Framework Agreement**](terms-of-service), the **Processor** (CAOS Ltd., also **ZITADEL**) processes **Personal Data** on behalf of the **Customer** (Responsible Party), collectively the **"Parties"**. +This Data Protection Agreement and its annexes (“**DPA**”) are part of the [Framework Agreement](./terms-of-service) between Zitadel, Inc. and it's affiliates ("**Zitadel**") and the Customer in respect of the provision of certain services, including any applicable statement of work, booking, purchase order (PO) or any agreed upon instructions (the "**Agreement**") and applies where, and to the extent that, Zitadel processes Personal Data as a Processor on behalf of the Customer under the Framework Agreement (each a “**Party**” and together the “**Parties**”). -This Annex to the Agreement governs the Parties' data protection obligations in addition to the provisions of the Agreement. +All capitalized terms not defined in this DPA will have the meanings set forth in the Agreement. +Any privacy or data protection related clauses or agreement previously entered into by Zitadel and the Customer, with regards to the subject matter of this DPA, will be superseded and replaced by this DPA. +No one other than a Party to this DPA, their successors and permitted assignees will have any right to enforce any of its terms. -## Subject matter, duration, nature and purpose of the processing as well as the type of personal data and categories of data subjects +This DPA shall become legally binding upon Customer entering into the Agreement. -This annex reflects the commitment of both parties to abide by the applicable data protection laws for the processing of Personal Data for the purpose of Processor's execution of the Framework Agreement. +## Definitions -The duration of the Processing shall correspond to the duration of the Agreement, unless otherwise provided for in this Annex or unless individual provisions obviously result in obligations going beyond this. +"**Applicable Data Protection Law**" means all worldwide data protection and privacy laws and regulations applicable to the Personal Data, including, where applicable, EU/UK Data Protection Law and US Data Protection Laws (in each case, as amended, adopted, or superseded from time to time). -In particular, the following Personal Data are part of the processing: - +“**Controller**,” “**collecting**,” “**processor**,” and “**processing**,” shall have the meanings given to them under Applicable Data Protection Law. -## Scope and responsibility +“**Business**,” “**service provider**,” “**contractor**,” “**selling**,” “**sharing**” and “**third party**” shall have the meanings given to them under applicable US Data Protection Laws. -Under this Agreement, the Processor shall process Personal Data on behalf of the Customer. +"**Customer Data**" means information, data and other content, in any form or medium, that is submitted, posted or otherwise transmitted by or on behalf of the Customer through the Zitadel Cloud or Services. For the avoidance of doubt, Customer Data includes Customer Personal Data. -This Annex applies to all processing of Customer's data (including data of the users of Customer's organization) with reference to persons ("**Personal Data**") which is related to the Agreement and which is carried out by the Processor, its employees or agents. +“**Customer Personal Data**” means, in any form or medium, all Personal Data that is processed by Zitadel or its sub-processors on behalf of Customer in connection with the Agreement. -The Customer shall be responsible for compliance with the statutory provisions of the data protection laws, in particular for the lawfulness of the transfer of data to the Processor as well as for the lawfulness of the data processing. +“**EU/UK Data Protection Law**” means: (i) Regulation 2016/679 of the European Parliament and of the Council on the protection of natural persons with regard to the processing of Personal Data and on the free movement of such data, also known as the General Data Protection Regulation (“**GDPR**”); (ii) the GDPR as saved into United Kingdom law by virtue of section 3 of the United Kingdom’s European Union (Withdrawal) Act 2018 (“**UK GDPR**”); (iii) the EU e-Privacy Directive (Directive 2002/58/EC); (iv) the Swiss Federal Act on Data Protection of 2020 and its Ordinance (“**Swiss FADP**”) and (v) any and all applicable national data protection laws and regulatory requirements made under, pursuant to or that apply in conjunction with any of (i), (ii) or (iii); in each case as may be amended or superseded from time to time. -The Processor is responsible for taking appropriate technical and organizational protection measures so that its processing complies with the legal requirements and ensures the protection of the rights of the Data Subjects. +“**Personal Data**” shall have the meaning given to it, or to the terms “personally identifiable information” and “personal information” under applicable Data Protection Law, but shall include, at a minimum, any information related to an identified or identifiable natural person. + +“**Restricted Transfer**” means: (i) where the GDPR applies, a transfer of Personal Data from the EEA to a country outside of the EEA which is not subject to an adequacy determination by the European Commission; and (ii) where UK-GDPR applies, a transfer of Personal Data from the United Kingdom to any other country which is not subject to adequacy regulations pursuant to Section 17A of the United Kingdom Data Protection Act 2018, in each case whether such transfer is direct or via onward transfer. + +“**Security Incident**” means any unauthorized or unlawful breach of security leading to, or reasonably believed to have led to, the accidental or unlawful destruction, loss, or alteration of, or unauthorized disclosure or access to, Personal Data transmitted, stored or otherwise processed by Zitadel under or in connection with the Agreement. + +“**Standard Contractual Clauses**” or “**SCCs**” means the contractual clauses annexed to the European Commission’s Implementing Decision 2021/914 of 4 June 2021 on standard contractual clauses for the transfer of personal data to third countries pursuant to Regulation (EU) 2016/679 of the European Parliament and of the Council. + +“**sub-processor**” means any third-party processor engaged by Zitadel to process Customer Data (but shall not include Zitadel employees, contractors or consultants). + +“**UK Addendum**” means the International Data Transfer Addendum (version B1.0) issued by the Information Commissioner’s Office under S119(A) of the UK Data Protection Act 2018, as updated or amended from time to time. + +“**US Data Protection Laws**” means any relevant U.S. federal and state privacy laws (and any implementing regulations and amendment thereto) effective as of the date of this DPA and that applies to the processing of Customer Personal Data under the Agreement, which may include, depending on the circumstances and without limitation, (i) the California Consumer Privacy Act (Cal. Civ. Code §§ 1798.100 et seq.), as amended by the California Privacy Rights Act of 2020 along with its implementing regulations (“**CCPA**”), (ii) the Colorado Privacy Act (Colo. Rev. Stat. §§ 6-1-1301 et seq.) (CPA), (iii) Connecticut’s Data Privacy Act (CTDPA), (iv) the Utah Consumer Privacy Act (Utah Code Ann. §§ 13-61-101 et seq.) (UCPA) and (v) the Virginia Consumer Data Protection Act VA Code Ann. §§ 59.1-575 et seq. (VCDPA). + +## Processing of Personal Data + +This DPA applies where and only to the extent that Zitadel processes Customer Personal Data in connection with the provision of the Services under the Agreement involving the processing of Personal Data protected by Applicable Data Protection Law. +This DPA reflects the commitment of both Parties to abide by Applicable Data Protection Law for the processing of Personal Data by Zitadel as a processor for the purpose of the Zitadel's provision of the Services and its execution of the Agreement. + +This DPA will become effective on the date the Agreement enters into effect and will remain in force for the term of the Agreement, unless otherwise provided for in this DPA or unless individual provisions obviously result in obligations going beyond this. +For the avoidance of doubt, the terms of the Framework Agreement will continue in full force and effect; however, to the extent any term in any Agreement regarding either Party’s obligations with respect to Customer Data is less restrictive than or is inconsistent with this DPA, the terms of this DPA shall supersede and control. + +The Parties acknowledge that the following Customer Data will be processed as part of the Services: + +import { PiiTable } from "../../src/components/pii_table"; + + + +## Scope + +Under this Agreement, Zitadel shall process Customer Personal Data to perform its obligations under the Agreement and and strictly in accordance with the documented instructions of Customer (the “**Permitted Purpose**”), except where otherwise required by law(s) that are not incompatible with Applicable Data Protection Law. + +The Parties acknowledge and agree that for the purposes of this DPA, the Customer is the controller and appoints Zitadel as a processor to process the Customer Personal Data. +To the extent that the Parties are subject to the California Consumer Privacy Act (CCPA), the Customer is the business whereas Zitadel is a service provider to the Customer. +Each Party shall comply with the obligations that apply to it under Applicable Data Protection Law. + +Each Party shall comply with its own obligations under Applicable Data Protection Law in respect of any Customer Personal Data processed under the Agreement. + +## Customer's Responsibilities + +The Customer’s instructions to Zitadel shall comply with Applicable Data Protection Law. +The Customer will have sole responsibility for the accuracy, quality and legality of the Customer Data, the means by which the Customer acquired the Customer Data, and the Customer's permissions to process the Customer Data pursuant to this DPA. + +As required under Applicable Data Protection Law, the Customer will provide all necessary notices to data subjects and secure the applicable lawful grounds for processing Data under the DPA, including where applicable, all necessary permissions and consents from them. To the extent required under Applicable Data Protection Law, the Customer will receive and document the appropriate consent from the data subject(s). + +The Customer represents and warrants that (i) it complies with Applicable Data Protection Law as relevant to the lawful processing by Zitadel of Customer Personal Data for the purposes contemplated by this DPA and the Agreement; and (ii) to the knowledge of the Customer, the processing of Customer Personal Data by Zitadel in accordance with the Customer’s instructions will not cause Zitadel to be in breach of any Applicable Data Protection Law. + +The Customer shall not disclose any special categories of Personal Data or sensitive personal information (as these terms are defined under Applicable Data Protection Law) to Zitadel for processing. ## Obligations of the processor -### Bound by directions +### Bound by the Customer's directions and instructions -The Processor processes personal data in accordance with its privacy policy (cf. [Privacy Policy](/legal/policies/privacy-policy)) and on the documented directions of the Customer. The initial direction result from the Agreement. Subsequent instructions shall be given either in writing, whereby e-mail shall suffice, or orally with immediate written confirmation. +Customer hereby instructs Zitadel to process Customer Data for the Permitted Purpose. -If the Processor is of the opinion that a direction of the Customer violates the Agreement, the GDPR or other data protection provisions of the EU, EU Member States or Switzerland, it shall inform the Customer thereof and shall be entitled to suspend the Processing until the instruction is withdrawn or confirmed. +Zitadel processes Personal Data in accordance with its privacy policy (cf. [Privacy Policy](./policies/privacy-policy)) and upon the documented directions of the Customer (which includes the Agreement). +Subsequent instructions shall be given either in writing, whereby e-mail shall suffice, or orally with immediate written confirmation. -### Obligation of the processing persons to confidentiality +Zitadel shall promptly inform Customer if it becomes aware that such processing instructions infringe Applicable Data Protection Law (but without obligation to actively monitor compliance with Applicable Data Protection Law). +In such case, Zitadel shall be entitled to suspend the processing until the infringing instruction is withdrawn or confirmed. -The Processor shall ensure that the persons authorized to process the Personal Data have committed themselves to confidentiality, unless they are already subject to an appropriate statutory duty of confidentiality. +### Confidentiality obligations + +Zitadel shall ensure that any person that it authorizes to process Customer Data (including Zitadel’s staff, agents and sub-processors) (an “Authorized Person”) shall be subject to a strict duty of confidentiality (whether a contractual duty or a statutory duty) and shall not permit any person to process the Customer Data that is not under such a duty of confidentiality. +Zitadel shall ensure that all Authorised Persons process the Customer Data only as necessary for the Permitted Purpose. ### Technical and organizational measures -The Processor has taken appropriate technical and organizational security measures, maintains them for the duration of the Processing and updates them on an ongoing basis in accordance with the current state of technology. - -The technical and organizational security measures are described in more detail in the [annex](#annex-regarding-security-measures) to this appendix. +Zitadel shall implement appropriate technical and organizational measures to protect the Customer Data from a Security Incident, as described in Annex II to this DPA. +Such measures shall comply with all Applicable Data Protection Law and shall further have regard to the state of the art, the costs of implementation and the nature, scope, context and purposes of processing as well as the risk of varying likelihood and severity for the rights and freedoms of natural persons. +The Customer acknowledges that such measures are subject to technical progress and development and that Zitadel may update or modify such measures from time to time, provided that such updates and modifications do not degrade or diminish overall security of the Customer Data, or of the Services under the Agreement. ### Involvement of subcontracted processors -A current and complete [list of involved and approved sub-processors](./subprocessors) can be found in our legal section. +Customer agrees that Zitadel may engage sub-processors to process Customer Data on Customer’s behalf. A current and complete [list of involved and approved sub-processors](https://zitadel.com/trust) can be found on our [Trust Center](https://zitadel.com/trust) (as may be updated from time to time in accordance with this DPA). -The Processor is entitled to involve additional sub-processors. -In this case, the Processor shall inform the Responsible Party about any intended change regarding sub-processors and update the list of involved an approved sub-processors. -The Customer has the right to object to such changes. -If the Parties are unable to reach a mutual agreement within 30 days of receipt of the objection by the Processor, the Customer may terminate the Agreement extraordinarily. +Zitadel will notify Customer by updating the list of sub-processors and, if Customer has subscribed to notices, via email. +If, within five (5) calendar days after such notice, Customer notifies Zitadel in writing that Customer objects to Zitadel's appointment of a new sub-processor based on reasonable data protection concerns, the parties will discuss such concerns in good faith with a view to achieving a commercially reasonable resolution. +If the parties are not able to mutually agree to a resolution of such concerns, Customer, as its sole and exclusive remedy, may terminate the Agreement for convenience with no refunds and Customer will remain liable to pay any committed fees in an order form, order, statement of work or other similar ordering document. -The Processor obligates itself to impose on all sub-processors, by means of a contract (or in another appropriate manner), the same data protection obligations as are imposed on it by this Annex. -In particular, sufficient guarantees shall be provided that the appropriate technical and organizational measures are implemented in such a way that the processing by the sub-processor is carried out in accordance with the legal requirements. +Zitadel shall inform the Customer if it adds or replaces any sub-processor at least fifteen (15) days prior to any such change (including details of the processing it performs or will perform). +The Customer may object in writing to Zitadel’s engagement of a new sub-processor on reasonable grounds relating to the protection of Customer Personal Data by notifying Zitadel promptly in writing within fifteen (15) calendar days of receipt of Zitadel’s notice. +In such case, the parties shall discuss Customer’s concerns in good faith with a view to achieving a commercially reasonable resolution. +If such objection right is not exercised by Customer, silence shall be deemed to constitute an approval of the relevant sub-processor engagement. -Our websites and services may involve processing by third-party sub-processors with country of registration outside of Switzerland or the EU/EAA. -In these cases, we only transfer personal data after we have implemented the legally required measures for this, such as concluding standard contractual clauses on data protection or obtaining the consent of the data subjects. If interested, the documentation on these measures can be obtained from the contact person mentioned above. -The country of registration of a sub-processor may be different from the hosting location of the data. Please refer to the [list of involved and approved sub-processors](./subprocessors) for more details. +Where Zitadel appoints a sub-processor, Zitadel shall: (i) enter into an agreement with each sub-processor containing data protection terms that provide at least the same level of protection for Customer Data as those contained in this DPA, to the extent applicable to the nature of the services provided by such sub-processor; and (ii) remain responsible to the Customer for Zitadel’s sub-processors’ failure to perform their obligations with respect to the processing of Customer Data. -If the sub-processor fails to comply with its data protection obligations, the processor shall be liable to the customer for this as for its own conduct. +Taking into account the safeguards set forth in this DPA, Customer Data may be processed outside of Switzerland or the EU/EAA, such as in the United States or any country in which Zitadel or is sub-processors operate. Our [list of involved and approved sub-processors](https://zitadel.com/trust) provides additional details. ### Assistance in responding to requests -The Processor shall support the Customer as far as possible with suitable technical and organizational measures in fulfilling its obligation to respond to requests to exercise the data subject's rights (**"Data Subject Request"**). -The Processor will promptly notify the Customer if it receives a Data Subject Request. -The Processor will not respond to a Data Subject Request, provided that the Customer agrees the Processor may at its discretion respond to confirm that such request relates to the Customer. -The Customer acknowledges and agrees that the Services include features which will allow the Customer to manage Data Subject Requests directly through the Services without additional assistance from the Processor. -If the Customer does not have the ability to address a Data Subject Request, the Processor will, upon the Customer’s written request, provide reasonable assistance to facilitate the Customer’s response to the Data Subject Request to the extent such assistance is consistent with applicable law; provided that the Customer will be responsible for paying for any costs incurred or fees charged by the Processor for providing such assistance. +Zitadel shall provide all reasonable and timely assistance (which may include by appropriate technical and organizational measures) to the Customer to enable the Customer to respond to: (i) any request from a data subject to exercise any of their rights under Applicable Data Protection Law ("**Data Subject Request**"); and (ii) any other correspondence, enquiry or complaint received from a data subject, regulator or other third party in connection with the processing of Customer Personal Data. -The Processor, unless prohibited from doing so by applicable law, will promptly notify the Customer of any requests from a regulator or any other authority in relation to Personal Data that is being processed on behalf of the Customer, given that request resulted in disclosure of Personal Data to the regulator or any other authority. +In the event that any such request, correspondence, enquiry or complaint is made directly to Zitadel, Zitadel shall promptly inform the Customer providing full details of the same. +Zitadel will not respond to a Data Subject Request, however the Customer acknowledges and agrees that Zitadel may at its discretion respond to confirm that such request relates to the Customer. -### Further support for the customer +The Customer hereby acknowledges and agrees that the Services include features which will allow the Customer to manage Data Subject Requests directly through the Services without additional assistance from the Processor. +If the Customer does not have the ability to address a Data Subject Request, Zitadel will, upon the Customer’s written request, provide reasonable assistance to facilitate the Customer’s response to such Data Subject Request to the extent such assistance is consistent with Applicable Data Protection Law; provided that the Customer will be responsible for paying for any reasonable costs incurred or fees charged by Zitadel for providing such assistance. -The Processor shall, taking into account the nature of the processing and the information available to it, assist the Customer in complying with its obligations in connection with the security of the processing, any notifications of [Security Incidents](#security-incidents), and any data protection impact assessments. +Zitadel, unless prohibited from doing so by applicable law, will promptly notify the Customer of any requests from a regulator, law enforcement authority or any other relevant and competent authority in relation to the Customer Personal Data that is being processed on behalf of the Customer, to the extent that the request may result in the disclosure of Customer Personal Data to such regulator, law enforcement authority or any other relevant and competent authority. + +### Cooperation and support for the Customer + +Zitadel shall provide the Customer with all such reasonable and timely assistance as Customer may require in order to enable it to conduct a data protection impact assessment (or equivalent document) where required by Applicable Data Protection Law, including, if necessary, to assist Customer to consult with its relevant data protection or other regulatory authority. ### Security incidents -The Processor will notify the Customer of any incident, meaning breach of security or other action or inaction leading to the accidental or unlawful destruction, loss, alteration, unauthorised disclosure of, or access to, personal data covered under this (***Security Incident"**) without undue delay, and will promptly provide the Customer with all reasonable information concerning the Security Incident insofar as it affects the Customer. -If possible, the Processor will promptly implement measures proposed in the notification. -Insofar required the Processor will assist the Customer in notifying any applicable regulatory authority. +Upon becoming aware of a Security Incident, Zitadel shall inform Customer without undue delay and provide all such timely information and cooperation as Customer may require for the Customer to fulfil its data breach or cybersecurity incident reporting obligations under (and in accordance with the timescales required by) Applicable Data Protection Law. +Customer shall further take all such measures and actions as are reasonable and necessary to investigate, contain, and remediate or mitigate the effects of the Security Incident, to the extent that the remediation is within Zitadel's control, and shall keep Customer informed of all material developments in connection with the Security Incident. + +Notwithstanding anything to the contrary, Zitadel's notification of or response to a Security Incident under this section will not be construed as an acknowledgment by Zitadel of any fault or liability with respect to such Security Incident. ### Deletion or destruction after termination -Upon Customer's request, the Processor shall delete personal data received after the end of the agreement, unless there is a legal obligation for the Processor to store or further process such data. +Upon termination or expiry of the Agreement, Zitadel shall (at the Customer’s election) destroy or return to the Customer all Customer Data (including all copies of the Customer Data) in its possession or control (including any Customer Data subcontracted to a third party for processing). +This requirement shall not apply to the extent that Zitadel is required by any applicable law to retain some or all Customer Data, in which case Zitadel shall isolate and protect the Customer Data from any further processing except to the extent required by such law until deletion is possible. -### Information and control rights of the customer +### Customer's information and audit rights -The Processor shall provide the Customer with all information necessary to demonstrate compliance with the obligations set forth in this annex or to respond to requests from an applicable supervisory authority, subject to the confidentiality terms in the Framework Agreement. -The Processor shall enable and contribute to audits, including inspections, carried out by the Customer or an auditor appointed by the Customer. +To the extent required under Applicable Data Protection Law and on written request from the Customer, Zitadel shall provide written responses (which may include audit report summaries/extracts) to all reasonable requests for information made by the Customer related to its processing of Customer Personal Data as necessary to confirm Zitadel's compliance with this DPA. +The Customer shall not exercise this right more than once in any twelve (12)-month rolling period, except (i) if and when required by instruction of a competent data protection or other regulatory authority; or (ii) if Zitadel has experienced a Security Incident where Customer was directly impacted. -The procedure to be followed in the event of directions that are presumed to be unlawful is governed by the section [Bound by directions](#bound-by-directions) of this Appendix. +Nothing in this section shall be construed to require Zitadel to document or provide: (i) trade secrets or any proprietary information; (ii) any information that would violate Zitadel’s confidentiality obligations, contractual obligations, or applicable law; or (iii) any information, the disclosure of which could threaten, compromise, or otherwise put at risk the security, confidentiality, or integrity of Zitadel’s infrastructure, networks, systems, algorithms or data. -## Annex regarding security measures +### Service Optimization -The Processor has taken the following organizational and technical security measures to ensure a level of protection of the Personal Data processed that is appropriate to the risk: +Where permitted by Applicable Data Protection Law, Zitadel may process Customer Data: (i) for its internal uses to build or improve the quality of its services; (ii) to detect Security Incidents; and (iii) to protect against fraudulent or illegal activity. + +Zitadel may: (i) compile aggregated and/or de-identified information in connection with the provision of the Services, provided that such information cannot reasonably be used to identify Customer or any data subject to whom Customer Personal Data relates (“Aggregated and/or De-Identified Data”); and (ii) use such Aggregated and/or De-Identified Data for its lawful business purposes in accordance with Applicable Data Protection Law. + +### Data Transfers + +Where either Party intends to transfer Personal Data cross-border and Applicable Data Protection Law requires certain measures to be implemented prior to such transfer, each Party agrees to implement such measures to ensure compliance with Applicable Data Protection Law. + +To the extent that the transfer of Personal Data from Customer to Zitadel involves a transfer of Personal Data outside the European Economic Area (EEA), Switzerland, or the United Kingdom to a jurisdiction which is not subject to an adequacy determination by the European Commission, United Kingdom or Swiss authorities (as applicable) that covers such transfer, then the SCCs are hereby incorporated by reference and form an integral part of the DPA. + +#### EEA Transfers + +To the extent that Customer Personal Data is subject to the GDPR, and the transfer would be a Restricted Transfer, the SCCs apply as follows: + +1) the Customer is the ‘data exporter’ and Zitadel is the ‘data importer’; +2) the Module Two terms (Transfer controller to processor) apply; +3) in Clause 7, the optional docking clause does not apply; +4) in Clause 9, Option 2 (General Authorization) applies and the time period for prior notice of sub-processor changes is set out in this DPA; +5) in Clause 11, the optional language does not apply; +6) in Clause 17, Option 1 applies, and the SCCs are governed by German law; +7) in Clause 18(b), disputes will be resolved before the courts of Hamburg in Germany; +8) in Annex I, the details of the parties and the transfer are set out in the Agreement; +9) in Clause 13(a) and Annex I, the Hamburg data protection authority will act as competent supervisory authority; +10) in Annex II, the description of the technical and organizational security measures is set out in Annex 2 of this DPA or, if not set out therein, the applicable statement of work; and +11) in Annex III, the list of sub-processors is set out at the address [https://zitadel.com/trust](https://zitadel.com/trust) or, if not set out therein, applicable statement of work. + +#### Swiss Transfers + +To the extent that Customer Personal Data is subject to Swiss law, and the transfer would be a Restricted Transfer, the SCCs apply as set out above with the following modifications: + +1) references to ‘Regulation (EU) 2016/679’ are interpreted as references to the Swiss FADP or any successor thereof; +2) references to specific articles of ‘Regulation (EU) 2016/679’ are replaced with the equivalent article or section of the Swiss FADP, +3) references to ‘EU’, ‘Union’ and ‘Member State’ are replaced with ‘Switzerland’, +4) Clause 13(a) and Part C of Annex 2 is not used and the ‘competent supervisory authority’ is the Swiss Federal Data Protection Information Commissioner (“**FDPIC**”) or, if the transfer is subject to both the Swiss FADP and the GDPR, the FDPIC (insofar as the transfer is governed by the Swiss FADP) or the DPC (insofar as the transfer is governed by the GDPR), +5) references to the ‘competent supervisory authority’ and ‘competent courts’ are replaced with the FDPIC and ‘competent Swiss courts’, +6) in Clause 17, the SCCs are governed by the laws of Switzerland, +7) in Clause 18(b), disputes will be resolved before the competent Swiss courts, and +8) the SCCs also protect the data of legal entities until entry into force of the revised Swiss FADP. + +#### UK Transfers + +To the extent that Customer Personal Data is subject to Applicable Data Protection Law of the United Kingdom, and the transfer would be a Restricted Transfer, the SCCs as set out above shall apply as amended by Part 2 of the UK Addendum, and Part 1 of the UK Addendum is deemed completed as follows: + +1) in Table 1, the details of the parties are set out in the Agreement or, if not set out therein, the applicable statement of work; +2) in Table 2, the selected modules and clauses are set out in Section 6.3 of this DPA; +3) in Table 3, the appendix information is set out in the annexes to this DPA or, if not set out therein, the applicable statement of work; and +4) in Table 4, the ‘Exporter’ is selected. + +#### Alternative Transfer Mechanism + +In the event that a court of competent jurisdiction or supervisory authority orders (for whatever reason) that the measures described in this DPA cannot be relied on to lawfully transfer Customer Personal Data, or Zitadel adopts an alternative data transfer mechanism to the mechanisms described in this DPA, including any new version of or successor to the standard contractual clauses (“Alternative Transfer Mechanism”), the Customer agrees to fully co-operate with Zitadel to agree an amendment to this DPA and/or execute such other documents and take such other actions as may be necessary to remedy such non-compliance or give legal effect to such Alternative Transfer Mechanism. + +### Additional Provisions under US Data Protection Laws + +The Parties agree that all Customer Personal Data that is subject to US Data Protection Laws (including the CCPA) is disclosed to Zitadel by the Customer for the Permitted Purpose and its use or sharing by the Customer with Zitadel is necessary to perform such Permitted Purpose. + +Zitadel agrees that it will not: + +1. sell or share any Customer Personal Data to a third party for any purpose other than than for the Permitted Purpose; +2. retain, use, or disclose any Customer Personal Data (i) for any purpose other than for the Permitted Purpose, including for any commercial purpose, or (ii) outside of the direct business relationship between the Parties, except as necessary to perform the Permitted Purpose or as otherwise permitted by US Data Protection Laws; or +3. combine Customer Personal Data received from or on behalf of Customer with Personal Data received from or on behalf of any third party or collected from Zitadel’s own interaction with individuals or data subjects, except to perform a Permitted Purpose in accordance with the CCPA, the Agreement and this DPA. + +The Parties acknowledge that the Customer Personal Data that Customer discloses to Zitadel is provided only for the limited and specified purposes set forth as the Permitted Purpose in the Agreement and this DPA. + +Zitadel shall provide the same level of protection to Customer Personal Data as required by the CCPA and will: (i) assist the Customer in responding to any request from a data subject to exercise rights under US Data Protection Laws; and (ii) immediately notify the Customer if it is not able to meet the requirements under the CCPA. + +The Customer may take such reasonable and appropriate steps as may be necessary (a) to ensure that the Customer Personal Data collected is used in a manner consistent with the business’s obligations under the CCPA; and (b) to stop and remediate any unauthorized use of Customer Personal Data, and (b) to ensure that Customer Personal Data is used in a manner consistent with the CCPA. + +### Miscellaneous + +This DPA shall be governed by and construed in accordance with the governing law and jurisdiction provisions set out in the Agreement, unless required otherwise by Applicable Data Protection Law. + +Any liability owed by one party to the other under this DPA shall be subject to the limitations of liability set forth in the Agreement. + +This DPA shall terminate upon the earlier of (i) the termination or expiry of all Agreement under which Customer Data may be processed, or (ii) the written agreement of the Parties. + +Any notices shall be delivered to a Party in accordance with the notice provisions of the Agreement, unless otherwise specified hereunder. + +## Annex 1: Description of Processing Activities / Transfer + +### List of Parties + +| Data Exporter | Data Importer | +| :---- | :---- | +| Name: The Party identified as the Customer in the Agreement. | Name: The Party identified as Zitadel in the Agreement. | +| Address: As identified in the Agreement. | Address: As identified in the Agreement. | +| Contact Person's Name, position and contact details: As identified in the Agreement. | Contact Person's Name, position and contact details: As identified in the Agreement. | +| Activities relevant to the transfer: See below | Activities relevant to the transfer: See below | +| Role: Controller | Role: Processor | + +### Description of processing / transfer + +| | Description | +| :---- | :---- | +| **Categories of data subjects:** | As described in the section "Processing of Personal Data" of the DPA | +| **Categories of personal data:** | As described in the section "Processing of Personal Data" of the DPA | +| **Sensitive data:** | None. | +| **If sensitive data, the applied restrictions or safeguards** | N/A | +| **Frequency of the transfer:** | Continuous | +| **Nature and subject matter of processing:** | The Services described in the Agreement. | +| **Purpose(s) of the data transfer and further processing:** | As set forth in the Agreement. | +| **Retention period (or, if not possible to determine, the criteria used to determine that period):** | The personal data may be retained until termination or expiry of the DPA. | + +### Competent supervisory authority + +The competent supervisory authority in connection with Customer Personal Data protected by the GDPR, is the Hamburg data protection authority. +If this is not possible, then as otherwise agreed by the parties consistent with the conditions set forth in Clause 13. + +In connection with Customer Personal Data that is protected by UK-GDPR, the competent supervisory authority is the Information Commissioners Office (the "ICO"). + +## Annex 2: Technical and organizational measures + +Zitadel has implemented an information security program, that is designed to protect the confidentiality, integrity and availability of Customer Data. Zitadel's information security program includes the following organizational and technical security measures to ensure a level of protection of the Personal Data processed that is appropriate to the risk: ### Pseudonymization / Encryption The following measures for pseudonymization and encryption exist: -1. All communication is encrypted with TLS >1.2 with PFS +1. All communication is encrypted with TLS >1.2 with PFS 2. Critical data is exclusively stored in encrypted form 3. Storage media that store customer data are always encrypted 4. Passwords are irreversibly stored with a hash function diff --git a/docs/docs/legal/policies/account-lockout-policy.md b/docs/docs/legal/policies/account-lockout-policy.md index a593eac1bc..663fd12d9d 100644 --- a/docs/docs/legal/policies/account-lockout-policy.md +++ b/docs/docs/legal/policies/account-lockout-policy.md @@ -4,56 +4,69 @@ sidebar_label: Account Lockout Policy custom_edit_url: null --- -Last updated on May 31, 2023 +Last updated on June 25, 2025 -This policy is an annex to the [Terms of Service](../terms-of-service) that clarifies your obligations and our procedure handling requests where you can't get access to your ZITADEL Cloud services and data. This policy is applicable to situations where we, ZITADEL, need to restore your access for a otherwise available service and not in cases where the services are unavailable. +This policy is an annex to the [Terms of Service](../terms-of-service) and outlines your responsibilities, as well as our procedures, for handling situations where you are unable to access your ZITADEL Cloud services or data. -## Why to do we have this policy? +It applies specifically to cases where **ZITADEL** must restore your access to services that are otherwise operational, and does **not** cover service outages or unavailability. -Users may not be able to access our services anymore due to loss of credentials or misconfiguration. -In certain circumstances it might not be possible to recover the credentials through a self-service flow (eg, loss of 2FA credentials) or access the system to undo the configuration that caused the issue. -These cases might require help from our support, so you can regain access to your data. -We will require some initial information and conditions to be able to assist you, and will require further information to handle the request. -We also keep the right to refuse any such request without providing a reason, in case you can't provide the requested information. +## Why do we have this policy? -## Scope +Users may lose access to ZITADEL services due to lost credentials or misconfiguration. -In scope of this policy are requests to recover +In some cases, it may not be possible to recover access through self-service options—for example, losing access to 2FA credentials or being unable to reverse a misconfiguration. These situations may require support from our team to help you regain access to your data. -- ZITADEL Cloud account (customer portal) -- Manager accounts to a specific instance -- Undo configuration changes resulting in lockout (eg, misconfigured Action) +To assist with such requests, we will require specific information and may request additional details throughout the process. -Out of scope are requests to recover access +**ZITADEL reserves the right to decline any access recovery request without providing a reason if the required information cannot be verified or provided.** + + +## Scope of This Policy + +This policy applies to the following situations: + +- Loss of access to your **ZITADEL Cloud Admin Account** (customer portal) +- Inability to access **Instance Manager accounts** for a specific instance +- Need to **undo configuration changes** that caused a lockout (e.g., a misconfigured Action) + + +## Out of Scope + +The following types of access recovery requests are **not** covered by this policy: + +- Situations where you can request access from another **Admin** or **Instance Manager** +- Requests made by **end-users** who should instead contact their Admin or Manager +- Issues related to **self-hosted ZITADEL instances** +- **Free accounts/Instances** -- Where you have to option to ask another Admin/Manager -- by end-users who should ask an Admin/Manager instead -- self-hosted instances ## Process -Before you send a request to restore access to your account, please make sure that can't ask your manager/admin or another manager/admin to recover access. +Before submitting a request to restore access to your account, please ensure that you are unable to regain access through your existing **Manager** or **Admin**, or by contacting another **Manager/Admin** within your organization. -### ZITADEL Cloud account -If you need to recover your ZITADEL Cloud account for the customer portal, please send an email to [support@zitadel.com](mailto:support@zitadel.com?subject=ZITADEL%20Cloud%20account%20lockout): +### ZITADEL Cloud account (Customer Portal) + +Please visit the [support page in the customer portal](https://zitadel.com/admin/support): - State clearly in the subject line that this is related to an account lockout for a ZITADEL Cloud account - The sender's email address must match the verified email address of the account owner - State the reason why you're not able to recover the account yourself -Please allow us time to validate your request. -Our support will get back to you to request additional information for verification. +Please allow us time to validate your request. +Our support team will follow up with additional verification steps if needed. -### Manager access to an Instance +### Instance Manager access recovery If you need to recover a Manager account to an instance, please make sure you can't recover the account via another user or service user with Manager permissions. -Please visit the [support page in the customer portal](https://zitadel.cloud/admin/support): +Please visit the [support page in the customer portal](https://zitadel.com/admin/support): -- State clearly in the subject line that this is related to an account lockout the affected instance +- State clearly in the subject line that this is related to an account lockout **for** the affected instance +- The sender's email address must match the verified email address of the affected instance manager - State the reason why you're not able to recover the account yourself -Please allow us time to validate your request. -Our support will get back to you to request additional information for verification. +Please allow us time to validate your request. +Our support team will follow up with additional verification steps if needed. + diff --git a/docs/docs/legal/policies/privacy-policy.mdx b/docs/docs/legal/policies/privacy-policy.mdx index 30e213adb0..3dc544f8ae 100644 --- a/docs/docs/legal/policies/privacy-policy.mdx +++ b/docs/docs/legal/policies/privacy-policy.mdx @@ -2,20 +2,42 @@ title: Privacy Policy custom_edit_url: null --- -import PiidTable from '../_piid-table.mdx'; -Last updated on March 07, 2024 +Last updated on 20 March, 2025 -This privacy policy applies to CAOS Ltd., the websites it operates (including zitadel.ch, zitadel.cloud and zitadel.com) and the services and products it provides (including ZITADEL). This privacy policy describes how we process personal data for the provision of this websites and our products. +This privacy policy describes how ZITADEL Inc. and its wholly owned subsidiaries and affiliates (collectively, "**ZITADEL**", “**CAOS**", "**we**" or "**us**") collect, use, disclose and otherwise process your personal data in connection with the management of our business and our relationships with customers, visitors and event attendees. -If any inconsistencies arise between this Privacy Policy and the otherwise applicable contractual terms, framework agreement, or general terms of service, the provisions of this Privacy Policy shall prevail. This privacy policy covers both existing personal data and personal data collected from you in the future. +This privacy policy explains your rights and choices related to the personal data we collect when: -The responsible party for the data processing described in this privacy policy and contact for questions and issues regarding data protection is +* You interact with our websites, including zitadel.com, zitadel.cloud and zitadel.ch as well any other websites that we operate and that link to this privacy policy (our “**Sites**”) -**CAOS AG** +* You visit, interact with, or use any of our offices, events, sales, marketing or other activities; and + +* You use our platform, including ZITADEL and our software, mobile application, and other products and services (the “**Services**”). + +This privacy policy does not cover: + +* **Organizational Use**. When you use our Services on behalf of an organization (your employer), your use is administered and provisioned by your organization under its policies regarding the use and protection of personal data. If you have questions about how your data is being accessed or used by your organization, please refer to your organization's privacy policy and direct your inquiries to your organization's system administrator. + +* **Third Parties**. Our Sites include links to websites and/or applications operated and maintained by third parties (e.g. GitHub, LinkedIn, etc.). This privacy policy does not apply to any products, services, websites, or content that are offered by third parties and/or have their own privacy policy. + +If any inconsistencies arise between this privacy policy and the otherwise applicable contractual terms, framework agreement, or general terms of service, the provisions of this privacy policy shall prevail (where applicable). This privacy policy covers both existing personal data and personal data which may be collected from you in the future. + +ZITADEL determines the purposes for and means of the processing (i.e., we are the data controller) of your personal data as described in this privacy policy, unless expressly specified otherwise. The responsible party for the data processing described in this privacy policy and contact for questions and issues regarding data protection is: + +**Zitadel Inc.** +Data Protection Officer +Four Embarcadero Center, Suite 1400 +San Francisco, CA 94111-4164 +United States of America +[legal@zitadel.com](mailto:legal@zitadel.com) + +**CAOS AG (Affiliate of Zitadel, Inc.)** Data Protection Officer Lerchenfeldstrasse 3 -9014 St. Gallen +9014 St. Gallen +Switzerland +[legal@zitadel.com](mailto:legal@zitadel.com) Switzerland [legal@zitadel.com](mailto:legal@zitadel.com) @@ -41,15 +63,13 @@ This website uses TLS encryption for security reasons and to protect the transmi We process personal data in accordance with Swiss data protection law. In addition, we process - to the extent and insofar as the EU Data Protection Regulation is applicable - personal data in accordance with the following legal bases within the meaning of Art. 6 (1) DSGVO : -- Insofar as we obtain the consent of the data subject for processing operations, Art. 6 (1) a) DSGVO serves as the legal basis. -- When processing personal data for the fulfillment of a contract with the data subject as well as for the implementation of corresponding pre-contractual measures, Art. 6 para. 1 lit. b DSGVO serves as the legal basis. -- To the extent that processing of personal data is necessary to comply with a legal obligation to which we are subject under any applicable law of the EU or under any applicable law of a country in which the GDPR applies in whole or in part, Art. 6 para. 1 lit. c GDPR serves as the legal basis. -- For the processing of personal data in order to protect vital interests of the data subject or another natural person, Art. 6 para. 1 lit. d DSGVO serves as the legal basis. -- If personal data is processed in order to protect the legitimate interests of us or of third parties and if the fundamental freedoms and rights and interests of the data subject do not override our interests and the interests of third parties, Article 6 (1) (f) of the GDPR serves as the legal basis. Legitimate interests are in particular our business interest in being able to provide our website and our products, information security, the enforcement of our own legal claims and compliance with Swiss law. +* Insofar as we obtain the consent of the data subject for processing operations, Art. 6 (1) a) DSGVO serves as the legal basis. +* When processing personal data for the fulfillment of a contract with the data subject as well as for the implementation of corresponding pre-contractual measures, Art. 6 para. 1 lit. b DSGVO serves as the legal basis. +* To the extent that processing of personal data is necessary to comply with a legal obligation to which we are subject under any applicable law of the EU or under any applicable law of a country in which the GDPR applies in whole or in part, Art. 6 para. 1 lit. c GDPR serves as the legal basis. +* For the processing of personal data in order to protect vital interests of the data subject or another natural person, Art. 6 para. 1 lit. d DSGVO serves as the legal basis. +* If personal data is processed in order to protect the legitimate interests of us or of third parties and if the fundamental freedoms and rights and interests of the data subject do not override our interests and the interests of third parties, Article 6 (1) (f) of the GDPR serves as the legal basis. Legitimate interests are in particular our business interest in being able to provide our website and our products, information security, the enforcement of our own legal claims and compliance with Swiss law. -We will retain personal data for the period of time necessary for the particular purpose for which it was collected. - -Subsequently, they are either deleted or made anonymous, unless we need them for a longer period of time in exceptional cases, e.g. due to legal storage and documentation obligations or our legitimate interests, such as the protection of rights to which we are entitled or the defense of claims. +We will retain personal data for the period of time necessary for the particular purpose for which it was collected and where we have an ongoing legitimate business need to do so (for example to comply with applicable legal, tax or accounting requirements). Subsequently, they are either deleted or made anonymous, unless we need them for a longer period of time in exceptional cases, e.g. due to legal storage and documentation obligations or our legitimate interests, such as the protection of rights to which we are entitled or the defense of claims. ### Processing of personal data when using the website, contact forms and in connection with newsletters @@ -57,45 +77,49 @@ Our websites can generally be visited without registration. Each time one of our This data is processed to enable correct delivery and functioning of the website. In addition, we use the data to optimize the website and to ensure the security of our systems. -Personal data, in particular name, address or e-mail address are collected as far as possible on a voluntary basis, for example when you contact us via a contact form or by e-mail. Without your consent, the data will not be passed on to third parties, unless shown in this privacy policy. +Personal data, in particular name, address or e-mail address are collected as far as possible on a voluntary basis, for example when you contact us via a contact form or by e-mail. Without your consent, the data will not be passed on to third parties, unless otherwise stated in this privacy policy. If you send us inquiries via contact form, your data from the form, including any data you provided, will be stored by us for the purpose of processing the inquiry and in case of follow-up questions. We do not pass on this data without your consent, except insofar as this is shown in this privacy policy. -If you would like to receive newsletters offered on our websites, we require an e-mail address from you as well as information that allows us to verify that you are the owner of the specified e-mail address and agree to receive the newsletter. Further data will not be collected. We use this data exclusively for sending the requested information and do not pass it on to third parties, except as described in this privacy policy. +If you would like to receive newsletters offered on our Sites, we require an e-mail address from you as well as information that allows us to verify that you are the owner of the specified e-mail address and agree to receive the newsletter. Further data will not be collected. We use this data exclusively for sending the requested information and do not pass it on to third parties, except as described in this privacy policy. You can revoke your consent to the storage of the data, the e-mail address and their use for sending the newsletter at any time, for example via the "unsubscribe link" in the newsletter. -### Processing of personal data in connection with the use of our products +### Processing of personal data when applying for a job with us + +Our Sites can generally be visited without registration. If you apply for a job with us, we may collect and process according to the [Privacy policy for the ZITADEL employer branding and recruitment](https://jobs.zitadel.com/privacy-policy). You may request and delete your data with the links on our [data & privacy page](https://jobs.zitadel.com/data-privacy). + +### Processing of personal data in connection with the use of our Services The use of our services is generally only possible with registration. During registration and in the course of using the services, we collect and process various personal data. In particular, the following personal data are part of the processing: - + +import { PiiTable } from "../../../src/components/pii_table"; + + Unless otherwise mentioned, the nature and purpose of the processing is as follows: -The data is uploaded by customers in our services or collected by us based on requests from users. The personal data is processed by us exclusively for the provision of the requested services or the use of the agreed services. +The data is uploaded by customers in our Services or collected by us based on requests from users. The personal data is processed by us exclusively for the provision of the requested Services or the use of the agreed Services. The fulfillment of the contract includes in particular, but is not limited to, the processing of personal data for the purpose of: -- Authentication and authorization of users -- Storage and processing of user actions in the audit trail -- Processing of personal data and login information -- Verification of communication means -- Communication regarding service interruptions or service changes +* Authentication and authorization of users +* Storage and processing of user actions in the audit trail +* Processing of personal data and login information +* Verification of communication means +* Communication regarding service interruptions or service changes ## Disclosure to third parties ### Third party sub-processors -We use third-party services to provide the website and our offers. An up-to-date list of all the providers we use and their areas of activity can be found on our [list of involved and approved sub-processors](../subprocessors). +We use third-party services to provide the website and our offers. An up-to-date list of all the providers we use and their areas of activity can be found on our [Trust Center](/trust). ### External payment providers -This website uses external payment service providers through whose platforms users and we can make payment transactions. For example via - -- [Stripe](https://stripe.com/ch/privacy) -- [Bexio AG](https://www.bexio.com/de-CH/datenschutz) +This Site uses external payment service providers through whose platforms users and we can make payment transactions. For example, via [Stripe](https://stripe.com/ch/privacy). As an alternative, we offer customers the option to pay by invoice instead of using external payment providers. However, this may require a positive credit check in advance. @@ -105,91 +129,166 @@ For payment transactions, the terms and conditions and the data protection notic ### Law enforcement -We disclose personal information to law enforcement agencies, investigative authorities or in legal proceedings to the extent we are required to do so by law or when necessary to protect our rights or the rights of users. +We disclose personal data to law enforcement agencies, investigative authorities or in legal proceedings to the extent we are required to do so by law or when necessary to protect our rights or the rights of users. ## Cookies -Our websites use cookies. These are small text files that make it possible to store specific information related to the user on the user's terminal device while the user is using the website. Cookies enable us, in particular, to offer a single sign-on procedure, to control the performance of our services, but also to make our offer more customer-friendly. Cookies remain stored beyond the end of a browser session and can be retrieved when the user visits the site again. +Our Sites use cookies. These are small text files that make it possible to store specific information related to the user on the user's terminal device while the user is using the website. Cookies enable us, in particular, to offer a single sign-on procedure, to control the performance of our Services, but also to make our offer more customer-friendly. Cookies remain stored beyond the end of a browser session and can be retrieved when the user visits the site again. -In particular, we use the following cookies to provide our services: - -When you use our services, we may collect information about your visit, including via cookies, beacons, invisible tags, and similar technologies (collectively “cookies”) in your browser and on emails sent to you. -This information may include Personal Information, such as your IP address, web browser, device type, and the web pages that you visit just before or just after you use the services, as well as information about your interactions with the services, such as the date and time of your visit, and where you have clicked. +When you use our Services, we may collect information about your visit, including via cookies, beacons, invisible tags, and similar technologies (collectively “cookies”) in your browser and on emails sent to you. This information may include personal data, such as your IP address, web browser, device type, and the web pages that you visit just before or just after you use the Services, as well as information about your interactions with the Services, such as the date and time of your visit, and where you have clicked. ### Necessary cookies -Some cookies are strictly necessary to make our services available to you. -We cannot provide you with our services without this type of cookies. +Some cookies are strictly necessary to make our Services available to you. We cannot provide you with our Services without this type of cookies. Necessary cookies provide basic functionality such as: -- Session Management -- Single Sign-On -- Rate Limiting -- DDoS Mitigation -- Remembering Preferences +* Session Management +* Single Sign-On +* Rate Limiting +* DDoS Mitigation +* Remembering Preferences ### Analytical cookies -We also use cookies for website analytics purposes in order to operate, maintain, and improve the services for you. -We use Google Analytics 4 to collect and process certain analytics data on our behalf. -Google Analytics helps us understand how you engage with the services and may also collect information about your use of other websites, apps, and online resources. -We don't use google analytics on customer instances of ZITADEL, only on our public websites and customer portal. +We also use cookies for website analytics purposes in order to operate, maintain, and improve the Services for you. We use Google Analytics 4 and PostHog to collect and process certain analytics data on our behalf. Google Analytics and PostHog helps us understand how you engage with the Services and may also collect information about your use of other websites, apps, and online resources. -You can learn about Google’s practices by going to https://www.google.com/policies/privacy/partners/ and opt out by managing your cookie consent through our services or an third-party tool of your choice. +You can learn about the analytics providers' practices by going to -If you do not want us to use cookies during your visit, you can disable their use in your browser settings. -In this case, certain parts of our website (e.g. language selection) may not function or may not function fully. -Where required by applicable law, we obtain your consent to use cookies. +* [https://www.google.com/policies/privacy/partners/](https://www.google.com/policies/privacy/partners/) +* [https://posthog.com/privacy](https://posthog.com/privacy) +* [https://legal.hubspot.com/privacy-policy](https://legal.hubspot.com/privacy-policy) +* [https://www.commonroom.io/privacy-policy/](https://www.commonroom.io/privacy-policy/) + +and opt out by managing your cookie consent through our Services or a third-party tool of your choice. + +If you do not want us to use cookies during your visit, you can disable their use in your browser settings. In this case, certain parts of our Sites (e.g. language selection) may not function or may not function fully. Where required by applicable law, we obtain your consent to use cookies. + +## How we protect personal data + +Personal data is maintained on our servers or those of our service providers, and is accessible by authorized employees, representatives, and agents as necessary for the purposes described in this privacy policy. + +We maintain a range of physical, electronic, and procedural safeguards designed to help protect personal data. While we attempt to protect your personal data in our possession, we cannot guarantee at all times the security of the data as no method of transmission over the internet or security system is perfect. + +If you choose to remain logged in, you should be aware that anyone with access to your device will be able to access your account and we therefore strongly recommend that you take appropriate steps to protect against unauthorized access to, and use, of your account. Please also notify us as soon as possible if you suspect any unauthorized use of your account or password. ## Rights of data subjects +Depending on your location and subject to applicable law, you may have the following rights regarding the personal data we process: + ### Right to information -Any person affected by the processing has the right to obtain information from the responsible data processor at any time about the personal data stored about him or her. +You have the right to know what personal data we hold and process about you and to access such personal data. ### Right to rectification -Every person affected by the processing has the right to demand the correction of inaccurate personal data concerning him or her. Furthermore, the data subject has the right to request the completion of incomplete personal data, taking into account the purposes of the processing. +You have the right to request the correction of inaccurate personal data concerning you. ### Right to erasure (right to be forgotten) -Any person affected by the processing has the right, in certain cases, to request from the responsible data processor to delete the personal data concerning him or her. +You have the right to request the deletion or erasure of the personal data concerning you. ### Right to restrict processing -Every person affected by the processing has the right in certain cases to request from the responsible data processor to restrict the processing. +You have the right to request to restrict the processing of your personal data in certain cases. ### Right to data portability -Every person affected by the processing has the right to receive the personal data concerning him or her in a structured, common and machine-readable format. He or she also has the right to have this data transferred to another data processor if the legal requirements are met. +You have the right to receive the personal data concerning you in a structured, common and machine-readable format, and to have this data transferred to another data processor if the legal requirements are met. ### Right to object -Every person affected by the processing has the right to object to the processing of personal data concerning him or her, insofar as we base the processing of his or her personal data on a balancing of interests. This is the case if the processing is not necessary, for example, to fulfill a contract or a legal obligation. +Depending on the circumstances, you have the right to object to the processing of personal data concerning you, insofar as we base the processing of your personal data on a balancing of interests. This is the case if the processing is not necessary, for example, to fulfill a contract or a legal obligation. -To exercise such an objection, the data subject must explain his or her reasons why we should not process his or her personal data as we have done. We will then review the situation and either stop or adjust the data processing or show the data subject our reasons for continuing the processing. +To exercise such an objection, please indicate your reasons why we should not process your personal data as we have done. We will then review the situation and either stop or adjust the data processing or explain our reasons for continuing the processing. ### Right to revoke consent under data protection law -Insofar as our processing is based on consent, the data subject has the right to revoke this consent at any time with effect for the future. +Insofar as our processing is based on consent, you have the right to revoke your consent at any time with effect. Withdrawing your consent will not affect the lawfulness of any processing we conducted prior to your withdrawal, nor will it affect processing of your personal data conducted in reliance on lawful processing grounds other than consent. ### Assertion of rights by the data subjects If you wish to exercise your rights, you may do so by contacting the above-mentioned contact person. -A data subject also has the right to lodge a complaint with the competent data protection authority. The competent data protection authority in Switzerland is the Federal Data Protection and Information Commissioner (www.edoeb.admin.ch). The competent data protection authorities of EU countries can be viewed at this link: [https://ec.europa.eu/justice/article-29/structure/data-protection-authorities/index\_en.htm](https://ec.europa.eu/justice/article-29/structure/data-protection-authorities/index_en.htm) +You can opt out of receiving marketing emails from us by following the unsubscribe link in the emails or by emailing us. If you choose to no longer receive marketing information, we may still communicate with you regarding such things as your security updates, product functionality, responses to service requests, or other transactional, non-marketing purposes. -## Note on data transfer abroad +If you have a concern about how we collect and use personal data, please contact us using the contact details provided at the beginning of this privacy policy. You also have the right to contact your local data protection authority if you prefer, such as: -Our websites and services make use of tools from companies based in countries outside of Switzerland or the EU/EEA, namely those based in the USA. When these tools are active, your personal data may be transferred to the servers of the respective companies abroad. We would like to point out that some of these countries, namely the USA, are not a safe third country in the sense of Swiss and EU data protection law. In these cases, we only transfer personal data after we have implemented the legally required measures for this, such as concluding standard contractual clauses on data protection or obtaining the consent of the data subjects. If interested, the documentation on these measures can be obtained from the contact person mentioned above. +* Data protection authorities in the European Economic Area (EEA): [https://edpb.europa.eu/about-edpb/board/members\_en](https://edpb.europa.eu/about-edpb/board/members_en); +* Swiss data protection authorities: [https://www.edoeb.admin.ch/edoeb/en/home/deredoeb/kontakt.html](https://www.edoeb.admin.ch/edoeb/en/home/deredoeb/kontakt.html); +* UK data protection authority: [https://ico.org.uk/global/contact-us/](https://ico.org.uk/global/contact-us/). + +## Additional Information for U.S. Residents + +Categories of personal data we collect and our purposes for collection and use +You can find a list of the categories of personal data that we collect in the section above titled “Processing of personal data, legal basis, storage period”. In the last 12 months, we collected the following categories of personal data depending on the Services used: + +* Identifiers and account information, such as the username and email address; +* Commercial information, such as information about transactions undertaken with us; +* Internet or other electronic network activity information, such as information about activity on our Site and Services. +* Geolocation information based on the IP address. +* Audiovisual information in pictures, audio, or video content that you may choose to submit to us. +* Professional or employment-related information or demographic information, but only if you explicitly provide it to us, such as by filling out a survey or by applying for a job with us. +* Inferences we make based on other collected data, for purposes such as recommending content and analytics. + +For details regarding the sources from which we obtain personal data, please see the “Processing of personal data, legal basis, storage period” section above. +We collect and use personal data for the business or commercial purposes described in the “Processing of personal data, legal basis, storage period” section above. + +Categories of personal data disclosed and categories of recipients + +We disclose the following categories of personal data for business or commercial purposes to the categories of recipients listed below: + +* We disclose identifiers with businesses, service providers, and third parties, such as analytics providers and social media networks. + * We disclose Internet or other network activity with businesses, service providers, and third parties, such as analytics providers and social media networks. + * We disclose geolocation information with businesses, service providers, and third parties such as advertising networks, analytics, and social media. + * We disclose payment information with businesses and service providers who process payments. + * We disclose commercial information with businesses, service providers, and third parties, such as analytics providers and social media networks. + * We disclose audiovisual information with businesses and service providers who help administer customer service and fraud or loss prevention services. + * We disclose inferences with businesses and service providers who help administer marketing and personalization. + +### Privacy rights + +Right to Opt-Out of Cookies and Sale/Sharing: Although we do not sell personal data for monetary value, our use of cookies and automated technologies may be considered a “sale” / “sharing” in certain states, such as California. Visitors to our US website can opt out of such third parties by clicking the “Manage cookie preferences” link at the bottom of our Site. The categories of personal data disclosed that may be considered a “sale” / “sharing” include identifiers, device information, Internet or other network activity, geolocation data, and commercial data. + +The categories of third parties to whom personal data was disclosed that may be considered “sale”/ “sharing” include data analytics providers and social media networks. + +We do not have actual knowledge that we sell or share the personal data of individuals under 16 years of age. + +If you are a resident of the State of Nevada, Chapter 603A of the Nevada Revised Statutes permits a Nevada resident to opt out of future sales of certain covered information that a website operator has collected or will collect about the resident. Although we do not currently sell covered information, please contact us to submit such a request. + +Right to Limit the Use of Sensitive Personal Information: We only collect sensitive personal information, as defined by applicable privacy laws, for the purposes allowed by law or with your consent. We do not use or disclose sensitive personal information except to provide you the Services or as otherwise permitted by law. We do not collect or process sensitive personal information for the purpose of inferring characteristics. + +Right to Access, Correct, and Delete Personal Data: Depending on your state of residence in the U.S., you may have: +(i) the right to request access to and receive details about the personal data we maintain and how we have processed it, including the categories of personal data, the categories of sources from which personal data is collected, the business or commercial purpose for collecting, selling, or sharing personal data, the categories of third parties to whom personal data is disclosed, and the specific pieces of personal data collected; +(ii) the right to delete personal data collected, subject to certain exceptions; +(iii) the right to correct inaccurate personal data. + +When you make a request, we will verify your identity by asking you to sign into your account or if necessary by requesting additional information from you. You may also make a request using an authorized agent. If you submit a rights request through an authorized agent, we may ask such agent to provide proof that you gave a signed permission to submit the request to exercise privacy rights on your behalf. We may also require you to verify your own identity directly with us or confirm to us that you otherwise provided such agent permission to submit the request. Once you have submitted your request, we will respond within the time frame permitted by the applicable law. + +If you have any questions or concerns, you may reach us by contacting using one of the contact details listed at the beginning of this privacy policy. + +Depending on your state of residence, you may be able to appeal our decision to your request regarding your personal data. To do so, please contact us by using one of the contact details listed at the beginning of this privacy policy. We respond to all appeal requests as soon as we reasonably can, and no later than legally required. + +We do not discriminate against customers who exercise any of their rights described in our privacy policy. + +California Shine the Light: Customers who are residents of California may request information concerning the categories of personal data (if any) we disclose to third parties or affiliates for their direct marketing purposes. If you would like more information, please submit a written request to us by using one of the contact details listed at the beginning of this privacy policy. + +Do Not Track signals: Most modern web browsers give you the option to send a 'Do Not Track' signal to the sites you visit, indicating that you do not wish to be tracked. However, there is currently no accepted standard for how a site should respond to this signal, and we do not take any action in response to this signal.‍ + +## Note on international data transfers + +Our Sites and Services make use of tools from companies based in countries outside of Switzerland or the EU/EEA, namely those based in the USA. When these tools are active, your personal data may be transferred to the servers of the respective companies abroad. If you are using the Site or Services from outside the United States, your personal data may be processed in a foreign country, where privacy laws may be less stringent than the laws in your country. In these cases, we only transfer personal data after we have implemented the legally required measures for this, such as concluding standard contractual clauses on data protection or obtaining the consent of the data subjects. If interested, the documentation on these measures can be obtained from the contact person mentioned above. By submitting your personal data to us you agree to the transfer, storage, and processing of your personal data in a country other than your country of residence including, but not necessarily limited to, the United States. We actively try to minimize the use of tools from companies located in countries without equivalent data protection, however, due to the lack of alternatives, this is currently not always feasible without major inconvenience. If you have any concerns, please contact us directly and we will try to find a mutual solution for your needs. -## Changes +## Children's Privacy -We may amend this privacy policy at any time without prior notice. Always the current version published on our website applies to users and customers of our website and services. Insofar as the data protection declaration is part of an agreement with you, we will inform you of the change by e-mail or other suitable means in the event of an update. +Our Site is not intended for or directed to children under the age of 14. We do not knowingly collect personal data directly from children under the age of 14 without parental consent. If we become aware that a child under the age of 14 has provided us with personal data, we will delete the information from our records. -## Questions about data processing by us +## Changes to this Privacy Policy -If you have any questions about our data processing, please email us or contact the person in our organization listed at the beginning of this privacy statement directly. +We may revise this privacy policy from time to time and will post the date it was last updated at the top of this privacy policy. We will provide additional notice to you if we make any changes that materially affect your privacy rights. + +## Contact us + +If you have any questions about our data processing, please email us or contact us by using the contact details listed at the beginning of this privacy notice. diff --git a/docs/docs/product/_beta-ga.mdx b/docs/docs/product/_beta-ga.mdx new file mode 100644 index 0000000000..229f94cdf5 --- /dev/null +++ b/docs/docs/product/_beta-ga.mdx @@ -0,0 +1 @@ +This describes the progression of features from a limited, pre-release testing phase (Beta) to their official, stable, and publicly available version (General Availability), ready for widespread use, with the specific transitions listed below. \ No newline at end of file diff --git a/docs/docs/product/_breaking-changes.mdx b/docs/docs/product/_breaking-changes.mdx new file mode 100644 index 0000000000..dd903a0f2e --- /dev/null +++ b/docs/docs/product/_breaking-changes.mdx @@ -0,0 +1 @@ +These are modifications to existing functionalities that may require users to alter their current implementation or usage to ensure continued compatibility; see the list below for specifics. \ No newline at end of file diff --git a/docs/docs/product/_deprecated.mdx b/docs/docs/product/_deprecated.mdx new file mode 100644 index 0000000000..e5848d68f0 --- /dev/null +++ b/docs/docs/product/_deprecated.mdx @@ -0,0 +1 @@ +This announces that specific existing features are being phased out and are scheduled for future removal, often because they have become outdated or are being replaced by an improved alternative; please see the deprecated items listed below. \ No newline at end of file diff --git a/docs/docs/product/_new-feature.mdx b/docs/docs/product/_new-feature.mdx new file mode 100644 index 0000000000..1877a1d690 --- /dev/null +++ b/docs/docs/product/_new-feature.mdx @@ -0,0 +1 @@ +These introduce brand-new functionalities or capabilities, expanding the product's offerings and value to users, as detailed below. \ No newline at end of file diff --git a/docs/docs/product/_sdk_v3.mdx b/docs/docs/product/_sdk_v3.mdx new file mode 100644 index 0000000000..76040640a2 --- /dev/null +++ b/docs/docs/product/_sdk_v3.mdx @@ -0,0 +1,32 @@ +import NewFeature from './_new-feature.mdx'; + +An initial version of our Software Development Kit (SDK) will be published. +To better align our versioning with the [ZITADEL core](#zitadel-core), the SDK will be released as version 3.x. +This strategic versioning will ensure a more consistent and intuitive development experience across our entire ecosystem. + +
+ New Features + + + +
+ Machine User Authentication Methods + + This feature introduces robust and standardized authentication methods for your machine users, enabling secure automated access to your resources. + + Choose from the following authentication methods: + - **Private Key JWT Authentication**: Enhance security by using asymmetric cryptography. A client with a registered public key can generate and sign a JSON Web Token (JWT) with its private key to authenticate. + - **Client Credentials Grant**: A simple and direct method for machine-to-machine authentication where the client confidentially provides its credentials to the authorization server in exchange for an access token. + - **Personal Access Tokens (PATs)**: Ideal for individual developers or specific scripts, PATs offer a convenient way to create long-lived, revocable tokens with specific scopes, acting as a substitute for a password. + +
+ +
+ Zitadel APIs Wrapper + + This SDK provides a convenient client for interacting with the ZITADEL APIs, simplifying how you manage resources within your instance. + + Currently, the client is tailored for machine-to-machine communication, enabling machine users to authenticate and manage ZITADEL resources programmatically. + Please note that this initial version is focused on API calls for automated tasks and does not yet include support for human user authentication flows like OAuth or OIDC. +
+
diff --git a/docs/docs/product/release-cycle.mdx b/docs/docs/product/release-cycle.mdx new file mode 100644 index 0000000000..f38c81a36d --- /dev/null +++ b/docs/docs/product/release-cycle.mdx @@ -0,0 +1,63 @@ +--- +title: Release Cycle +sidebar_label: Release Cycle +--- + +We release a new major version of our software every three months. This predictable schedule allows us to introduce significant features and enhancements in a structured way. + +This cadence provides enough time for thorough development and rigorous testing. Before each stable release, we engage with our community and customers to test and stabilize the new version. This ensures high quality and reliability. For our customers, this approach creates a clear and manageable upgrade path. + +While major changes are reserved for these three-month releases, we address urgent needs by backporting smaller updates, such as critical bug and security fixes, to earlier versions. This allows us to provide essential updates without altering the predictable rhythm of our major release cycle. + + +![Release Cycle](/img/product/release-cycle.png) + +## Preparation + +The first quarter of our cycle is for Preparation and Planning, where we create the blueprint for the upcoming major release. +During this time, we define the core architecture, map out the implementation strategy, and finalize the design for the new features. + + +## Implementation + +The second month is the Implementation and Development Phase, where our engineers build the features defined in the planning stage. + +During this period, we focus on writing the code for the new enhancements. +We also integrate accepted contributions from our community and create the necessary documentation alongside the development work. +This phase concludes when the new version is feature-complete and ready to enter the testing phase. + +## Release Candidate (RC) + +The first month of the third quarter is for the Release Candidate (RC) and Stabilization Phase. +At the beginning of this month, we publish a Release Candidate version. +This is a feature-complete version that we believe is ready for public release, made available to our customers and community for widespread testing. + +This phase is critical for ensuring the quality of the final release. We have two main objectives: +- **Community Feedback and Bug Fixing**: This is when we rely on your feedback. By testing the RC in your own environments, you help us find and fix bugs and other issues we may have missed. Your active participation is crucial for stabilizing the new version. +- **Enhanced Internal Testing**: While the community provides feedback, our internal teams conduct enhanced quality assurance. This includes in-depth feature validation, rigorous testing of upgrade paths from previous versions, and comprehensive performance and benchmark testing. + +The goal of this phase is to use both community feedback and internal testing to ensure the new release is robust, bug-free, and performs well, so our customers can upgrade with confidence. + +## General Availability (GA) / Stable + +Following the month-long Release Candidate and Stabilization phase, we publish the official General Availability (GA) / Stable Release. +This is the final, production-ready version of our software that has been thoroughly tested by both our internal teams and the community. + +This release is available to everyone, and we recommend that customers begin reviewing the official upgrade path for their production environments. +The deployment of this new major version to our cloud services also happens at this time. + +**Ongoing Maintenance: Minor and Patch Releases** +Once a major version becomes stable, we provide ongoing support through back-porting. This means we carefully select and apply critical updates from our main development track to the stable release, ensuring it remains secure and reliable. These updates are delivered in two ways: + +- Minor Releases: These include simple features and enhancements from the next release cycle that are safe to add, requiring no major refactoring or large database migrations. +- Patch Releases: These are focused exclusively on high-priority bug and security fixes to address critical issues promptly. + +This process ensures that you can benefit from the stability of a major release while still receiving important updates and fixes in a timely manner. + +## Deprecated + +Each major version is actively supported for a full release cycle after its launch. This means that approximately six months after its initial stable release, a version enters its deprecation period. + +Once a version is deprecated, we strongly encourage all self-hosted customers to upgrade to a newer version as soon as possible to continue receiving the latest features, improvements, and bug fixes. + +For our enterprise customers, we may offer extended support by providing critical security fixes for a deprecated version beyond the standard six-month lifecycle. This extended support is evaluated on a case-by-case basis to ensure a secure and manageable transition for large-scale deployments. \ No newline at end of file diff --git a/docs/docs/product/roadmap.mdx b/docs/docs/product/roadmap.mdx new file mode 100644 index 0000000000..b61323fa90 --- /dev/null +++ b/docs/docs/product/roadmap.mdx @@ -0,0 +1,705 @@ +--- +title: Zitadel Release Versions and Roadmap +sidebar_label: Release Versions and Roadmap +--- + + +import NewFeature from './_new-feature.mdx'; +import BreakingChanges from './_breaking-changes.mdx'; +import Deprecated from './_deprecated.mdx'; +import BetaToGA from './_beta-ga.mdx'; +import SDKv3 from './_sdk_v3.mdx'; + + +## Timeline and Overview + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
20252026
Q1Q2Q3Q4Q1Q2Q3Q4
JanFebMarAprMayJunJulAugSepOctNovDecJanFebMarAprMayJunJulAugSepOctNovDec
Zitadel Versions
[v2.x](/docs/product/roadmap#v2x)GA / Stable Deprecated
[v3.x](/docs/product/roadmap#v3x)ImplementationRCGA / Stable Deprecated
[v4.x](/docs/product/roadmap#v4x)ImplementationRCGA / Stable Deprecated
[v5.x](/docs/product/roadmap#v5x)ImplementationRCGA / Stable Deprecated
+ +For more detailed description about the different stages and the release cycle check out the following Page: [Release Cycle](/docs/product/release-cycle) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
25-Q125-Q225-Q325-Q4
Zitadel Core
+ [v2.x](/docs/product/roadmap#v2x) + + [v3.x](/docs/product/roadmap#v3x) +
    +
  • + Actions V2 +
  • +
  • + Removed CockroachDB Support +
  • +
  • + License Change +
  • +
  • + Login v2 +
      +
    • + Initial Release +
    • +
    • + All standard authentication methods +
    • +
    • + OIDC & SAML +
    • +
    +
  • +
+
+ [v4.x](/docs/product/roadmap#v4x) +
    +
  • Resource API
  • +
  • + Login v2 as default + +
      +
    • + Device Authorization Flow +
    • +
    • + LDAP IDP +
    • +
    • + JWT IDP +
    • +
    • + Custom Login UI Texts +
    • +
    +
  • +
+ +
+ [v5.x](/docs/product/roadmap#v5x) +
    +
  • Analytics
  • +
  • User Groups
  • +
  • User Uniqueness on Instance Level
  • +
  • Remove Required Fields from User
  • +
+
Zitadel SDKs
+ + + + +
    +
  • + [Initial Version of PHP SDK](/docs/product/roadmap#v3x-1) +
  • +
  • + [Initial Version of Java SDK](/docs/product/roadmap#v3x-2) +
  • +
  • + [Initial Version of Ruby SDK](/docs/product/roadmap#v3x-3) +
  • +
  • + [Initial Version of Python SDK](/docs/product/roadmap#v3x-4) +
  • +
+
+
+ +## Zitadel Core + +Check out all [Zitadel Release Versions](https://github.com/zitadel/zitadel/releases) + +### v2.x + +**Current State**: General Availability / Stable + +**Release**: [v2.x](https://github.com/zitadel/zitadel/releases?q=v2.&expanded=true) + +In Zitadel versions 2.x and earlier, new releases were deployed with a minimum frequency of every two weeks. +This practice resulted in a significant number of individual versions. +To review the features and bug fixes for these releases, please consult the linked release information provided above. + +### v3.x + +ZITADEL v3 is here, bringing key changes designed to empower your identity management experience. +This release transitions our licensing to AGPLv3, reinforcing our commitment to open and collaborative development. +We've streamlined our database support by removing CockroachDB. +Excitingly, v3 introduces the foundational elements for Actions V2, opening up a world of possibilities for tailoring and extending ZITADEL to perfectly fit your unique use cases. + +**Current State**: General Availability / Stable + +**Release**: [v3.x](https://github.com/zitadel/zitadel/releases?q=v3.&expanded=true) + +**Blog**: [Zitadel v3: AGPL License, Streamlined Releases, and Platform Updates](https://zitadel.com/blog/zitadel-v3-announcement) + + +
+ New Features + + + +
+ Actions V2 + + Zitadel Actions V2 empowers you to customize Zitadel's workflows by executing your own logic at specific points. You define external Endpoints containing your code and configure Targets and Executions within Zitadel to trigger them based on various conditions and events. + + Why we built it: To provide greater flexibility and control, allowing you to implement custom business rules, automate tasks, enrich user data, control access, and integrate with other systems seamlessly. Actions V2 enables you to tailor Zitadel precisely to your unique needs. + + Read more in our [documentation](https://zitadel.com/docs/concepts/features/actions_v2) +
+ +
+ License Change Apache 2.0 to AGPL3 + + Zitadel is switching to the AGPL 3.0 license to ensure the project's sustainability and encourage community contributions from commercial users, while keeping the core free and open source. + + Read more about our [decision](https://zitadel.com/blog/apache-to-agpl) +
+
+ +
+ Breaking Changes + + + +
+ CockroachDB Support removed + + After careful consideration, we have made the decision to discontinue support for CockroachDB in Zitadel v3 and beyond. + While CockroachDB is an excellent distributed SQL database, supporting multiple database backends has increased our maintenance burden and complicated our testing matrix. + Check out our [migration guide](https://zitadel.com/docs/self-hosting/manage/cli/mirror) to migrate from CockroachDB to PostgreSQL. + + More details can be found [here](https://github.com/zitadel/zitadel/issues/9414) +
+ +
+ Actions API v3 alpha removed + + With the current release we have published the Actions V2 API as a beta version, and got rid of the previously published alpha API. + Check out the [new API](http://localhost:3000/docs/apis/resources/action_service_v2) + +
+
+ +### v4.x + +**Current State**: Implementation + + +
+ New Features + + + +
+ Resource API (v2) + + We are revamping our APIs to improve the developer experience. + Currently, our use-case-based APIs are complex and inconsistent, causing confusion and slowing down integration. + To fix this, we're shifting to a resource-based approach. + This means developers will use consistent endpoints (e.g., /users) to manage resources, regardless of their own role. + This change, along with standardized naming and improved documentation, will simplify integration, accelerate development, and create a more intuitive experience for our customers and community. + + Resources integrated in this release: + - Instances + - Organizations + - Projects + - Users + + For more details read the [Github Issue](https://github.com/zitadel/zitadel/issues/6305) +
+ +
+ Login V2 + + Our new login UI has been enhanced with additional features, bringing it to feature parity with Version 1. + +
+ Device Authorization Flow + + The Device Authorization Grant is an OAuth 2.0 flow designed for devices that have limited input capabilities (like smart TVs, gaming consoles, or IoT devices) or lack a browser. + + Read our docs about how to integrate your application using the [Device Authorization Flow](https://zitadel.com/docs/guides/integrate/login/oidc/device-authorization) +
+ +
+ LDAP IDP + + This feature enables users to log in using their existing LDAP (Lightweight Directory Access Protocol) credentials. + It integrates your system with an LDAP directory, allowing it to act as an Identity Provider (IdP) solely for authentication purposes. + This means users can securely access the service with their familiar LDAP username and password, streamlining the login process. +
+ +
+ JWT IDP + + This "JSON Web Token Identity Provider (JWT IdP)" feature allows you to use an existing JSON Web Token (JWT) from another system (like a Web Application Firewall managing a session) as a federated identity for authentication in new applications managed by ZITADEL. + + Essentially, it enables session reuse by letting ZITADEL trust and validate a JWT issued by an external source. This allows users already authenticated in an existing system to seamlessly access new applications without re-logging in. + + Read more in our docs about how to login users with [JWT IDP](https://zitadel.com/docs/guides/integrate/identity-providers/jwt_idp) +
+ +
+ Custom Login UI Texts + + This feature provides customers with the flexibility to personalize the user experience by customizing various text elements across different screens of the login UI. Administrators can modify default messages, labels, and instructions to align with their branding, provide specific guidance, or cater to unique regional or organizational needs, ensuring a more tailored and intuitive authentication process for their users. +
+
+
+ + +
+ General Availability + + + +
+ Hosted Login v2 + + We're officially moving our new Login UI v2 from beta to General Availability. + Starting now, it will be the default login experience for all new customers. + With this release, 8.0we are also focused on implementing previously missing features, such as device authorization and LDAP IDP support, to make the new UI fully feature-complete. + + - [Hosted Login V2](http://localhost:3000/docs/guides/integrate/login/hosted-login#hosted-login-version-2-beta) +
+ +
+ Web Keys + + Web Keys in ZITADEL are used to sign and verify JSON Web Tokens (JWT). + ID tokens are created, signed and returned by ZITADEL when a OpenID connect (OIDC) or OAuth2 authorization flow completes and a user is authenticated. + Based on customer and community feedback, we've updated our key management system. You now have full manual control over key generation and rotation, instead of the previous automatic process. + + Read the full description about Web Keys in our [Documentation](https://zitadel.com/docs/guides/integrate/login/oidc/webkeys). +
+ +
+ SCIM 2.0 Server - User Resource + + The Zitadel SCIM v2 service provider interface enables seamless integration of identity and access management (IAM) systems with Zitadel, following the System for Cross-domain Identity Management (SCIM) v2.0 specification. + This interface allows standardized management of IAM resources, making it easier to automate user provisioning and deprovisioning. + + - [SCIM 2.0 API](https://zitadel.com/docs/apis/scim2) + - [Manage Users Guide](https://zitadel.com/docs/guides/manage/user/scim2) +
+ +
+ Caches + + ZITADEL supports the use of a caches to speed up the lookup of frequently needed objects. + As opposed to HTTP caches which might reside between ZITADEL and end-user applications, the cache build into ZITADEL uses active invalidation when an object gets updated. + Another difference is that HTTP caches only cache the result of a complete request and the built-in cache stores objects needed for the internal business logic. + For example, each request made to ZITADEL needs to retrieve and set instance information in middleware. + + Read more about Zitadel Caches [here](https://zitadel.com/docs/self-hosting/manage/cache) +
+
+ +### v5.x + +**Current State**: Planning + +
+ New Features + + + +
+ Analytics + + We provide comprehensive and insightful analytics capabilities that empower you with the information needed to understand platform usage, monitor system health, and make data-driven decisions. + +
+ Daily Active Users (DAU) & Monthly Active Users (MAU) + + Administrators need to track user activity to understand platform usage and identify trends. + This feature provides basic metrics for daily and monthly active users, allowing for filtering by date range and scope (instance-wide or within a specific organization). + The metrics should ensure that each user is counted only once per day or month, respectively, regardless of how many actions they performed. + This minimal feature serves as a foundation for future expansion into more detailed analytics. + + For more details track our [github issue](https://github.com/zitadel/zitadel/issues/7506). +
+ +
+ Resource Count Metrics + + To effectively manage a Zitadel instance, administrators need to understand resource utilization. + This feature provides metrics for resource counts, including organizations, users (with filtering options), projects, applications, and authorizations. + For users, we will offer filters to retrieve the total count, counts per organization, and counts by user type (human or machine). + These metrics will provide administrators with valuable insights into the scale and complexity of their Zitadel instance. + + + For more details track our [github issue](https://github.com/zitadel/zitadel/issues/9709). +
+ +
+ Operational Metrics + + To empower customers to better manage and optimize their Zitadel instances, we will provide access to detailed operational metrics. + This data will help customers identify potential issues, optimize performance, and ensure the stability of their deployments. + The provided data will encompass basic system information, infrastructure details, configuration settings, error reports, and the health status of various Zitadel components, accessible via a user interface or an API. + + + For more details track our [github issue](https://github.com/zitadel/zitadel/issues/9476). + +
+
+ +
+ User Groups + + Administrators will be able to define groups within an organization and assign users to these groups. + More details about the feature can be found [here](https://github.com/zitadel/zitadel/issues/9702) +
+ +
+ User Uniqueness on Organization Level + + Administrators will be able to define weather users should be unique across the instance or within an organization. + This allows managing users independently and avoids conflicts due to shared user identifiers. + Example: The user with the username user@gmail.com can be created in the Organization "Customer A" and "Customer B" if uniqueness is defined on the organization level. + + Stay updated on the progress and details on our [GitHub Issue](https://github.com/zitadel/zitadel/issues/9535) +
+ + +
+ Remove Required Fields + + Currently, the user creation process requires several fields, such as email, first name, and last name, which can be restrictive in certain scenarios. This feature allows administrators to create users with only a username, making other fields optional. + This provides flexibility for systems that don't require complete user profiles upon initial creation for example simplified onboarding flows. + + For more details check out our [GitHub Issue](https://github.com/zitadel/zitadel/issues/4386) +
+
+ +
+ Feature Deprecation + + + +
+ Actions V1 +
+
+ +
+ Breaking Changes + + + +
+ Hosted Login v1 will be removed +
+ + +
+ Zitadel APIs v1 will be removed +
+
+ + +### v6.x + +
+ New Features + + + +
+ Basic Threat Detection Framework + + This initial version of our Threat Detection Framework is designed to enhance the security of your account by identifying and challenging potentially anomalous user behavior. + When the system detects unusual activity, it will present a challenge, such as a reCAPTCHA, to verify that the user is legitimate and not a bot or malicious actor. + Security administrators will also have the ability to revoke user sessions based on the output of the threat detection model, providing a crucial tool to mitigate potential security risks in real-time. + + We are beginning with a straightforward reCAPTCHA-style challenge to build and refine the core framework. + This foundational step will allow us to gather insights into how the system performs and how it can be improved. + Future iterations will build upon this groundwork to incorporate more sophisticated detection methods and a wider range of challenge and response mechanisms, ensuring an increasingly robust and intelligent security posture for all users. + + More details can be found in the (GitHub Issue](https://github.com/zitadel/zitadel/issues/9707) +
+ +
+ SCIM Outbound + + Automate user provisioning to your external applications with our new SCIM Client. + This feature ensures users are automatically created in downstream systems before their first SSO login, preventing access issues and streamlining onboarding. + + It also synchronizes user lifecycle events, so changes like deactivations or deletions are instantly reflected across all connected applications for consistent and secure access management. + The initial release will focus on provisioning the user resource. + + More details can be found in the (GitHub Issue](https://github.com/zitadel/zitadel/issues/6601) +
+ +
+ Analytics + + We provide comprehensive and insightful analytics capabilities that empower you with the information needed to understand platform usage, monitor system health, and make data-driven decisions. + +
+ Login Insights: Successful and Failed Login Metrics + + To enhance security monitoring and gain insights into user authentication patterns, administrators need access to login metrics. + This feature provides data on successful and failed login attempts, allowing for filtering by time range and level (overall instance, within a specific organization, or for a particular application). + This will enable administrators to detect suspicious login activity, analyze authentication trends, and proactively address potential security concerns. + + For more details track our [GitHub issue](https://github.com/zitadel/zitadel/issues/9711). +
+
+ +
+ Impersonation: External Token Exchange + + This feature expands our existing impersonation capabilities to support seamless and secure integration with external, third-party applications. + Currently, our platform supports impersonation for internal use cases, allowing administrators or support staff to obtain a temporary token for an end-user to troubleshoot issues or provide assistance within applications that already use ZITADEL for authentication. (You can find more details in our [existing documentation](/docs/guides/integrate/token-exchange)). + + The next evolution of this feature will focus on external applications. + This enables scenarios where a user, already authenticated in a third-party system (like their primary e-banking portal), can seamlessly access a connected application that is secured by ZITADEL without needing to log in again. + + For example, a user in their e-banking app could click to open an integrated "Budget Planning" tool that relies on ZITADEL for access. + Using a secure token exchange, the budget app will grant the user a valid session on their behalf, creating a smooth, uninterrupted user experience while maintaining a high level of security. + This enhancement bridges the authentication gap between external platforms and ZITADEL-powered applications. +
+
+ +### Future Vision / Upcoming Features + +#### Fine Grained Authorization + +We're planning the future of Zitadel and fine-grained authorization is high on our list. +While Zitadel already offers strong role-based access (RBAC), we know many of you need more granular control. + +**What is Fine-Grained Authorization?** + +It's about moving beyond broad roles to define precise access based on: + +- Attributes (ABAC): User details (department, location), resource characteristics (sensitivity), or context (time of day). +- Relationships (ReBAC): Connections between users and resources (e.g., "owner" of a document, "manager" of a team). +- Policies (PBAC): Explicit rules combining attributes and relationships. + +**Why Explore This?** + +Fine-grained authorization can offer: +- Tighter Security: Minimize access to only what's essential. +- Greater Flexibility: Adapt to complex and dynamic business rules. +- Easier Compliance: Meet strict regulatory demands. +- Scalable Permissions: Manage access effectively as you grow. + +**We Need Your Input!** 🗣️ + +As we explore the best way to bring this to Zitadel, tell us: +- Your Use Cases: Where do you need more detailed access control than standard roles provide? +- Preferred Models: Are you thinking attribute-based, relationship-based, or something else? +- Integration Preferences: + - A fully integrated solution within Zitadel? + - Or integration with existing authorization vendors (e.g. openFGA, cerbos, etc.)? + +Your feedback is crucial for shaping our roadmap. + +🔗 Share your thoughts and needs in our [discussion forum](https://discord.com/channels/927474939156643850/1368861057669533736) + +#### Threat Detection + +We're taking the next step in securing your applications by exploring a new Threat Detection framework for Zitadel. +Our goal is to proactively identify and stop malicious activity in real-time. + +**Our First Step: A Modern reCAPTCHA Alternative** +We will begin by building a system to detect and mitigate malicious bots, serving as a smart, privacy-focused alternative to CAPTCHA. +This initial use case will help us combat credential stuffing, spam registrations, and other automated attacks, forming the foundation of our larger framework. + +**How We Envision It** + +Our exploration is focused on creating an intelligent system that: +- **Analyzes Signals**: Gathers data points like IP reputation, device characteristics, and user behavior to spot suspicious activity. +- **Uses AI/**: Trains models to distinguish between legitimate users and bots, reducing friction for real users. +- **Mitigates Threats**: Enables flexible responses when a threat is detected, such as blocking the attempt, requiring MFA, or sending an alert. + +**Help Us Shape the Future** 🤝 + +As we design this framework, we need to know: +- What are your biggest security threats today? +- What kind of automated responses (e.g., block, notification) would be most useful for you? +- What are your key privacy or compliance concerns regarding threat detection? + +Your feedback will directly influence our development and ensure we build a solution that truly meets your needs. + +🔗 Join the conversation and share your insights [here](https://discord.com/channels/927474939156643850/1375383775164235806) + + +#### The Role of AI in Zitadel + +As we look to the future, we believe Artificial Intelligence will be a critical tool for enhancing both user experience and security within Zitadel. +Our vision for AI is focused on two key areas: providing intelligent, contextual assistance and building a collective defense against emerging threats. + +1. **AI-Powered Support** + + We want you to get fast, accurate answers to your questions without ever having to leave your workflow. + To achieve this, we are integrating an AI-powered support assistant trained on our knowledge base, including our documentation, tutorials, and community discussions. + + Our rollout is planned in phases to ensure we deliver a helpful experience: + - **Phase 1 (Happening Now)**: We are currently testing a preliminary version of our AI bot within our [community channels](https://discord.com/channels/927474939156643850/1357076488825995477). This allows us to gather real-world questions and answers, refining the AI's accuracy and helpfulness based on direct feedback. + - **Phase 2 (Next Steps)**: Once we are confident in its capabilities, we will integrate this AI assistant directly into our documentation. You'll be able to ask complex questions and get immediate, well-sourced answers. + - **Phase 3 (The Ultimate Goal)**: The final step is to embed the assistant directly into the Zitadel Console/Customer Portal. Imagine getting help based on the exact context of what you're doing—whether you're configuring an action, setting up a new organization, or integrating social login. + +2. **Decentralized AI for Threat Detection** + + Security threats are constantly evolving. + A threat vector that targets one customer today might target another tomorrow. + We believe in the power of collective intelligence to provide proactive security for everyone. + + This leads to our second major AI initiative: **decentralized model training** for our Threat Detection framework. + + Here’s how it would work: + - **Collective Data, Anonymously**: Customers across our cloud and self-hosted environments experience different user behaviors and threat vectors. We plan to offer an opt-in system where anonymized, non-sensitive data (like behavioral patterns and threat signals) can be collected from participating instances. + - **Centralized Training**: This collective, anonymized data will be used to train powerful, next-generation AI security models. With a much larger and more diverse dataset, these models can learn to identify subtle and emerging threats far more effectively than a model trained on a single instance's data. + - **Shared Protection**: These constantly improving models would then be distributed to all participating Zitadel instances. + + The result is a powerful security network effect. You could receive protection from a threat vector you haven't even experienced yet, simply because the system learned from an attack on another member of the community. + + + +## Zitadel Ecosystem + +### PHP SDK + +GitHub Repository: [PHP SDK](https://github.com/zitadel/client-php) + +#### v3.x + + + +### Java SDK + +GitHub Repository: [Java SDK](https://github.com/zitadel/client-java) + +#### v3.x + + + +### Ruby SDK + +GitHub Repository: [Ruby SDK](https://github.com/zitadel/client-ruby) + +#### v3.x + + + +### Python SDK + +GitHub Repository: [Python SDK](https://github.com/zitadel/client-python) + +#### v3.x + + diff --git a/docs/docs/sdk-examples/client-libraries/_category_.json b/docs/docs/sdk-examples/client-libraries/_category_.json new file mode 100644 index 0000000000..f47958cf1c --- /dev/null +++ b/docs/docs/sdk-examples/client-libraries/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Client Libraries", + "position": 2 +} diff --git a/docs/docs/sdk-examples/client-libraries/java.mdx b/docs/docs/sdk-examples/client-libraries/java.mdx new file mode 100644 index 0000000000..565d2d932a --- /dev/null +++ b/docs/docs/sdk-examples/client-libraries/java.mdx @@ -0,0 +1,194 @@ +--- +title: Java Client +sidebar_label: 'Java Client' +--- + + + + + + +
+ java logo + + This guide covers the official Zitadel Management API Client for the JVM (Java 11+), which allows you to programmatically manage resources in your Zitadel instance. +
+ +:::info +**This is a Management API Client, not an Authentication SDK.** + +This library is designed for server-to-server communication to manage your Zitadel instance (e.g., creating users, managing projects, and updating settings). It is **not** intended for handling end-user login flows in your web application. For user authentication, you should use a standard OIDC library like Spring Security. +::: + +The Zitadel Java Client provides an idiomatic way to access the full gamut of Zitadel's v2 Management APIs from your JVM-based backend applications. + +> Please be aware that this client library is currently in an **incubating stage**. +While it is available for use, the API and its functionality may evolve, potentially introducing +breaking changes in future updates. We advise caution when considering it for production environments. + +### Installation + +You can add the client library to your project using Maven by adding the following dependency to your `pom.xml` : + +```xml + + io.github.zitadel + client + 4.0.0-beta-1 + +``` + +### Using the SDK + +Your SDK offers three ways to authenticate with Zitadel. Each method has its +own benefits—choose the one that fits your situation best. + +#### 1. Private Key JWT Authentication + +**What is it?** +You use a JSON Web Token (JWT) that you sign with a private key stored in a +JSON file. This process creates a secure token. + +**When should you use it?** + +- **Best for production:** It offers strong security. +- **Advanced control:** You can adjust token settings like expiration. + +**How do you use it?** + +1. Save your private key in a JSON file. +2. Build the authenticator using the helper method. + +**Example:** + +```java +import com.zitadel.ApiException; +import com.zitadel.Zitadel; +import com.zitadel.model.UserServiceAddHumanUserRequest; +import com.zitadel.model.UserServiceAddHumanUserResponse; +import com.zitadel.model.UserServiceSetHumanEmail; +import com.zitadel.model.UserServiceSetHumanProfile; + +class Demo { + public static void main(String[] args) throws ApiException { + Zitadel zitadel = Zitadel.withPrivateKey("https://example.us1.zitadel.cloud", "path/to/jwt-key.json"); + + UserServiceAddHumanUserResponse response = zitadel.users.userServiceAddHumanUser( + new UserServiceAddHumanUserRequest() + .username("john.doe") + .profile(new UserServiceSetHumanProfile() + .givenName("John") + .familyName("Doe")) + .email(new UserServiceSetHumanEmail() + .email("john@doe.com")) + ); + System.out.println("User created: " + response); + } +} +``` + +#### 2. Client Credentials Grant + +**What is it?** +This method uses a client ID and client secret to get a secure access token, +which is then used to authenticate. + +**When should you use it?** + +- **Simple and straightforward:** Good for server-to-server communication. +- **Trusted environments:** Use it when both servers are owned or trusted. + +**How do you use it?** + +1. Provide your client ID and client secret. +2. Build the authenticator using the helper method. + +**Example:** + +```java +import com.zitadel.ApiException; +import com.zitadel.Zitadel; +import com.zitadel.model.UserServiceAddHumanUserRequest; +import com.zitadel.model.UserServiceAddHumanUserResponse; +import com.zitadel.model.UserServiceSetHumanEmail; +import com.zitadel.model.UserServiceSetHumanProfile; + +class Demo { + public static void main(String[] args) throws ApiException { + Zitadel zitadel = Zitadel.withClientCredentials("https://example.us1.zitadel.cloud", "id", "secret"); + + UserServiceAddHumanUserResponse response = zitadel.users.addHumanUser( + new UserServiceAddHumanUserRequest() + .username("john.doe") + .profile(new UserServiceSetHumanProfile() + .givenName("John") + .familyName("Doe")) + .email(new UserServiceSetHumanEmail() + .email("john@doe.com")) + ); + System.out.println("User created: " + response); + } +} +``` + +#### 3. Personal Access Tokens (PATs) + +**What is it?** +A Personal Access Token (PAT) is a pre-generated token that you can use to +authenticate without exchanging credentials every time. + +**When should you use it?** + +- **Easy to use:** Great for development or testing scenarios. +- **Quick setup:** No need for dynamic token generation. + +**How do you use it?** + +1. Obtain a valid personal access token from your account. +2. Build the authenticator using the helper method. + +**Example:** + +```java +import com.zitadel.ApiException; +import com.zitadel.Zitadel; +import com.zitadel.model.UserServiceAddHumanUserRequest; +import com.zitadel.model.UserServiceAddHumanUserResponse; +import com.zitadel.model.UserServiceSetHumanEmail; +import com.zitadel.model.UserServiceSetHumanProfile; + +class Demo { + + public static void main(String[] args) throws ApiException { + Zitadel zitadel = Zitadel.withAccessToken("https://example.us1.zitadel.cloud", "token"); + + UserServiceAddHumanUserResponse response = zitadel.users.addHumanUser( + new UserServiceAddHumanUserRequest() + .username("john.doe") + .profile(new UserServiceSetHumanProfile() + .givenName("John") + .familyName("Doe")) + .email(new UserServiceSetHumanEmail() + .email("john@doe.com")) + ); + System.out.println("User created: " + response); + } +} +``` + +--- + +Choose the authentication method that best suits your needs based on your +environment and security requirements. For more details, please refer to the +[Zitadel documentation on authenticating service users](https://zitadel.com/docs/guides/integrate/service-users/authenticate-service-users). + +### Versioning + +The client library's versioning is aligned with the Zitadel core project. The major version of the +client corresponds to the major version of Zitadel it is designed to work with. For example, +v2.x.x of the client is built for and tested against Zitadel v2, ensuring a predictable and stable integration. + +### Resources + +- [GitHub Repository](https://github.com/zitadel/client-java): For source code, examples, and to report issues. +- [Maven Package](https://central.sonatype.com/artifact/io.github.zitadel/client): The official package artifact for Maven. diff --git a/docs/docs/sdk-examples/client-libraries/php.mdx b/docs/docs/sdk-examples/client-libraries/php.mdx new file mode 100644 index 0000000000..1f589dd51c --- /dev/null +++ b/docs/docs/sdk-examples/client-libraries/php.mdx @@ -0,0 +1,193 @@ +--- +title: PHP Client +sidebar_label: 'PHP Client' +--- + + + + + + +
+ php logo + + This guide covers the official Zitadel Management API Client for PHP, which allows you to programmatically manage resources in your Zitadel instance. +
+ +:::info +**This is a Management API Client, not an Authentication SDK.** + +This library is designed for server-to-server communication to manage your Zitadel instance (e.g., creating users, managing projects, and updating settings). It is **not** intended for handling end-user login flows in your web application. For user authentication, you should use a standard OIDC library with your PHP framework of choice. +::: + +The Zitadel PHP Client provides an idiomatic way to access the full gamut of +Zitadel's v2 Management APIs from your PHP backend. + +> Please be aware that this client library is currently in an **incubating stage**. +While it is available for use, the API and its functionality may evolve, potentially introducing +breaking changes in future updates. We advise caution when considering it for production environments. + +### Installation + +You can add the client library to your project using Composer: + +```bash +composer require zitadel/client:"^4.0.0-beta1" +``` + +### Using the SDK + +Your SDK offers three ways to authenticate with Zitadel. Each method has its +own benefits—choose the one that fits your situation best. + +#### 1. Private Key JWT Authentication + +**What is it?** +You use a JSON Web Token (JWT) that you sign with a private key stored in a +JSON file. This process creates a secure token. + +**When should you use it?** + +- **Best for production:** It offers strong security. +- **Advanced control:** You can adjust token settings like expiration. + +**How do you use it?** + +1. Save your private key in a JSON file. +2. Use the provided method to load this key and create a JWT-based +authenticator. + +**Example:** + +```php +use \Zitadel\Client\Zitadel; + +$zitadel = Zitadel::withPrivateKey("https://example.us1.zitadel.cloud", "path/to/jwt-key.json"); + +try { + $response = $zitadel->users->userServiceAddHumanUser([ + 'username' => 'john.doe', + 'profile' => [ + 'givenName' => 'John', + 'familyName' => 'Doe' + ], + 'email' => [ + 'email' => 'john@doe.com' + ] + ]); + echo "User created: " . print_r($response, true); +} catch (ApiException $e) { + echo "Error: " . $e->getMessage(); +} +``` + +#### 2. Client Credentials Grant + +**What is it?** +This method uses a client ID and client secret to get a secure access token, +which is then used to authenticate. + +**When should you use it?** + +- **Simple and straightforward:** Good for server-to-server communication. +- **Trusted environments:** Use it when both servers are owned or trusted. + +**How do you use it?** + +1. Provide your client ID and client secret. +2. Build the authenticator + +**Example:** + +```php +use Zitadel\Client\Zitadel; +use Zitadel\Client\Model\UserServiceAddHumanUserRequest; +use \Zitadel\Client\Model\UserServiceAddHumanUserRequest; +use \Zitadel\Client\Model\UserServiceSetHumanProfile; +use \Zitadel\Client\Model\UserServiceSetHumanEmail; + +$zitadel = Zitadel::withClientCredentials("https://example.us1.zitadel.cloud", "id", "secret"); + +try { + $response = $zitadel->users->addHumanUser((new UserServiceAddHumanUserRequest()) + ->setUsername('john.doe') + ->setProfile( + (new UserServiceSetHumanProfile()) + ->setGivenName('John') + ->setFamilyName('Doe') + ) + ->setEmail( + (new UserServiceSetHumanEmail()) + ->setEmail('john@doe.com') + )); + echo "User created: " . print_r($response, true); +} catch (ApiException $e) { + echo "Error: " . $e->getMessage(); +} +``` + +#### 3. Personal Access Tokens (PATs) + +**What is it?** +A Personal Access Token (PAT) is a pre-generated token that you can use to +authenticate without exchanging credentials every time. + +**When should you use it?** + +- **Easy to use:** Great for development or testing scenarios. +- **Quick setup:** No need for dynamic token generation. + +**How do you use it?** + +1. Obtain a valid personal access token from your account. +2. Create the authenticator with: `PersonalAccessTokenAuthenticator` + +**Example:** + +```php +use \Zitadel\Client\Zitadel; +use Zitadel\Client\Zitadel; +use Zitadel\Client\Model\UserServiceAddHumanUserRequest; +use \Zitadel\Client\Model\UserServiceAddHumanUserRequest; +use \Zitadel\Client\Model\UserServiceSetHumanProfile; +use \Zitadel\Client\Model\UserServiceSetHumanEmail; + +$zitadel = Zitadel::withAccessToken("https://example.us1.zitadel.cloud", "token"); + +try { + $response = $zitadel->users->addHumanUser( + (new UserServiceAddHumanUserRequest()) + ->setUsername('john.doe') + ->setProfile( + (new UserServiceSetHumanProfile()) + ->setGivenName('John') + ->setFamilyName('Doe') + ) + ->setEmail( + (new UserServiceSetHumanEmail()) + ->setEmail('john@doe.com') + ) + ); + echo "User created: " . print_r($response, true); +} catch (ApiException $e) { + echo "Error: " . $e->getMessage(); +} +``` + +--- + +Choose the authentication method that best suits your needs based on your +environment and security requirements. For more details, please refer to the +[Zitadel documentation on authenticating service users](https://zitadel.com/docs/guides/integrate/service-users/authenticate-service-users). + +### Versioning + +The client library's versioning is aligned with the Zitadel core project. The major version of the +client corresponds to the major version of Zitadel it is designed to work with. For example, +v2.x.x of the client is built for and tested against Zitadel v2, ensuring a predictable and stable integration. + +### Resources + +- [GitHub Repository](https://github.com/zitadel/client-php): For source code, examples, and to report issues. +- [Packagist Package](https://packagist.org/packages/zitadel/client): The official package artifact for Composer. + diff --git a/docs/docs/sdk-examples/client-libraries/python.mdx b/docs/docs/sdk-examples/client-libraries/python.mdx new file mode 100644 index 0000000000..eb49145053 --- /dev/null +++ b/docs/docs/sdk-examples/client-libraries/python.mdx @@ -0,0 +1,197 @@ +--- +title: Python Client +sidebar_label: 'Python Client' +--- + + + + + + +
+ python logo + + This guide covers the official Zitadel Management API Client for Python (3.9+), which allows you to programmatically manage resources in your Zitadel instance. +
+ +:::info +**This is a Management API Client, not an Authentication SDK.** + +This library is designed for server-to-server communication to manage your Zitadel instance (e.g., creating users, managing projects, and updating settings). It is **not** intended for handling end-user login flows in your web application. For user authentication, you should use a standard OIDC library with your Python framework of choice, such as `mozilla-django-oidc` for Django or `Authlib` for Flask. +::: + +The Zitadel Python Client provides an idiomatic way to access the full gamut of Zitadel's v2 Management APIs from your Python backend. + +> Please be aware that this client library is currently in an **incubating stage**. +While it is available for use, the API and its functionality may evolve, potentially introducing +breaking changes in future updates. We advise caution when considering it for production environments. + +### Installation + +You can add the client library to your project using pip: + +```bash +pip install --pre zitadel-client +``` + +### Using the SDK + +Your SDK offers three ways to authenticate with Zitadel. Each method has its +own benefits—choose the one that fits your situation best. + +#### 1. Private Key JWT Authentication + +**What is it?** +You use a JSON Web Token (JWT) that you sign with a private key stored in a +JSON file. This process creates a secure token. + +**When should you use it?** + +- **Best for production:** It offers strong security. +- **Advanced control:** You can adjust token settings like expiration. + +**How do you use it?** + +1. Save your private key in a JSON file. +2. Use the provided method to load this key and create a JWT-based +authenticator. + +**Example:** + +```python +import zitadel_client as zitadel +from zitadel_client.exceptions import ApiError +from zitadel_client.models import ( + UserServiceAddHumanUserRequest, + UserServiceSetHumanEmail, + UserServiceSetHumanProfile, +) + +zitadel = zitadel.Zitadel.with_private_key("https://example.us1.zitadel.cloud", "path/to/jwt-key.json") + +try: + request = UserServiceAddHumanUserRequest( + username="john.doe", + profile=UserServiceSetHumanProfile( + givenName="John", + familyName="Doe" + ), + email=UserServiceSetHumanEmail( + email="john@doe.com" + ), + ) + response = zitadel.users.add_human_user(request) + print("User created:", response) +except ApiError as e: + print("Error:", e) +``` + +#### 2. Client Credentials Grant + +**What is it?** +This method uses a client ID and client secret to get a secure access token, +which is then used to authenticate. + +**When should you use it?** + +- **Simple and straightforward:** Good for server-to-server communication. +- **Trusted environments:** Use it when both servers are owned or trusted. + +**How do you use it?** + +1. Provide your client ID and client secret. +2. Build the authenticator + +**Example:** + +```python +import zitadel_client as zitadel +from zitadel_client.exceptions import ApiError +from zitadel_client.models import ( + UserServiceAddHumanUserRequest, + UserServiceSetHumanEmail, + UserServiceSetHumanProfile, +) + +zitadel = zitadel.Zitadel.with_client_credentials("https://example.us1.zitadel.cloud", "id", "secret") + +try: + request = UserServiceAddHumanUserRequest( + username="john.doe", + profile=UserServiceSetHumanProfile( + givenName="John", + familyName="Doe" + ), + email=UserServiceSetHumanEmail( + email="john@doe.com" + ), + ) + response = zitadel.users.add_human_user(request) + print("User created:", response) +except ApiError as e: + print("Error:", e) +``` + +#### 3. Personal Access Tokens (PATs) + +**What is it?** +A Personal Access Token (PAT) is a pre-generated token that you can use to +authenticate without exchanging credentials every time. + +**When should you use it?** + +- **Easy to use:** Great for development or testing scenarios. +- **Quick setup:** No need for dynamic token generation. + +**How do you use it?** + +1. Obtain a valid personal access token from your account. +2. Create the authenticator with: `PersonalAccessTokenAuthenticator` + +**Example:** + +```python +import zitadel_client as zitadel +from zitadel_client.exceptions import ApiError +from zitadel_client.models import ( + UserServiceAddHumanUserRequest, + UserServiceSetHumanEmail, + UserServiceSetHumanProfile, +) + +zitadel = zitadel.Zitadel.with_access_token("https://example.us1.zitadel.cloud", "token") + +try: + request = UserServiceAddHumanUserRequest( + username="john.doe", + profile=UserServiceSetHumanProfile( + givenName="John", + familyName="Doe" + ), + email=UserServiceSetHumanEmail( + email="john@doe.com" + ), + ) + response = zitadel.users.add_human_user(request) + print("User created:", response) +except ApiError as e: + print("Error:", e) +``` + +--- + +Choose the authentication method that best suits your needs based on your +environment and security requirements. For more details, please refer to the +[Zitadel documentation on authenticating service users](https://zitadel.com/docs/guides/integrate/service-users/authenticate-service-users). + +### Versioning + +The client library's versioning is aligned with the Zitadel core project. The major version of the +client corresponds to the major version of Zitadel it is designed to work with. For example, +v2.x.x of the client is built for and tested against Zitadel v2, ensuring a predictable and stable integration. + +### Resources + +- [GitHub Repository](https://github.com/zitadel/client-python): For source code, examples, and to report issues. +- [PyPI Package](https://pypi.org/project/zitadel-client): The official package artifact for pip. + diff --git a/docs/docs/sdk-examples/client-libraries/ruby.mdx b/docs/docs/sdk-examples/client-libraries/ruby.mdx new file mode 100644 index 0000000000..eeb0df9b2e --- /dev/null +++ b/docs/docs/sdk-examples/client-libraries/ruby.mdx @@ -0,0 +1,200 @@ +--- +title: Ruby Client +sidebar_label: 'Ruby Client' +--- + + + + + + +
+ ruby logo + + This guide covers the official Zitadel Management API Client for Ruby (3.1+), which allows you to programmatically manage resources in your Zitadel instance. +
+ +:::info +**This is a Management API Client, not an Authentication SDK.** + +This library is designed for server-to-server communication to manage your Zitadel instance (e.g., creating users, managing projects, and updating settings). It is **not** intended for handling end-user login flows in your web application. For user authentication, you should use a standard OIDC library with your Ruby framework of choice. +::: + +The Zitadel Ruby Client provides an idiomatic way to access the full gamut of Zitadel's v2 Management APIs from your Ruby backend. + +> Please be aware that this client library is currently in an **incubating stage**. +While it is available for use, the API and its functionality may evolve, potentially introducing +breaking changes in future updates. We advise caution when considering it for production environments. + +### Installation + +You can add the client library to your project using RubyGems. Add this line to your application's Gemfile: + +```ruby +gem install zitadel-client --pre +``` + +### Using the SDK + +Your SDK offers three ways to authenticate with Zitadel. Each method has its +own benefits—choose the one that fits your situation best. + +#### 1. Private Key JWT Authentication + +**What is it?** +You use a JSON Web Token (JWT) that you sign with a private key stored in a +JSON file. This process creates a secure token. + +**When should you use it?** + +- **Best for production:** It offers strong security. +- **Advanced control:** You can adjust token settings like expiration. + +**How do you use it?** + +1. Save your private key in a JSON file. +2. Use the provided method to create an authenticator. + +**Example:** + +```ruby +require 'zitadel-client' +require 'securerandom' + +client = Zitadel::Client::Zitadel.with_private_key("https://example.us1.zitadel.cloud", "path/to/jwt-key.json") + +begin + response = client.users.add_human_user( + Zitadel::Client::UserServiceAddHumanUserRequest.new( + username: SecureRandom.hex, + profile: Zitadel::Client::UserServiceSetHumanProfile.new( + given_name: 'John', + family_name: 'Doe' + ), + email: Zitadel::Client::UserServiceSetHumanEmail.new( + email: "john.doe@example.com" + ) + ) + ) + puts "User created: #{response}" +rescue StandardError => e + puts "Error: #{e.message}" +end +``` + +#### 2. Client Credentials Grant + +**What is it?** +This method uses a client ID and client secret to get a secure access token, +which is then used to authenticate. + +**When should you use it?** + +- **Simple and straightforward:** Good for server-to-server communication. +- **Trusted environments:** Use it when both servers are owned or trusted. + +**How do you use it?** + +1. Provide your client ID and client secret. +2. Use the provided method to create an authenticator. + +**Example:** + +```ruby +require 'zitadel-client' +require 'securerandom' + +client = Zitadel::Client::Zitadel.with_client_credentials("https://example.us1.zitadel.cloud", "id", "secret") + +begin + response = client.users.add_human_user( + Zitadel::Client::UserServiceAddHumanUserRequest.new( + username: SecureRandom.hex, + profile: Zitadel::Client::UserServiceSetHumanProfile.new( + given_name: 'John', + family_name: 'Doe' + ), + email: Zitadel::Client::UserServiceSetHumanEmail.new( + email: "john.doe@example.com" + ) + ) + ) + puts "User created: #{response}" +rescue StandardError => e + puts "Error: #{e.message}" +end +``` + +#### 3. Personal Access Tokens (PATs) + +**What is it?** +A Personal Access Token (PAT) is a pre-generated token that you can use to +authenticate without exchanging credentials every time. + +**When should you use it?** + +- **Easy to use:** Great for development or testing scenarios. +- **Quick setup:** No need for dynamic token generation. + +**How do you use it?** + +1. Obtain a valid personal access token from your account. +2. Use the provided method to create an authenticator. + +**Example:** + +```ruby +require 'zitadel-client' +require 'securerandom' + +client = Zitadel::Client::Zitadel.with_access_token("https://example.us1.zitadel.cloud", "token") + +begin + response = client.users.add_human_user( + Zitadel::Client::UserServiceAddHumanUserRequest.new( + username: SecureRandom.hex, + profile: Zitadel::Client::UserServiceSetHumanProfile.new( + given_name: 'John', + family_name: 'Doe' + ), + email: Zitadel::Client::UserServiceSetHumanEmail.new( + email: "john.doe@example.com" + ) + ) + ) + puts "User created: #{response}" +rescue StandardError => e + puts "Error: #{e.message}" +end +``` + +--- + +Choose the authentication method that best suits your needs based on your +environment and security requirements. For more details, please refer to the +[Zitadel documentation on authenticating service users](https://zitadel.com/docs/guides/integrate/service-users/authenticate-service-users). + +### Debugging + +The SDK supports debug logging, which can be enabled for troubleshooting +and debugging purposes. You can enable debug logging by setting the `debug` +flag to `true` when initializing the `Zitadel` client, like this: + +```ruby +zitadel = zitadel.Zitadel("your-zitadel-base-url", 'your-valid-token', lambda config: config.debug = True) +``` + +When enabled, the SDK will log additional information, such as HTTP request +and response details, which can be useful for identifying issues in the +integration or troubleshooting unexpected behavior. + +### Versioning + +The client library's versioning is aligned with the Zitadel core project. The major version of the +client corresponds to the major version of Zitadel it is designed to work with. For example, +v2.x.x of the client is built for and tested against Zitadel v2, ensuring a predictable and stable integration. + +### Resources + +- [GitHub Repository](https://github.com/zitadel/client-ruby): For source code, examples, and to report issues. +- [RubyGems Package](https://rubygems.org/gems/zitadel-client): The official package artifact for RubyGems. diff --git a/docs/docs/sdk-examples/introduction.mdx b/docs/docs/sdk-examples/introduction.mdx index 203e21e9f3..0e7aea7a66 100644 --- a/docs/docs/sdk-examples/introduction.mdx +++ b/docs/docs/sdk-examples/introduction.mdx @@ -19,9 +19,17 @@ We provide this list for informational purposes and to foster community engageme import { Frameworks } from "../../src/components/frameworks"; +## Clients + + framework.client === true } /> + +## SDKs + + framework.sdk === true } /> + ## Resources - + framework.client === false || framework.client == null} /> To further streamline your setup, simply visit the console in ZITADEL where you can select one of the languages or frameworks. This will allow you to instantly set up the configuration for that specific sample in ZITADEL, ensuring you have everything you need to get started right away. diff --git a/docs/docs/sdk-examples/java.mdx b/docs/docs/sdk-examples/java.mdx index e7afb5188b..e4862422d0 100644 --- a/docs/docs/sdk-examples/java.mdx +++ b/docs/docs/sdk-examples/java.mdx @@ -41,7 +41,7 @@ The following features are covered by Java Spring Security: The goal is to have a ZITADEL Java SDK in the future which will cover the following: - Wrapper around Java Spring Security - Authentication with OIDC -- Authorization and checking Rolls +- Authorization and checking Roles - Integrate ZITADEL APIs to read and manage resources - Integrate ZITADEL Session API to create your own login UI diff --git a/docs/docs/self-hosting/deploy/.gitignore b/docs/docs/self-hosting/deploy/.gitignore new file mode 100644 index 0000000000..83754bbee4 --- /dev/null +++ b/docs/docs/self-hosting/deploy/.gitignore @@ -0,0 +1 @@ +login-client-pat diff --git a/docs/docs/self-hosting/deploy/compose.mdx b/docs/docs/self-hosting/deploy/compose.mdx index 370c0e7f5d..f47a33da16 100644 --- a/docs/docs/self-hosting/deploy/compose.mdx +++ b/docs/docs/self-hosting/deploy/compose.mdx @@ -1,67 +1,65 @@ --- -title: Set up ZITADEL with Docker Compose +title: Set up Zitadel with Docker Compose sidebar_label: Docker Compose --- import CodeBlock from '@theme/CodeBlock'; import DockerComposeSource from '!!raw-loader!./docker-compose.yaml' -import DockerComposeSaSource from '!!raw-loader!./docker-compose-sa.yaml' -import Disclaimer from './_disclaimer.mdx' -import DefaultUser from './_defaultuser.mdx' -import Next from './_next.mdx' -import NoteInstanceNotFound from './troubleshooting/_note_instance_not_found.mdx'; +import ExampleZitadelConfigSource from '!!raw-loader!./example-zitadel-config.yaml' +import ExampleZitadelSecretsSource from '!!raw-loader!./example-zitadel-secrets.yaml' +import ExampleZitadelInitStepsSource from '!!raw-loader!./example-zitadel-init-steps.yaml' +The stack consists of four long-running containers and a couple of short-lived containers: +- A [Traefik](https://doc.traefik.io/traefik/) reverse proxy container with upstream HTTP/2 enabled, issuing a self-signed TLS certificate. +- A Login container that is accessible via Traefik at `/ui/v2/login` +- A Zitadel container that is accessible via Traefik at all other paths than `/ui/v2/login`. +- An insecure [PostgreSQL](https://www.postgresql.org/docs/current/index.html). -The setup is tested against Docker version 20.10.17 and Docker Compose version v2.2.3 +The Traefik container and the login container call the Zitadel container via the internal Docker network at `h2c://zitadel:8080` -## Docker compose +The setup is tested against Docker version 28.3.2 and Docker Compose version v2.38.2 -By executing the commands below, you will download the following file: +By executing the commands below, you will download the following files:
docker-compose.yaml {DockerComposeSource}
+
+ example-zitadel-config.yaml + {ExampleZitadelConfigSource} +
+
+ example-zitadel-secrets.yaml + {ExampleZitadelSecretsSource} +
+
+ example-zitadel-init-steps.yaml + {ExampleZitadelInitStepsSource} +
```bash # Download the docker compose example configuration. wget https://raw.githubusercontent.com/zitadel/zitadel/main/docs/docs/self-hosting/deploy/docker-compose.yaml -# Run the database and application containers. -docker compose up --detach +# Download and adjust the example configuration file containing standard configuration. +wget https://raw.githubusercontent.com/zitadel/zitadel/main/docs/docs/self-hosting/deploy/example-zitadel-config.yaml + +# Download and adjust the example configuration file containing secret configuration. +wget https://raw.githubusercontent.com/zitadel/zitadel/main/docs/docs/self-hosting/deploy/example-zitadel-secrets.yaml + +# Download and adjust the example configuration file containing database initialization configuration. +wget https://raw.githubusercontent.com/zitadel/zitadel/main/docs/docs/self-hosting/deploy/example-zitadel-init-steps.yaml + +# Make sure you have the latest version of the images +docker compose pull + +# Run the containers +docker compose up ``` - +Open your favorite internet browser at https://localhost/ui/console?login_hint=zitadel-admin@zitadel.localhost. +Your browser warns you about the insecure self-signed TLS certificate. As localhost resolves to your local machine, you can safely proceed. +Use the password *Password1!* to log in. - - -## VideoGuide - - -## Docker compose with service account - -By executing the commands below, you will download the following file: - -
- docker-compose-sa.yaml - {DockerComposeSaSource} -
- -```bash -# Download the docker compose example configuration. -wget https://raw.githubusercontent.com/zitadel/zitadel/main/docs/docs/self-hosting/deploy/docker-compose-sa.yaml -O docker-compose.yaml - -# create the machine key directory -mkdir machinekey - -# Run the database and application containers. -docker compose up --detach - -# then you can move your machine key -mv ./machinekey/zitadel-admin-sa.json $HOME/zitadel-admin-sa.json -``` - -This key can be used to provision resources with for example [Terraform](/docs/guides/manage/terraform-provider). - - - +Read more about [the login process](/guides/integrate/login/oidc/login-users). \ No newline at end of file diff --git a/docs/docs/self-hosting/deploy/docker-compose-sa.yaml b/docs/docs/self-hosting/deploy/docker-compose-sa.yaml deleted file mode 100644 index 9edd95faa0..0000000000 --- a/docs/docs/self-hosting/deploy/docker-compose-sa.yaml +++ /dev/null @@ -1,49 +0,0 @@ -services: - zitadel: - # The user should have the permission to write to ./machinekey - user: "${UID:-1000}" - restart: 'always' - networks: - - 'zitadel' - image: 'ghcr.io/zitadel/zitadel:latest' - command: 'start-from-init --masterkey "MasterkeyNeedsToHave32Characters" --tlsMode disabled' - environment: - ZITADEL_DATABASE_POSTGRES_HOST: db - ZITADEL_DATABASE_POSTGRES_PORT: 5432 - ZITADEL_DATABASE_POSTGRES_DATABASE: zitadel - ZITADEL_DATABASE_POSTGRES_USER_USERNAME: zitadel - ZITADEL_DATABASE_POSTGRES_USER_PASSWORD: zitadel - ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE: disable - ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME: postgres - ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD: postgres - ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_MODE: disable - ZITADEL_EXTERNALSECURE: false - ZITADEL_FIRSTINSTANCE_MACHINEKEYPATH: /machinekey/zitadel-admin-sa.json - ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINE_USERNAME: zitadel-admin-sa - ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINE_NAME: Admin - ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINEKEY_TYPE: 1 - depends_on: - db: - condition: 'service_healthy' - ports: - - '8080:8080' - volumes: - - ./machinekey:/machinekey - - db: - restart: 'always' - image: postgres:17-alpine - environment: - PGUSER: postgres - POSTGRES_PASSWORD: postgres - networks: - - 'zitadel' - healthcheck: - test: ["CMD-SHELL", "pg_isready", "-d", "zitadel", "-U", "postgres"] - interval: '10s' - timeout: '30s' - retries: 5 - start_period: '20s' - -networks: - zitadel: diff --git a/docs/docs/self-hosting/deploy/docker-compose.yaml b/docs/docs/self-hosting/deploy/docker-compose.yaml index f5164eb3b7..23d651efc9 100644 --- a/docs/docs/self-hosting/deploy/docker-compose.yaml +++ b/docs/docs/self-hosting/deploy/docker-compose.yaml @@ -1,41 +1,117 @@ services: - zitadel: - restart: 'always' - networks: - - 'zitadel' - image: 'ghcr.io/zitadel/zitadel:latest' - command: 'start-from-init --masterkey "MasterkeyNeedsToHave32Characters" --tlsMode disabled' + + db: + image: postgres:17-alpine + restart: unless-stopped environment: - ZITADEL_DATABASE_POSTGRES_HOST: db - ZITADEL_DATABASE_POSTGRES_PORT: 5432 - ZITADEL_DATABASE_POSTGRES_DATABASE: zitadel - ZITADEL_DATABASE_POSTGRES_USER_USERNAME: zitadel - ZITADEL_DATABASE_POSTGRES_USER_PASSWORD: zitadel - ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE: disable - ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME: postgres - ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD: postgres - ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_MODE: disable - ZITADEL_EXTERNALSECURE: false + - POSTGRES_USER=root + - POSTGRES_PASSWORD=postgres + networks: + - 'storage' + healthcheck: + test: [ "CMD-SHELL", "pg_isready", "-d", "db_prod" ] + interval: 10s + timeout: 60s + retries: 5 + start_period: 10s + volumes: + - 'data:/var/lib/postgresql/data:rw' + + zitadel-init: + restart: 'no' + networks: + - 'storage' + image: 'ghcr.io/zitadel/zitadel:v4.0.0-rc.2' + command: [ init, --config, /example-zitadel-config.yaml, --config, /example-zitadel-secrets.yaml ] depends_on: db: condition: 'service_healthy' - ports: - - '8080:8080' + volumes: + - './example-zitadel-config.yaml:/example-zitadel-config.yaml:ro' + - './example-zitadel-secrets.yaml:/example-zitadel-secrets.yaml:ro' - db: - restart: 'always' - image: postgres:17-alpine - environment: - PGUSER: postgres - POSTGRES_PASSWORD: postgres + zitadel-setup: + restart: 'no' networks: - - 'zitadel' + - 'storage' + image: 'ghcr.io/zitadel/zitadel:v4.0.0-rc.2' + command: [ setup, --config, /current-dir/example-zitadel-config.yaml, --config, /current-dir/example-zitadel-secrets.yaml, --steps, /current-dir/example-zitadel-init-steps.yaml, --masterkey, MasterkeyNeedsToHave32Characters ] + depends_on: + zitadel-init: + condition: 'service_completed_successfully' + restart: false + volumes: + - '.:/current-dir:rw' + + zitadel: + restart: 'unless-stopped' + networks: + - 'backend' + - 'storage' + labels: + - "traefik.http.routers.zitadel.rule=!PathPrefix(`/ui/v2/login`)" + - "traefik.http.routers.zitadel.tls=true" # Traefik uses a self-signed certificate + - "traefik.http.services.zitadel.loadbalancer.passhostheader=true" + - "traefik.http.services.zitadel.loadbalancer.server.scheme=h2c" + - "traefik.http.services.zitadel.loadbalancer.server.port=8080" + image: 'ghcr.io/zitadel/zitadel:v4.0.0-rc.2' + command: [ start, --config, /example-zitadel-config.yaml, --config, /example-zitadel-secrets.yaml, --masterkey, MasterkeyNeedsToHave32Characters ] + depends_on: + zitadel-setup: + condition: 'service_completed_successfully' + restart: true + volumes: + - './example-zitadel-config.yaml:/example-zitadel-config.yaml:ro' + - './example-zitadel-secrets.yaml:/example-zitadel-secrets.yaml:ro' healthcheck: - test: ["CMD-SHELL", "pg_isready", "-d", "zitadel", "-U", "postgres"] - interval: '10s' - timeout: '30s' + test: [ "CMD", "/app/zitadel", "ready", "--config", "/example-zitadel-config.yaml", "--config", "/example-zitadel-secrets.yaml" ] + interval: 10s + timeout: 60s retries: 5 - start_period: '20s' + start_period: 10s + + login: + restart: 'unless-stopped' + labels: + - "traefik.http.routers.login.rule=PathPrefix(`/ui/v2/login`)" + - "traefik.http.routers.login.tls=true" # Traefik uses a self-signed certificate + - "traefik.http.services.login.loadbalancer.passhostheader=true" + - "traefik.http.services.login.loadbalancer.server.port=3000" + image: 'ghcr.io/zitadel/zitadel-login:v4.0.0-rc.2' + # If you can't use the network_mode service:zitadel, you can pass the environment variable CUSTOM_REQUEST_HEADERS=Host:localhost instead. + network_mode: service:zitadel + environment: + - ZITADEL_API_URL=http://localhost:8080 + - NEXT_PUBLIC_BASE_PATH=/ui/v2/login + - ZITADEL_SERVICE_USER_TOKEN_FILE=/current-dir/login-client-pat + user: "${UID:-1000}" + volumes: + - '.:/current-dir:ro' + depends_on: + zitadel-setup: + condition: 'service_completed_successfully' + restart: false + + traefik: + image: traefik:latest + command: --providers.docker --api.insecure=true --entrypoints.websecure.address=:443 --log.level=DEBUG --accesslog + networks: + - 'backend' + ports: + - "443:443" + - "8080:8080" + volumes: + - /var/run/docker.sock:/var/run/docker.sock + depends_on: + zitadel: + condition: 'service_healthy' + login: + condition: 'service_started' networks: - zitadel: + storage: + backend: + + +volumes: + data: diff --git a/docs/docs/self-hosting/deploy/example-zitadel-config.yaml b/docs/docs/self-hosting/deploy/example-zitadel-config.yaml new file mode 100644 index 0000000000..baacd6cefe --- /dev/null +++ b/docs/docs/self-hosting/deploy/example-zitadel-config.yaml @@ -0,0 +1,22 @@ +# All possible options and their defaults: https://github.com/zitadel/zitadel/blob/main/cmd/defaults.yaml + +ExternalSecure: true +ExternalPort: 443 + +# Traefik terminates TLS. Inside the Docker network, we use plain text. +TLS.Enabled: false + +# If not using the docker compose example, adjust these values for connecting ZITADEL to your PostgreSQL +Database: + postgres: + Host: 'db' + Port: 5432 + Database: zitadel + User.SSL.Mode: 'disable' + Admin.SSL.Mode: 'disable' + +# Access logs allow us to debug Network issues +LogStore.Access.Stdout.Enabled: true + +# Skipping the MFA init step allows us to immediately authenticate at the console +DefaultInstance.LoginPolicy.MfaInitSkipLifetime: "0s" diff --git a/docs/docs/self-hosting/deploy/example-zitadel-init-steps.yaml b/docs/docs/self-hosting/deploy/example-zitadel-init-steps.yaml new file mode 100644 index 0000000000..373c6ae744 --- /dev/null +++ b/docs/docs/self-hosting/deploy/example-zitadel-init-steps.yaml @@ -0,0 +1,11 @@ +# All possible options and their defaults: https://github.com/zitadel/zitadel/blob/main/cmd/setup/steps.yaml +FirstInstance: + LoginClientPatPath: '/current-dir/login-client-pat' + Org: + # We want to authenticate immediately at the console without changing the password + Human.PasswordChangeRequired: false + LoginClient: + Machine: + Username: 'login-client' + Name: 'Automatically Initialized IAM Login Client' + Pat.ExpirationDate: '2029-01-01T00:00:00Z' \ No newline at end of file diff --git a/docs/docs/self-hosting/deploy/loadbalancing-example/example-zitadel-secrets.yaml b/docs/docs/self-hosting/deploy/example-zitadel-secrets.yaml similarity index 100% rename from docs/docs/self-hosting/deploy/loadbalancing-example/example-zitadel-secrets.yaml rename to docs/docs/self-hosting/deploy/example-zitadel-secrets.yaml diff --git a/docs/docs/self-hosting/deploy/loadbalancing-example/docker-compose.yaml b/docs/docs/self-hosting/deploy/loadbalancing-example/docker-compose.yaml deleted file mode 100644 index d1d8c95bb2..0000000000 --- a/docs/docs/self-hosting/deploy/loadbalancing-example/docker-compose.yaml +++ /dev/null @@ -1,48 +0,0 @@ -services: - - traefik: - networks: - - 'zitadel' - image: "traefik:latest" - ports: - - "80:80" - - "443:443" - volumes: - - "./example-traefik.yaml:/etc/traefik/traefik.yaml" - - zitadel: - restart: 'always' - networks: - - 'zitadel' - image: 'ghcr.io/zitadel/zitadel:latest' - command: 'start-from-init --config /example-zitadel-config.yaml --config /example-zitadel-secrets.yaml --steps /example-zitadel-init-steps.yaml --masterkey "${ZITADEL_MASTERKEY}" --tlsMode external' - depends_on: - db: - condition: 'service_healthy' - volumes: - - './example-zitadel-config.yaml:/example-zitadel-config.yaml:ro' - - './example-zitadel-secrets.yaml:/example-zitadel-secrets.yaml:ro' - - './example-zitadel-init-steps.yaml:/example-zitadel-init-steps.yaml:ro' - - db: - image: postgres:17-alpine - restart: always - environment: - - POSTGRES_USER=root - - POSTGRES_PASSWORD=postgres - networks: - - 'zitadel' - healthcheck: - test: ["CMD-SHELL", "pg_isready", "-d", "db_prod"] - interval: 10s - timeout: 60s - retries: 5 - start_period: 10s - volumes: - - 'data:/var/lib/postgresql/data:rw' - -networks: - zitadel: - -volumes: - data: diff --git a/docs/docs/self-hosting/deploy/loadbalancing-example/example-traefik.yaml b/docs/docs/self-hosting/deploy/loadbalancing-example/example-traefik.yaml deleted file mode 100644 index c16f74a46d..0000000000 --- a/docs/docs/self-hosting/deploy/loadbalancing-example/example-traefik.yaml +++ /dev/null @@ -1,69 +0,0 @@ -log: - level: DEBUG - -accessLog: {} - -entrypoints: - web: - address: ":80" - - websecure: - address: ":443" - -tls: - stores: - default: - # generates self-signed certificates - defaultCertificate: - -providers: - file: - filename: /etc/traefik/traefik.yaml - -http: - middlewares: - zitadel: - headers: - isDevelopment: false - allowedHosts: - - 'my.domain' - customRequestHeaders: - authority: 'my.domain' - redirect-to-https: - redirectScheme: - scheme: https - port: 443 - permanent: true - - routers: - # Redirect HTTP to HTTPS - router0: - entryPoints: - - web - middlewares: - - redirect-to-https - rule: 'HostRegexp(`my.domain`, `{subdomain:[a-z]+}.my.domain`)' - service: zitadel - # The actual ZITADEL router - router1: - entryPoints: - - websecure - service: zitadel - middlewares: - - zitadel - rule: 'HostRegexp(`my.domain`, `{subdomain:[a-z]+}.my.domain`)' - tls: - domains: - - main: "my.domain" - sans: - - "*.my.domain" - - "my.domain" - - # Add the service - services: - zitadel: - loadBalancer: - servers: - # h2c is the scheme for unencrypted HTTP/2 - - url: h2c://zitadel:8080 - passHostHeader: true diff --git a/docs/docs/self-hosting/deploy/loadbalancing-example/example-zitadel-config.yaml b/docs/docs/self-hosting/deploy/loadbalancing-example/example-zitadel-config.yaml deleted file mode 100644 index 392bf1148e..0000000000 --- a/docs/docs/self-hosting/deploy/loadbalancing-example/example-zitadel-config.yaml +++ /dev/null @@ -1,26 +0,0 @@ -# All possible options and their defaults: https://github.com/zitadel/zitadel/blob/main/cmd/defaults.yaml -Log: - Level: 'info' - -# Make ZITADEL accessible over HTTPs, not HTTP -ExternalSecure: true -ExternalDomain: my.domain -ExternalPort: 443 - -# If not using the docker compose example, adjust these values for connecting ZITADEL to your PostgreSQL -Database: - postgres: - Host: 'db' - Port: 5432 - Database: zitadel - User: - SSL: - Mode: 'disable' - Admin: - SSL: - Mode: 'disable' - -LogStore: - Access: - Stdout: - Enabled: true diff --git a/docs/docs/self-hosting/deploy/loadbalancing-example/example-zitadel-init-steps.yaml b/docs/docs/self-hosting/deploy/loadbalancing-example/example-zitadel-init-steps.yaml deleted file mode 100644 index 804e3d18d8..0000000000 --- a/docs/docs/self-hosting/deploy/loadbalancing-example/example-zitadel-init-steps.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# All possible options and their defaults: https://github.com/zitadel/zitadel/blob/main/cmd/setup/steps.yaml -FirstInstance: - Org: - Name: 'My Org' - Human: - # use the loginname root@my-org.my.domain - Username: 'root' - Password: 'RootPassword1!' diff --git a/docs/docs/self-hosting/deploy/loadbalancing-example/loadbalancing-example.mdx b/docs/docs/self-hosting/deploy/loadbalancing-example/loadbalancing-example.mdx deleted file mode 100644 index d5e3984568..0000000000 --- a/docs/docs/self-hosting/deploy/loadbalancing-example/loadbalancing-example.mdx +++ /dev/null @@ -1,85 +0,0 @@ ---- -title: A ZITADEL Load Balancing Example ---- - -import CodeBlock from '@theme/CodeBlock'; -import DockerComposeSource from '!!raw-loader!./docker-compose.yaml' -import ExampleTraefikSource from '!!raw-loader!./example-traefik.yaml' -import ExampleZITADELConfigSource from '!!raw-loader!./example-zitadel-config.yaml' -import ExampleZITADELSecretsSource from '!!raw-loader!./example-zitadel-secrets.yaml' -import ExampleZITADELInitStepsSource from '!!raw-loader!./example-zitadel-init-steps.yaml' -import NoteInstanceNotFound from '../troubleshooting/_note_instance_not_found.mdx'; - -With this example configuration, you create a near production environment for ZITADEL with [Docker Compose](https://docs.docker.com/compose/). - -The stack consists of three long-running containers: -- A [Traefik](https://doc.traefik.io/traefik/) reverse proxy with upstream HTTP/2 enabled, issuing a self-signed TLS certificate. -- A secure ZITADEL container configured for a custom domain. As we terminate TLS with Traefik, we configure ZITADEL for `--tlsMode external`. -- An insecure [PostgreSQL](https://www.postgresql.org/docs/current/index.html). - -The setup is tested against Docker version 20.10.17 and Docker Compose version v2.2.3 - -By executing the commands below, you will download the following files: - -
- docker-compose.yaml - {DockerComposeSource} -
-
- example-traefik.yaml - {ExampleTraefikSource} -
-
- example-zitadel-config.yaml - {ExampleZITADELConfigSource} -
-
- example-zitadel-secrets.yaml - {ExampleZITADELSecretsSource} -
-
- example-zitadel-init-steps.yaml - {ExampleZITADELInitStepsSource} -
- -```bash -# Download the docker compose example configuration. -wget https://raw.githubusercontent.com/zitadel/zitadel/main/docs/docs/self-hosting/deploy/loadbalancing-example/docker-compose.yaml - -# Download the Traefik example configuration. -wget https://raw.githubusercontent.com/zitadel/zitadel/main/docs/docs/self-hosting/deploy/loadbalancing-example/example-traefik.yaml - -# Download and adjust the example configuration file containing standard configuration. -wget https://raw.githubusercontent.com/zitadel/zitadel/main/docs/docs/self-hosting/deploy/loadbalancing-example/example-zitadel-config.yaml - -# Download and adjust the example configuration file containing secret configuration. -wget https://raw.githubusercontent.com/zitadel/zitadel/main/docs/docs/self-hosting/deploy/loadbalancing-example/example-zitadel-secrets.yaml - -# Download and adjust the example configuration file containing database initialization configuration. -wget https://raw.githubusercontent.com/zitadel/zitadel/main/docs/docs/self-hosting/deploy/loadbalancing-example/example-zitadel-init-steps.yaml - -# A single ZITADEL instance always needs the same 32 bytes long masterkey -# Generate one to a file if you haven't done so already and pass it as environment variable -tr -dc A-Za-z0-9 ./zitadel-masterkey -export ZITADEL_MASTERKEY="$(cat ./zitadel-masterkey)" - -# Run the database and application containers -docker compose up --detach -``` - -Make `127.0.0.1` available at `my.domain`. For example, this can be achieved with an entry `127.0.0.1 my.domain` in the `/etc/hosts` file. - -Open your favorite internet browser at [https://my.domain/ui/console/](https://my.domain/ui/console/). -You can safely proceed, if your browser warns you about the insecure self-signed TLS certificate. -This is the IAM admin users login according to your configuration in the [example-zitadel-init-steps.yaml](./example-zitadel-init-steps.yaml): -- **username**: *root@my-org.my.domain* -- **password**: *RootPassword1!* - -Read more about [the login process](/guides/integrate/login/oidc/login-users). - - - -## Troubleshooting - -You can connect to the database like this: `docker exec -it loadbalancing-example-db-1 psql --host localhost` -For example, to show all login names: `docker exec -it loadbalancing-example-db-1 psql -d zitadel --host localhost -c 'select * from projections.login_names3'` diff --git a/docs/docs/self-hosting/deploy/macos.mdx b/docs/docs/self-hosting/deploy/macos.mdx index beb3182208..aea5fb07e9 100644 --- a/docs/docs/self-hosting/deploy/macos.mdx +++ b/docs/docs/self-hosting/deploy/macos.mdx @@ -64,4 +64,4 @@ mv /tmp/zitadel-admin-sa.json $HOME/zitadel-admin-sa.json This key can be used to provision resources with for example [Terraform](/docs/guides/manage/terraform-provider). - + \ No newline at end of file diff --git a/docs/docs/self-hosting/manage/configure/_compose.mdx b/docs/docs/self-hosting/manage/configure/_compose.mdx index 5e8b1c3937..837d4c6e62 100644 --- a/docs/docs/self-hosting/manage/configure/_compose.mdx +++ b/docs/docs/self-hosting/manage/configure/_compose.mdx @@ -43,7 +43,7 @@ wget https://raw.githubusercontent.com/zitadel/zitadel/main/docs/docs/self-hosti # A single ZITADEL instance always needs the same 32 bytes long masterkey # Generate one to a file if you haven't done so already and pass it as environment variable -tr -dc A-Za-z0-9 ./zitadel-masterkey +LC_ALL=C tr -dc '[:graph:]' ./zitadel-masterkey export ZITADEL_MASTERKEY="$(cat ./zitadel-masterkey)" # Run the database and application containers diff --git a/docs/docs/self-hosting/manage/configure/_helm.mdx b/docs/docs/self-hosting/manage/configure/_helm.mdx index b35957abb8..17fb8165a6 100644 --- a/docs/docs/self-hosting/manage/configure/_helm.mdx +++ b/docs/docs/self-hosting/manage/configure/_helm.mdx @@ -3,4 +3,4 @@ Configure Zitadel using native Helm values. You can manage secrets through Helm values, letting Helm create Kubernetes secrets. Alternatively, reference existing Kubernetes secrets managed outside of Helm. See the [referenced secrets example](https://github.com/zitadel/zitadel-charts/tree/main/examples/3-referenced-secrets) in the charts */examples* folder. -For a quick setup, check out the [insecure Postgres example](https://github.com/zitadel/zitadel-charts/tree/main/examples/1-insecure-postgres). +For a quick setup, check out the [insecure Postgres example](https://github.com/zitadel/zitadel-charts/tree/main/examples/1-postgres-insecure). diff --git a/docs/docs/self-hosting/manage/configure/_linuxunix.mdx b/docs/docs/self-hosting/manage/configure/_linuxunix.mdx index 6be833caea..65130ea195 100644 --- a/docs/docs/self-hosting/manage/configure/_linuxunix.mdx +++ b/docs/docs/self-hosting/manage/configure/_linuxunix.mdx @@ -35,7 +35,7 @@ wget https://raw.githubusercontent.com/zitadel/zitadel/main/docs/docs/self-hosti # A single ZITADEL instance always needs the same 32 characters long masterkey # If you haven't done so already, you can generate a new one # The key must be passed as argument -ZITADEL_MASTERKEY="$(tr -dc A-Za-z0-9 ./zitadel-masterkey +LC_ALL=C tr -dc '[:graph:]' ./zitadel-masterkey # Let the zitadel binary read configuration from environment variables zitadel start-from-init --masterkey "${ZITADEL_MASTERKEY}" --tlsMode disabled --masterkeyFile ./zitadel-masterkey diff --git a/docs/docs/self-hosting/manage/service_ping.md b/docs/docs/self-hosting/manage/service_ping.md new file mode 100644 index 0000000000..65f16725ad --- /dev/null +++ b/docs/docs/self-hosting/manage/service_ping.md @@ -0,0 +1,83 @@ +--- +title: Service Ping +sidebar_label: Service Ping +--- + +Service Ping is a feature that periodically sends anonymized analytics and usage data from your ZITADEL system to a central endpoint. +This data helps improve ZITADEL by providing insights into its usage patterns. + +The feature is enabled by default, but can be disabled either completely or for specific reports. +Checkout the configuration options below. + +## Data Sent by Service Ping + +### Base Information + +If the feature is enabled, the base information will always be sent. To prevent that, you can opt out by disabling the entire Service Ping: + +```yaml +ServicePing: + Enabled: false # ZITADEL_SERVICEPING_ENABLED +``` + +The base information sent back includes the following: +- your systemID +- the currently run version of ZITADEL +- information on all instances + - id + - creation date + - domains + +### Resource Counts + +Resource counts is a report that provides us with information about the number of resources in your ZITADEL instances. + +The following resources are counted: +- Instances +- Organizations +- Projects per organization +- Users per organization +- Instance Administrators +- Identity Providers +- LDAP Identity Providers +- Actions (V1) +- Targets and set up executions +- Login Policies +- Password Complexity Policies +- Password Expiry Policies +- Lockout Policies + +The list might be extended in the future to include more resources. + +To disable this report, set the following in your configuration file: + +```yaml +ServicePing: + Telemetry: + ResourceCounts: + Enabled: false # ZITADEL_SERVICEPING_TELEMETRY_RESOURCECOUNT_ENABLED +``` + +## Configuration + +The Service Ping feature can be configured through the runtime configuration. Please check out the configuration file +for all available options. Below is a list of the most important options: + +### Interval + +This defines at which interval the Service Ping feature sends data to the central endpoint. It supports the extended cron syntax +and by default is set to `@daily`, which means it will send data every day. The time is randomized on startup to prevent +all systems from sending data at the same time. + +You can adjust it to your needs to make sure there is no performance impact on your system. +For example, if you already have some scheduled job syncing data in and out of ZITADEL around a specific time or have regularly a +lot of traffic during the day, you might want to change it to a different time, e.g. `15 4 * * *` to send it every day at 4:15 AM. + +The interval must be at least 30 minutes to prevent too frequent requests to the central endpoint. + +### MaxAttempts + +This defines how many attempts the Service Ping feature will make to send data to the central endpoint before giving up +for a specific interval and report. If one report fails, it will be retried up to this number of times. +Other reports will still be handled in parallel and have their own retry count. This means if the base information +only succeeded after three attempts, the resource count still has five attempts to be sent. diff --git a/docs/docs/support/advisory/a10016.md b/docs/docs/support/advisory/a10016.md new file mode 100644 index 0000000000..6272eaa81d --- /dev/null +++ b/docs/docs/support/advisory/a10016.md @@ -0,0 +1,104 @@ +--- +title: Technical Advisory 10016 +--- + +## Date + +- v2.65.x: > v2.65.9 +- v2.66.x > v2.66.17 +- v2.67.x > v2.67.14 +- v2.68.x > v2.68.10 +- v2.69.x > v2.69.10 +- v2.70.x > v2.70.11 +- v2.71.x > v2.71.10 +- v3.x > v3.2.1 + + +Date: 2025-05-14 + +Last updated: 2025-05-19 + +## Description + +### Background + +Zitadel uses a eventstore table as main source of truth for state changes. +Projections are tables which provide alternative views of state, which are built using events. +In order to know which events are reduced into projections, we use a `position` column in the eventstore and a dedicated table which records the current state. + +### Problem + +Zitadel prior to the listed version had a precision bug. The `position` column uses a fixed-point numeric type. In Zitadel's Go code we used a `float64`. In certain cases we noticed a precision loss when Zitadel updated the `current_states` table. + +## Impact + +During a past attempt to fix this, we got reports of failing projections inside Zitadel. Because the precision became exact certain compare operations like *equal*, *less then*, etc would now return different results. This was because the values in `current_states` would already have lost precision from a broken version. This might happen to **some** deployments or projections: there is only a small probability. + +We are releasing the fix again and your system might get affected. + +- Original issue: [8671](https://github.com/zitadel/zitadel/issues/8671) +- Follow-up issue: [8863](https://github.com/zitadel/zitadel/issues/8863) + +## Mitigation + +When **after** deploying a fixed version and only when experiencing problems described by issue [8863](https://github.com/zitadel/zitadel/issues/8863), the following queries can be executed to fix `current_state` rows which have "broken" values. We recommend doing this in a transaction in order to double-check the affected rows, before committing the update. + +```sql +begin; + +with + broken as ( + select + s.projection_name, + s.instance_id, + s.aggregate_id, + s.aggregate_type, + s.sequence, + s."position" as old_position, + e."position" as new_position + from + projections.current_states s + join eventstore.events2 e on s.instance_id = e.instance_id + and s.aggregate_id = e.aggregate_id + and s.aggregate_type = e.aggregate_type + and s.sequence = e.sequence + and s."position" != e."position" + where + s."position" != 0 + and projection_name != 'projections.execution_handler' + ),fixed as ( + update projections.current_states s + set + "position" = b.new_position + from + broken b + where + s.instance_id = b.instance_id + and s.projection_name = b.projection_name + and s.aggregate_id = b.aggregate_id + and s.aggregate_type = b.aggregate_type + and s.sequence = b.sequence + returning * + ) +select + b.projection_name, + b.instance_id, + b.aggregate_id, + b.aggregate_type, + b.sequence, + b.old_position, + b.new_position, + b.old_position - b.new_position difference +from + broken b; +``` + +If the output from the above looks reasonable, for example not a huge number in the `difference` column, commit the transaction: + +```sql +commit; +``` + +When there are no rows returned, your system was not affected by precision loss. + +When there's unexpected output, use `rollback;` instead. diff --git a/docs/docs/support/technical_advisory.mdx b/docs/docs/support/technical_advisory.mdx index 0d8818c32c..5d13cfe3e5 100644 --- a/docs/docs/support/technical_advisory.mdx +++ b/docs/docs/support/technical_advisory.mdx @@ -238,6 +238,18 @@ We understand that these advisories may include breaking changes, and we aim to 3.0.0 2025-03-31 + + + A-10016 + + Position precision fix + Manual Intervention + + + + 2.65.10 + 2025-05-14 + ## Subscribe to our Mailing List diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index 1f45a017ac..0af3063972 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -23,6 +23,7 @@ module.exports = { description: "Documentation for ZITADEL - Identity infrastructure, simplified for you.", }, + themeConfig: { metadata: [ { @@ -71,13 +72,13 @@ module.exports = { label: "🚀 Quick Start", docId: "guides/start/quickstart", position: "left", - }, + }, { type: "doc", label: "Documentation", docId: "guides/overview", position: "left", - }, + }, { type: "doc", label: "APIs", @@ -174,20 +175,25 @@ module.exports = { { label: "Status", href: "https://status.zitadel.com/", - } + }, ], }, ], copyright: `Copyright © ${new Date().getFullYear()} ZITADEL Docs - Built with Docusaurus.`, }, - algolia: { - appId: "8H6ZKXENLO", - apiKey: "124fe1c102a184bc6fc70c75dc84f96f", - indexName: "zitadel", - selector: "div#", - }, prism: { - additionalLanguages: ["csharp", "dart", "groovy", "regex", "java", "php", "python", "protobuf", "json", "bash"], + additionalLanguages: [ + "csharp", + "dart", + "groovy", + "regex", + "java", + "php", + "python", + "protobuf", + "json", + "bash", + ], }, colorMode: { defaultMode: "dark", @@ -196,33 +202,11 @@ module.exports = { }, codeblock: { showGithubLink: true, - githubLinkLabel: 'View on GitHub', + githubLinkLabel: "View on GitHub", showRunmeLink: false, - runmeLinkLabel: 'Checkout via Runme' + runmeLinkLabel: "Checkout via Runme", }, }, - webpack: { - jsLoader: (isServer) => ({ - loader: require.resolve('swc-loader'), - options: { - jsc: { - parser: { - syntax: 'typescript', - tsx: true, - }, - transform: { - react: { - runtime: 'automatic', - }, - }, - target: 'es2017', - }, - module: { - type: isServer ? 'commonjs' : 'es6', - }, - }, - }), - }, presets: [ [ "classic", @@ -235,19 +219,43 @@ module.exports = { showLastUpdateTime: true, editUrl: "https://github.com/zitadel/zitadel/edit/main/docs/", remarkPlugins: [require("mdx-mermaid")], - - docItemComponent: '@theme/ApiItem' + docItemComponent: "@theme/ApiItem", }, theme: { customCss: require.resolve("./src/css/custom.css"), }, - }) + }), ], - ], plugins: [ [ - 'docusaurus-plugin-openapi-docs', + "@inkeep/cxkit-docusaurus", + { + SearchBar: { + baseSettings: { + apiKey: process.env.INKEEP_API_KEY, + primaryBrandColor: "#ff2069", + organizationDisplayName: "ZITADEL", + }, + }, + SearchSettings: { + tabs: ["All", "Docs", "GitHub", "Forums", "Discord"], + }, + }, + ], + [ + "@signalwire/docusaurus-plugin-llms-txt", + { + depth: 3, + logLevel: 1, + content: { + excludeRoutes: ["/search"], + enableMarkdownFiles: true, + }, + }, + ], + [ + "docusaurus-plugin-openapi-docs", { id: "apiDocs", docsPluginId: "classic", @@ -257,7 +265,7 @@ module.exports = { outputDir: "docs/apis/resources/auth", sidebarOptions: { groupPathsBy: "tag", - categoryLinkSource: "tag", + categoryLinkSource: "auto", }, }, mgmt: { @@ -265,7 +273,7 @@ module.exports = { outputDir: "docs/apis/resources/mgmt", sidebarOptions: { groupPathsBy: "tag", - categoryLinkSource: "tag", + categoryLinkSource: "auto", }, }, admin: { @@ -273,7 +281,7 @@ module.exports = { outputDir: "docs/apis/resources/admin", sidebarOptions: { groupPathsBy: "tag", - categoryLinkSource: "tag", + categoryLinkSource: "auto", }, }, system: { @@ -281,11 +289,12 @@ module.exports = { outputDir: "docs/apis/resources/system", sidebarOptions: { groupPathsBy: "tag", - categoryLinkSource: "tag", + categoryLinkSource: "auto", }, }, user_v2: { - specPath: ".artifacts/openapi/zitadel/user/v2/user_service.swagger.json", + specPath: + ".artifacts/openapi/zitadel/user/v2/user_service.swagger.json", outputDir: "docs/apis/resources/user_service_v2", sidebarOptions: { groupPathsBy: "tag", @@ -293,7 +302,8 @@ module.exports = { }, }, session_v2: { - specPath: ".artifacts/openapi/zitadel/session/v2/session_service.swagger.json", + specPath: + ".artifacts/openapi/zitadel/session/v2/session_service.swagger.json", outputDir: "docs/apis/resources/session_service_v2", sidebarOptions: { groupPathsBy: "tag", @@ -301,7 +311,8 @@ module.exports = { }, }, oidc_v2: { - specPath: ".artifacts/openapi/zitadel/oidc/v2/oidc_service.swagger.json", + specPath: + ".artifacts/openapi/zitadel/oidc/v2/oidc_service.swagger.json", outputDir: "docs/apis/resources/oidc_service_v2", sidebarOptions: { groupPathsBy: "tag", @@ -309,7 +320,8 @@ module.exports = { }, }, saml_v2: { - specPath: ".artifacts/openapi/zitadel/saml/v2/saml_service.swagger.json", + specPath: + ".artifacts/openapi/zitadel/saml/v2/saml_service.swagger.json", outputDir: "docs/apis/resources/saml_service_v2", sidebarOptions: { groupPathsBy: "tag", @@ -317,7 +329,8 @@ module.exports = { }, }, settings_v2: { - specPath: ".artifacts/openapi/zitadel/settings/v2/settings_service.swagger.json", + specPath: + ".artifacts/openapi/zitadel/settings/v2/settings_service.swagger.json", outputDir: "docs/apis/resources/settings_service_v2", sidebarOptions: { groupPathsBy: "tag", @@ -325,31 +338,35 @@ module.exports = { }, }, action_v2: { - specPath: ".artifacts/openapi/zitadel/action/v2beta/action_service.swagger.json", + specPath: + ".artifacts/openapi/zitadel/action/v2beta/action_service.swagger.json", outputDir: "docs/apis/resources/action_service_v2", sidebarOptions: { - groupPathsBy: "tag", - categoryLinkSource: "auto", + groupPathsBy: "tag", + categoryLinkSource: "auto", }, }, webkey_v2: { - specPath: ".artifacts/openapi/zitadel/webkey/v2beta/webkey_service.swagger.json", + specPath: + ".artifacts/openapi3/zitadel/webkey/v2/webkey_service.openapi.yaml", outputDir: "docs/apis/resources/webkey_service_v2", sidebarOptions: { - groupPathsBy: "tag", - categoryLinkSource: "auto", + groupPathsBy: "tag", + categoryLinkSource: "auto", }, }, feature_v2: { - specPath: ".artifacts/openapi/zitadel/feature/v2/feature_service.swagger.json", + specPath: + ".artifacts/openapi/zitadel/feature/v2/feature_service.swagger.json", outputDir: "docs/apis/resources/feature_service_v2", sidebarOptions: { - groupPathsBy: "tag", - categoryLinkSource: "auto", + groupPathsBy: "tag", + categoryLinkSource: "auto", }, }, org_v2: { - specPath: ".artifacts/openapi/zitadel/org/v2/org_service.swagger.json", + specPath: + ".artifacts/openapi/zitadel/org/v2/org_service.swagger.json", outputDir: "docs/apis/resources/org_service_v2", sidebarOptions: { groupPathsBy: "tag", @@ -357,13 +374,67 @@ module.exports = { }, }, idp_v2: { - specPath: ".artifacts/openapi/zitadel/idp/v2/idp_service.swagger.json", + specPath: + ".artifacts/openapi/zitadel/idp/v2/idp_service.swagger.json", outputDir: "docs/apis/resources/idp_service_v2", sidebarOptions: { groupPathsBy: "tag", categoryLinkSource: "auto", }, }, + org_v2beta: { + specPath: + ".artifacts/openapi3/zitadel/org/v2beta/org_service.openapi.yaml", + outputDir: "docs/apis/resources/org_service_v2beta", + sidebarOptions: { + groupPathsBy: "tag", + categoryLinkSource: "auto", + }, + }, + project_v2beta: { + specPath: + ".artifacts/openapi3/zitadel/project/v2beta/project_service.openapi.yaml", + outputDir: "docs/apis/resources/project_service_v2", + sidebarOptions: { + groupPathsBy: "tag", + categoryLinkSource: "auto", + }, + }, + application_v2: { + specPath: ".artifacts/openapi3/zitadel/app/v2beta/app_service.openapi.yaml", + outputDir: "docs/apis/resources/application_service_v2", + sidebarOptions: { + groupPathsBy: "tag", + categoryLinkSource: "auto", + }, + }, + instance_v2: { + specPath: + ".artifacts/openapi3/zitadel/instance/v2beta/instance_service.openapi.yaml", + outputDir: "docs/apis/resources/instance_service_v2", + sidebarOptions: { + groupPathsBy: "tag", + categoryLinkSource: "auto", + }, + }, + authorization_v2: { + specPath: + ".artifacts/openapi3/zitadel/authorization/v2beta/authorization_service.openapi.yaml", + outputDir: "docs/apis/resources/authorization_service_v2", + sidebarOptions: { + groupPathsBy: "tag", + categoryLinkSource: "auto", + }, + }, + internal_permission_v2: { + specPath: + ".artifacts/openapi3/zitadel/internal_permission/v2beta/internal_permission_service.openapi.yaml", + outputDir: "docs/apis/resources/internal_permission_service_v2", + sidebarOptions: { + groupPathsBy: "tag", + categoryLinkSource: "auto", + }, + }, }, }, ], @@ -380,5 +451,25 @@ module.exports = { }; }, ], - themes: [ "docusaurus-theme-github-codeblock", "docusaurus-theme-openapi-docs"], + markdown: { + mermaid:true, + }, + themes: [ + "docusaurus-theme-github-codeblock", + "docusaurus-theme-openapi-docs", + '@docusaurus/theme-mermaid', + ], + future: { + v4: false, // Disabled because of some problems related to https://github.com/facebook/docusaurus/issues/11040 + experimental_faster: { + swcJsLoader: false, // Disabled because of memory usage > 8GB which is a problem on vercel default runners + swcJsMinimizer: true, + swcHtmlMinimizer: true, + lightningCssMinimizer: true, + mdxCrossCompilerCache: true, + ssgWorkerThreads: false, // Disabled because of some problems related to https://github.com/facebook/docusaurus/issues/11040 + rspackBundler: true, + rspackPersistentCache: true, + }, + }, }; diff --git a/docs/frameworks.json b/docs/frameworks.json index 163b493274..f5fec02b42 100644 --- a/docs/frameworks.json +++ b/docs/frameworks.json @@ -1,127 +1,169 @@ [ - { - "id": "angular", - "title": "Angular", - "description": "This preset sets up an OIDC configuration with Authentication Code Flow, secured by PKCE", - "imgSrcLight": "/docs/img/tech/angular.svg", - "imgSrcDark": "/docs/img/tech/angular.svg", - "docsLink": "/docs/sdk-examples/angular" - }, - { - "id": "flutter", - "title": "Flutter", - "imgSrcDark": "/docs/img/tech/flutter.svg", - "docsLink": "/docs/sdk-examples/flutter" - }, - { - "title": "Go", - "imgSrcDark": "/docs/img/tech/golang.svg", - "docsLink": "/docs/sdk-examples/go" - }, - { - "id": "java", - "title": "Java", - "imgSrcDark": "/docs/img/tech/java.svg", - "docsLink": "/docs/sdk-examples/java" - }, - { - "title": "NestJS", - "imgSrcDark": "/docs/img/tech/nestjs.svg", - "docsLink": "/docs/sdk-examples/nestjs" - }, - { - "id": "next", - "title": "Next.js", - "imgSrcDark": "/docs/img/tech/nextjs.svg", - "imgSrcLight": "/docs/img/tech/nextjslight.svg", - "docsLink": "/docs/sdk-examples/nextjs" - }, - { - "id": "django", - "title": "Python Django", - "imgSrcDark": "/docs/img/tech/django.png", - "docsLink": "/docs/sdk-examples/python-django" - }, - { - "title": "Python Flask", - "imgSrcDark": "/docs/img/tech/flask.svg", - "imgSrcLight": "/docs/img/tech/flasklight.svg", - "docsLink": "/docs/sdk-examples/python-flask" - }, - { - "id": "react", - "title": "React", - "description": "This preset sets up an OIDC configuration with Authentication Code Flow, secured by PKCE", - "imgSrcDark": "/docs/img/tech/react.png", - "docsLink": "/docs/sdk-examples/react" - }, - { - "id": "symfony", - "title": "Symfony", - "imgSrcDark": "/docs/img/tech/php.svg", - "docsLink": "/docs/sdk-examples/symfony" - }, - { - "id": "vue", - "title": "Vue.js", - "description": "This preset sets up an OIDC configuration with Authentication Code Flow, secured by PKCE", - "imgSrcDark": "/docs/img/tech/vue.svg", - "docsLink": "/docs/sdk-examples/vue" - }, - { - "title": "Dart", - "imgSrcDark": "/docs/img/tech/dart.svg", - "docsLink": "https://github.com/smartive/zitadel-dart", - "external": true - }, - { - "title": "Elixir", - "imgSrcDark": "/docs/img/tech/elixir.svg", - "docsLink": "https://github.com/maennchen/zitadel_api", - "external": true - }, - { - "title": "FastAPI", - "imgSrcDark": "/docs/img/tech/fastapi.svg", - "docsLink": "https://github.com/cleanenergyexchange/fastapi-zitadel-auth", - "external": true - }, - { - "title": "NextAuth", - "imgSrcDark": "/docs/img/tech/nextjs.svg", - "imgSrcLight": "/docs/img/tech/nextjslight.svg", - "docsLink": "https://next-auth.js.org/providers/zitadel", - "external": true - }, - { - "title": "Node.js", - "imgSrcDark": "/docs/img/tech/nodejs.svg", - "docsLink": "https://www.npmjs.com/package/@zitadel/node", - "external": true - }, - { - "title": ".Net", - "imgSrcDark": "/docs/img/tech/dotnet.svg", - "docsLink": "https://github.com/smartive/zitadel-net", - "external": true - }, - { - "title": "Passport.js", - "imgSrcDark": "/docs/img/tech/passportjs.svg", - "docsLink": "https://github.com/buehler/node-passport-zitadel", - "external": true - }, - { - "title": "Rust", - "imgSrcLight": "/docs/img/tech/rust.svg", - "imgSrcDark": "/docs/img/tech/rustlight.svg", - "docsLink": "https://github.com/smartive/zitadel-rust", - "external": true - }, - { - "title": "Pylon", - "imgSrcDark": "/docs/img/tech/pylon.svg", - "docsLink": "https://github.com/getcronit/pylon", - "external": true - } + { + "id": "client-php", + "title": "PHP", + "imgSrcDark": "/docs/img/tech/php.svg", + "docsLink": "/docs/sdk-examples/client-libraries/php", + "client": true + }, + { + "id": "client-java", + "title": "Java", + "imgSrcDark": "/docs/img/tech/java.svg", + "docsLink": "/docs/sdk-examples/client-libraries/java", + "client": true + }, + { + "id": "client-go", + "title": "Go", + "imgSrcDark": "/docs/img/tech/golang.svg", + "docsLink": "/docs/sdk-examples/go", + "client": true, + "sdk": true + }, + { + "id": "client-ruby", + "title": "Ruby", + "imgSrcDark": "/docs/img/tech/ruby.svg", + "docsLink": "/docs/sdk-examples/client-libraries/ruby", + "client": true + }, + { + "id": "client-python", + "title": "Python", + "imgSrcDark": "/docs/img/tech/python.svg", + "docsLink": "/docs/sdk-examples/client-libraries/python", + "client": true + }, + + { + "id": "angular", + "title": "Angular", + "description": "This preset sets up an OIDC configuration with Authentication Code Flow, secured by PKCE", + "imgSrcLight": "/docs/img/tech/angular.svg", + "imgSrcDark": "/docs/img/tech/angular.svg", + "docsLink": "/docs/sdk-examples/angular" + }, + { + "id": "flutter", + "title": "Flutter", + "imgSrcDark": "/docs/img/tech/flutter.svg", + "docsLink": "/docs/sdk-examples/flutter" + }, + { + "title": "Go", + "imgSrcDark": "/docs/img/tech/golang.svg", + "docsLink": "/docs/sdk-examples/go" + }, + { + "id": "spring", + "title": "Spring", + "imgSrcDark": "/docs/img/tech/spring.svg", + "docsLink": "/docs/sdk-examples/java" + }, + { + "title": "NestJS", + "imgSrcDark": "/docs/img/tech/nestjs.svg", + "docsLink": "/docs/sdk-examples/nestjs" + }, + { + "id": "next", + "title": "Next.js", + "imgSrcDark": "/docs/img/tech/nextjs.svg", + "imgSrcLight": "/docs/img/tech/nextjslight.svg", + "docsLink": "/docs/sdk-examples/nextjs" + }, + { + "id": "django", + "title": "Python Django", + "imgSrcDark": "/docs/img/tech/django.svg", + "docsLink": "/docs/sdk-examples/python-django" + }, + { + "title": "Python Flask", + "imgSrcDark": "/docs/img/tech/flask.svg", + "imgSrcLight": "/docs/img/tech/flask.svg", + "docsLink": "/docs/sdk-examples/python-flask" + }, + { + "id": "react", + "title": "React", + "description": "This preset sets up an OIDC configuration with Authentication Code Flow, secured by PKCE", + "imgSrcDark": "/docs/img/tech/react.svg", + "docsLink": "/docs/sdk-examples/react" + }, + { + "id": "symfony", + "title": "Symfony", + "imgSrcDark": "/docs/img/tech/symfony.svg", + "docsLink": "/docs/sdk-examples/symfony" + }, + { + "id": "vue", + "title": "Vue.js", + "description": "This preset sets up an OIDC configuration with Authentication Code Flow, secured by PKCE", + "imgSrcDark": "/docs/img/tech/vue.svg", + "docsLink": "/docs/sdk-examples/vue" + }, + { + "title": "Dart", + "imgSrcDark": "/docs/img/tech/dart.svg", + "docsLink": "https://github.com/smartive/zitadel-dart", + "external": true, + "client": true, + "sdk": true + }, + { + "title": "Elixir", + "imgSrcDark": "/docs/img/tech/elixir.svg", + "docsLink": "https://github.com/maennchen/zitadel_api", + "external": true + }, + { + "title": "FastAPI", + "imgSrcDark": "/docs/img/tech/fastapi.svg", + "docsLink": "https://github.com/cleanenergyexchange/fastapi-zitadel-auth", + "external": true + }, + { + "title": "NextAuth", + "imgSrcDark": "/docs/img/tech/nextjs.svg", + "imgSrcLight": "/docs/img/tech/nextjslight.svg", + "docsLink": "https://next-auth.js.org/providers/zitadel", + "external": true + }, + { + "title": "Node.js", + "imgSrcDark": "/docs/img/tech/nodejs.svg", + "docsLink": "https://www.npmjs.com/package/@zitadel/node", + "external": true, + "client": true + }, + { + "title": ".Net", + "imgSrcDark": "/docs/img/tech/dotnet.svg", + "docsLink": "https://github.com/smartive/zitadel-net", + "external": true, + "client": true + }, + { + "title": "Passport.js", + "imgSrcDark": "/docs/img/tech/passportjs.svg", + "docsLink": "https://github.com/buehler/node-passport-zitadel", + "external": true + }, + { + "title": "Rust", + "imgSrcLight": "/docs/img/tech/rust.svg", + "imgSrcDark": "/docs/img/tech/rustlight.svg", + "docsLink": "https://github.com/smartive/zitadel-rust", + "client": true, + "external": true + }, + { + "title": "Pylon", + "imgSrcDark": "/docs/img/tech/pylon.svg", + "docsLink": "https://github.com/getcronit/pylon", + "external": true + } ] diff --git a/docs/package.json b/docs/package.json index f9636418dd..8423e5352d 100644 --- a/docs/package.json +++ b/docs/package.json @@ -4,51 +4,52 @@ "private": true, "scripts": { "docusaurus": "docusaurus", + "dev": "docusaurus start", "start": "docusaurus start", - "start:api": "yarn run generate && docusaurus start", - "build": "yarn run generate && NODE_OPTIONS=--max-old-space-size=8192 docusaurus build", + "start:api": "pnpm run generate && docusaurus start", + "build": "pnpm run ensure-plugins && pnpm run generate && docusaurus build", "swizzle": "docusaurus swizzle", "deploy": "docusaurus deploy", "clear": "docusaurus clear", "serve": "docusaurus serve", "write-translations": "docusaurus write-translations", "write-heading-ids": "docusaurus write-heading-ids", - "generate": "yarn run generate:grpc && yarn run generate:apidocs && yarn run generate:configdocs", - "generate:grpc": "buf generate ../proto", + "ensure-plugins": "if [ ! -f \"protoc-gen-connect-openapi/protoc-gen-connect-openapi\" ]; then sh ./plugin-download.sh; fi", + "debug-plugins": "echo \"PWD: $(pwd)\" && echo \"Plugin file exists: $(test -f protoc-gen-connect-openapi/protoc-gen-connect-openapi && echo 'yes' || echo 'no')\" && echo \"Plugin executable: $(test -x protoc-gen-connect-openapi/protoc-gen-connect-openapi && echo 'yes' || echo 'no')\" && ls -la protoc-gen-connect-openapi/ || echo 'Plugin directory not found'", + "generate": "pnpm run generate:grpc && pnpm run generate:apidocs && pnpm run generate:configdocs", + "generate:grpc": "pnpm run ensure-plugins && buf generate ../proto", "generate:apidocs": "docusaurus gen-api-docs all", "generate:configdocs": "cp -r ../cmd/defaults.yaml ./docs/self-hosting/manage/configure/ && cp -r ../cmd/setup/steps.yaml ./docs/self-hosting/manage/configure/", - "generate:re-gen": "yarn generate:clean-all && yarn generate", - "generate:clean-all": "docusaurus clean-api-docs all" + "generate:re-gen": "yarn generate:clean-all && pnpm generate", + "generate:clean-all": "docusaurus clean-api-docs all", + "postinstall": "sh ./plugin-download.sh", + "clean": "rm -rf node_modules .artifacts .docusaurus .turbo protoc-gen-connect-openapi docs/apis/resources" }, "dependencies": { "@bufbuild/buf": "^1.14.0", - "@docusaurus/core": "3.4.0", - "@docusaurus/preset-classic": "3.4.0", - "@docusaurus/theme-mermaid": "3.4.0", - "@docusaurus/theme-search-algolia": "3.4.0", + "@docusaurus/core": "^3.8.1", + "@docusaurus/faster": "^3.8.1", + "@docusaurus/preset-classic": "^3.8.1", + "@docusaurus/theme-mermaid": "^3.8.1", + "@docusaurus/theme-search-algolia": "^3.8.1", "@headlessui/react": "^1.7.4", "@heroicons/react": "^2.0.13", - "@mdx-js/react": "^3.0.0", - "@swc/core": "^1.3.74", + "@signalwire/docusaurus-plugin-llms-txt": "^1.2.0", + "@inkeep/cxkit-docusaurus": "^0.5.89", "autoprefixer": "^10.4.13", "clsx": "^1.2.1", - "docusaurus-plugin-image-zoom": "^1.0.1", - "docusaurus-plugin-openapi-docs": "3.0.1", + "docusaurus-plugin-image-zoom": "^3.0.1", + "docusaurus-plugin-openapi-docs": "4.4.0", "docusaurus-theme-github-codeblock": "^2.0.2", - "docusaurus-theme-openapi-docs": "3.0.1", + "docusaurus-theme-openapi-docs": "4.4.0", "mdx-mermaid": "^2.0.0", - "mermaid": "^10.9.1", "postcss": "^8.4.31", - "prism-react-renderer": "^2.1.0", "raw-loader": "^4.0.2", "react": "^18.2.0", "react-copy-to-clipboard": "^5.1.0", "react-dom": "^18.2.0", "react-google-charts": "^5.2.1", - "react-player": "^2.15.1", - "sitemap": "7.1.1", - "swc-loader": "^0.2.3", - "wait-on": "6.0.1" + "react-player": "^2.15.1" }, "browserslist": { "production": [ @@ -63,9 +64,9 @@ ] }, "devDependencies": { - "@docusaurus/module-type-aliases": "3.4.0", - "@docusaurus/types": "3.4.0", + "@docusaurus/module-type-aliases": "^3.8.1", + "@docusaurus/types": "^3.8.1", "tailwindcss": "^3.2.4" }, - "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" + "packageManager": "pnpm@9.1.2+sha256.19c17528f9ca20bd442e4ca42f00f1b9808a9cb419383cd04ba32ef19322aba7" } diff --git a/docs/plugin-download.sh b/docs/plugin-download.sh new file mode 100644 index 0000000000..64d4bfb320 --- /dev/null +++ b/docs/plugin-download.sh @@ -0,0 +1,70 @@ +#!/bin/bash +set -e + +echo "Downloading protoc-gen-connect-openapi plugin..." +echo "Architecture: $(uname -m)" +echo "OS: $(uname)" + +# Create directory if it doesn't exist +mkdir -p protoc-gen-connect-openapi +cd ./protoc-gen-connect-openapi/ + +# Skip download if plugin already exists and is executable +if [ -f "protoc-gen-connect-openapi" ] && [ -x "protoc-gen-connect-openapi" ]; then + echo "Plugin already exists and is executable" + ./protoc-gen-connect-openapi --version || echo "Plugin version check failed, but file exists" + exit 0 +fi + +# Clean up any partial downloads +rm -f protoc-gen-connect-openapi.tar.gz protoc-gen-connect-openapi + +# Determine download URL based on OS and architecture +if [ "$(uname)" = "Darwin" ]; then + echo "Downloading for Darwin..." + URL="https://github.com/sudorandom/protoc-gen-connect-openapi/releases/download/v0.18.0/protoc-gen-connect-openapi_0.18.0_darwin_all.tar.gz" +else + ARCH=$(uname -m) + case $ARCH in + x86_64) + ARCH="amd64" + ;; + aarch64|arm64) + ARCH="arm64" + ;; + *) + echo "Unsupported architecture: $ARCH" + exit 1 + ;; + esac + echo "Downloading for Linux ${ARCH}..." + URL="https://github.com/sudorandom/protoc-gen-connect-openapi/releases/download/v0.18.0/protoc-gen-connect-openapi_0.18.0_linux_${ARCH}.tar.gz" +fi + +# Download with retries +echo "Downloading from: $URL" +curl -L -o protoc-gen-connect-openapi.tar.gz "$URL" || { + echo "Download failed, trying with different curl options..." + curl -L --fail --retry 3 --retry-delay 1 -o protoc-gen-connect-openapi.tar.gz "$URL" +} + +echo "Extracting plugin..." +tar -xzf protoc-gen-connect-openapi.tar.gz + +# Verify extraction +if [ ! -f "protoc-gen-connect-openapi" ]; then + echo "ERROR: Plugin binary not found after extraction" + ls -la + exit 1 +fi + +# Make sure the plugin is executable +chmod +x protoc-gen-connect-openapi + +# Verify plugin works +echo "Plugin installed successfully" +ls -la protoc-gen-connect-openapi +./protoc-gen-connect-openapi --version || echo "Plugin version check failed, but installation completed" + +# Clean up +rm -f protoc-gen-connect-openapi.tar.gz \ No newline at end of file diff --git a/docs/sidebars.js b/docs/sidebars.js index 92c7a00b2d..acca7b1659 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -10,9 +10,15 @@ const sidebar_api_oidc_service_v2 = require("./docs/apis/resources/oidc_service_ const sidebar_api_settings_service_v2 = require("./docs/apis/resources/settings_service_v2/sidebar.ts").default const sidebar_api_feature_service_v2 = require("./docs/apis/resources/feature_service_v2/sidebar.ts").default const sidebar_api_org_service_v2 = require("./docs/apis/resources/org_service_v2/sidebar.ts").default +const sidebar_api_org_service_v2beta = require("./docs/apis/resources/org_service_v2beta/sidebar.ts").default const sidebar_api_idp_service_v2 = require("./docs/apis/resources/idp_service_v2/sidebar.ts").default const sidebar_api_actions_v2 = require("./docs/apis/resources/action_service_v2/sidebar.ts").default +const sidebar_api_project_service_v2 = require("./docs/apis/resources/project_service_v2/sidebar.ts").default const sidebar_api_webkey_service_v2 = require("./docs/apis/resources/webkey_service_v2/sidebar.ts").default +const sidebar_api_instance_service_v2 = require("./docs/apis/resources/instance_service_v2/sidebar.ts").default +const sidebar_api_authorization_service_v2 = require("./docs/apis/resources/authorization_service_v2/sidebar.ts").default +const sidebar_api_permission_service_v2 = require("./docs/apis/resources/internal_permission_service_v2/sidebar.ts").default +const sidebar_api_app_v2 = require("./docs/apis/resources/application_service_v2/sidebar.ts").default module.exports = { guides: [ @@ -68,7 +74,6 @@ module.exports = { { type: "category", label: "Examples & SDKs", - link: { type: "doc", id: "sdk-examples/introduction" }, items: [ { type: "autogenerated", @@ -180,7 +185,6 @@ module.exports = { items: [ "guides/manage/user/reg-create-user", "guides/manage/customize/user-metadata", - "guides/manage/customize/user-schema", "guides/manage/user/scim2", ], }, @@ -201,6 +205,8 @@ module.exports = { items: [ "guides/migrate/sources/zitadel", "guides/migrate/sources/auth0", + "guides/migrate/sources/keycloak-guide", + "guides/migrate/sources/auth0-guide", "guides/migrate/sources/keycloak", ], }, @@ -608,6 +614,20 @@ module.exports = { }, ], }, + { + type: "category", + label: "Product Information", + collapsed: true, + items: [ + "product/roadmap", + "product/release-cycle", + { + type: "link", + label: "Changelog", + href: "https://zitadel.com/changelog", + }, + ], + }, { type: "category", label: "Support", @@ -644,6 +664,228 @@ module.exports = { id: "apis/apis/index", }, items: [ + + { + type: "category", + label: "V2", + collapsed: false, + link: { + type: "doc", + id: "apis/v2", + }, + items: [ + { + type: "category", + label: "User", + link: { + type: "generated-index", + title: "User Service API", + slug: "/apis/resources/user_service_v2", + description: + "This API is intended to manage users in a ZITADEL instance.\n", + }, + items: sidebar_api_user_service_v2, + }, + { + type: "category", + label: "Session", + link: { + type: "generated-index", + title: "Session Service API", + slug: "/apis/resources/session_service_v2", + description: + "This API is intended to manage sessions in a ZITADEL instance.\n", + }, + items: sidebar_api_session_service_v2, + }, + { + type: "category", + label: "OIDC", + link: { + type: "generated-index", + title: "OIDC Service API", + slug: "/apis/resources/oidc_service_v2", + description: + "Get OIDC Auth Request details and create callback URLs.\n", + }, + items: sidebar_api_oidc_service_v2, + }, + { + type: "category", + label: "Settings", + link: { + type: "generated-index", + title: "Settings Service API", + slug: "/apis/resources/settings_service_v2", + description: + "This API is intended to manage settings in a ZITADEL instance.\n", + }, + items: sidebar_api_settings_service_v2, + }, + { + type: "category", + label: "Feature", + link: { + type: "generated-index", + title: "Feature Service API", + slug: "/apis/resources/feature_service_v2", + description: + 'This API is intended to manage features for ZITADEL. Feature settings that are available on multiple "levels", such as instance and organization. The higher level instance acts as a default for the lower level. When a feature is set on multiple levels, the lower level takes precedence. Features can be experimental where ZITADEL will assume a sane default, such as disabled. When over time confidence in such a feature grows, ZITADEL can default to enabling the feature. As a final step we might choose to always enable a feature and remove the setting from this API, reserving the proto field number. Such removal is not considered a breaking change. Setting a removed field will effectively result in a no-op.\n', + }, + items: sidebar_api_feature_service_v2, + }, + { + type: "category", + label: "Organization", + link: { + type: "generated-index", + title: "Organization Service API", + slug: "/apis/resources/org_service/v2", + description: + "This API is intended to manage organizations for ZITADEL. \n", + }, + items: sidebar_api_org_service_v2, + }, + { + type: "category", + label: "Organization (Beta)", + link: { + type: "generated-index", + title: "Organization Service Beta API", + slug: "/apis/resources/org_service/v2beta", + description: + "This beta API is intended to manage organizations for ZITADEL. Expect breaking changes to occur. Please use the v2 version for a stable API. \n", + }, + items: sidebar_api_org_service_v2beta, + }, + { + type: "category", + label: "Identity Provider", + link: { + type: "generated-index", + title: "Identity Provider Service API", + slug: "/apis/resources/idp_service_v2", + description: + "This API is intended to manage identity providers (IdPs) for ZITADEL.\n", + }, + items: sidebar_api_idp_service_v2, + }, + { + type: "category", + label: "Web Key", + link: { + type: "generated-index", + title: "Web Key Service API", + slug: "/apis/resources/webkey_service_v2", + description: + "This API is intended to manage web keys for a ZITADEL instance, used to sign and validate OIDC tokens.\n" + + "\n"+ + "The public key endpoint (outside of this service) is used to retrieve the public keys of the active and inactive keys.\n", + }, + items: sidebar_api_webkey_service_v2 + }, + { + type: "category", + label: "Action (Beta)", + link: { + type: "generated-index", + title: "Action Service API (Beta)", + slug: "/apis/resources/action_service_v2", + description: + "This API is intended to manage custom executions and targets (previously known as actions) in a ZITADEL instance.\n" + + "\n" + + "This service is in beta state. It can AND will continue breaking until a stable version is released.\n"+ + "\n" + + "The version 2 of actions provide much more options to customize ZITADELs behaviour than previous action versions.\n" + + "Also, v2 actions are available instance-wide, whereas previous actions had to be managed for each organization individually\n" + + "ZITADEL doesn't restrict the implementation languages, tooling and runtime for v2 action executions anymore.\n" + + "Instead, it calls external endpoints which are implemented and maintained by action v2 users.\n"+ + "\n" + + "Please make sure to enable the `actions` feature flag on your instance to use this service and that you're running Zitadel V3.", + }, + items: sidebar_api_actions_v2, + }, + { + type: "category", + label: "Instance (Beta)", + link: { + type: "generated-index", + title: "Instance Service API (Beta)", + slug: "/apis/resources/instance_service_v2", + description: + "This API is intended to manage instances, custom domains and trusted domains in ZITADEL.\n" + + "\n" + + "This service is in beta state. It can AND will continue breaking until a stable version is released.\n"+ + "\n" + + "This v2 of the API provides the same functionalities as the v1, but organised on a per resource basis.\n" + + "The whole functionality related to domains (custom and trusted) has been moved under this instance API." + , + }, + items: sidebar_api_instance_service_v2, + }, + { + type: "category", + label: "Project (Beta)", + link: { + type: "generated-index", + title: "Project Service API (Beta)", + slug: "/apis/resources/project_service_v2", + description: + "This API is intended to manage projects and subresources for ZITADEL. \n" + + "\n" + + "This service is in beta state. It can AND will continue breaking until a stable version is released.", + }, + items: sidebar_api_project_service_v2, + }, + { + type: "category", + label: "App (Beta)", + link: { + type: "generated-index", + title: "Application Service API (Beta)", + slug: "/apis/resources/application_service_v2", + description: + "This API lets you manage Zitadel applications (API, SAML, OIDC).\n"+ + "\n"+ + "The API offers generic endpoints that work for all app types (API, SAML, OIDC), "+ + "\n"+ + "This API is in beta state. It can AND will continue breaking until a stable version is released.\n" + }, + items: sidebar_api_app_v2, + }, + { + type: "category", + label: "Authorizations (Beta)", + link: { + type: "generated-index", + title: "Authorization Service API (Beta)", + slug: "/apis/resources/authorization_service_v2", + description: + "AuthorizationService provides methods to manage authorizations for users within your projects and applications.\n" + + "\n" + + "For managing permissions and roles for ZITADEL internal resources, like organizations, projects,\n" + + "users, etc., please use the InternalPermissionService."+ + "\n"+ + "This API is in beta state. It can AND will continue breaking until a stable version is released.\n" + }, + items: sidebar_api_authorization_service_v2, + }, + { + type: "category", + label: "Permissions (Beta)", + link: { + type: "generated-index", + title: "Permission Service API (Beta)", + slug: "/apis/resources/permission_service_v2", + description: + "This API is intended to manage internal permissions in ZITADEL.\n" + + "\n"+ + "This API is in beta state. It can AND will continue breaking until a stable version is released.\n" + }, + items: sidebar_api_permission_service_v2, + }, + ], + }, { type: "category", label: "V1", @@ -706,140 +948,7 @@ module.exports = { }, items: sidebar_api_system, }, - ], - }, - { - type: "category", - label: "V2", - collapsed: false, - link: { - type: "doc", - id: "apis/v2", - }, - items: [ - { - type: "category", - label: "User", - link: { - type: "generated-index", - title: "User Service API", - slug: "/apis/resources/user_service_v2", - description: - "This API is intended to manage users in a ZITADEL instance.\n", - }, - items: sidebar_api_user_service_v2, - }, - { - type: "category", - label: "Session", - link: { - type: "generated-index", - title: "Session Service API", - slug: "/apis/resources/session_service_v2", - description: - "This API is intended to manage sessions in a ZITADEL instance.\n", - }, - items: sidebar_api_session_service_v2, - }, - { - type: "category", - label: "OIDC", - link: { - type: "generated-index", - title: "OIDC Service API", - slug: "/apis/resources/oidc_service_v2", - description: - "Get OIDC Auth Request details and create callback URLs.\n", - }, - items: sidebar_api_oidc_service_v2, - }, - { - type: "category", - label: "Settings", - link: { - type: "generated-index", - title: "Settings Service API", - slug: "/apis/resources/settings_service_v2", - description: - "This API is intended to manage settings in a ZITADEL instance.\n", - }, - items: sidebar_api_settings_service_v2, - }, - { - type: "category", - label: "Feature", - link: { - type: "generated-index", - title: "Feature Service API", - slug: "/apis/resources/feature_service_v2", - description: - 'This API is intended to manage features for ZITADEL. Feature settings that are available on multiple "levels", such as instance and organization. The higher level instance acts as a default for the lower level. When a feature is set on multiple levels, the lower level takes precedence. Features can be experimental where ZITADEL will assume a sane default, such as disabled. When over time confidence in such a feature grows, ZITADEL can default to enabling the feature. As a final step we might choose to always enable a feature and remove the setting from this API, reserving the proto field number. Such removal is not considered a breaking change. Setting a removed field will effectively result in a no-op.\n', - }, - items: sidebar_api_feature_service_v2, - }, - { - type: "category", - label: "Organization", - link: { - type: "generated-index", - title: "Organization Service API", - slug: "/apis/resources/org_service/v2", - description: - "This API is intended to manage organizations for ZITADEL. \n", - }, - items: sidebar_api_org_service_v2, - }, - { - type: "category", - label: "Identity Provider", - link: { - type: "generated-index", - title: "Identity Provider Service API", - slug: "/apis/resources/idp_service_v2", - description: - "This API is intended to manage identity providers (IdPs) for ZITADEL.\n", - }, - items: sidebar_api_idp_service_v2, - }, - { - type: "category", - label: "Web key (Beta)", - link: { - type: "generated-index", - title: "Web Key Service API (Beta)", - slug: "/apis/resources/webkey_service_v2", - description: - "This API is intended to manage web keys for a ZITADEL instance, used to sign and validate OIDC tokens.\n" + - "\n" + - "This service is in beta state. It can AND will continue breaking until a stable version is released.\n"+ - "\n"+ - "The public key endpoint (outside of this service) is used to retrieve the public keys of the active and inactive keys.\n"+ - "\n"+ - "Please make sure to enable the `web_key` feature flag on your instance to use this service and that you're running ZITADEL V3.", - }, - items: sidebar_api_webkey_service_v2 - }, - { - type: "category", - label: "Action (Beta)", - link: { - type: "generated-index", - title: "Action Service API (Beta)", - slug: "/apis/resources/action_service_v2", - description: - "This API is intended to manage custom executions and targets (previously known as actions) in a ZITADEL instance.\n" + - "\n" + - "This service is in beta state. It can AND will continue breaking until a stable version is released.\n"+ - "\n" + - "The version 2 of actions provide much more options to customize ZITADELs behaviour than previous action versions.\n" + - "Also, v2 actions are available instance-wide, whereas previous actions had to be managed for each organization individually\n" + - "ZITADEL doesn't restrict the implementation languages, tooling and runtime for v2 action executions anymore.\n" + - "Instead, it calls external endpoints which are implemented and maintained by action v2 users.\n"+ - "\n" + - "Please make sure to enable the `actions` feature flag on your instance to use this service and that you're running Zitadel V3.", - }, - items: sidebar_api_actions_v2, - }, + "apis/migration_v1_to_v2" ], }, { @@ -975,7 +1084,6 @@ module.exports = { "self-hosting/deploy/devcontainer", "self-hosting/deploy/knative", "self-hosting/deploy/kubernetes", - "self-hosting/deploy/loadbalancing-example/loadbalancing-example", "self-hosting/deploy/troubleshooting/troubleshooting", ], }, @@ -1009,6 +1117,7 @@ module.exports = { "self-hosting/manage/tls_modes", "self-hosting/manage/database/database", "self-hosting/manage/cache", + "self-hosting/manage/service_ping", "self-hosting/manage/updating_scaling", "self-hosting/manage/usage_control", { diff --git a/docs/src/components/benchmark_chart.jsx b/docs/src/components/benchmark_chart.jsx index 4f0d4bc61c..cf93a842ef 100644 --- a/docs/src/components/benchmark_chart.jsx +++ b/docs/src/components/benchmark_chart.jsx @@ -1,11 +1,24 @@ import React from "react"; import Chart from "react-google-charts"; -export function BenchmarkChart(testResults=[], height='500px') { +export function BenchmarkChart({ testResults = [], height = '500px' } = {}) { + if (!Array.isArray(testResults)) { + console.error("BenchmarkChart: testResults is not an array. Received:", testResults); + return

Error: Benchmark data is not available or in the wrong format.

; + } + + if (testResults.length === 0) { + return

No benchmark data to display.

; + } + const dataPerMetric = new Map(); let maxVValue = 0; - JSON.parse(testResults.testResults).forEach((result) => { + testResults.forEach((result) => { + if (!result || typeof result.metric_name === 'undefined') { + console.warn("BenchmarkChart: Skipping invalid result item:", result); + return; + } if (!dataPerMetric.has(result.metric_name)) { dataPerMetric.set(result.metric_name, [ [ @@ -16,17 +29,16 @@ export function BenchmarkChart(testResults=[], height='500px') { ], ]); } - if (result.p99 > maxVValue) { + if (result.p99 !== undefined && result.p99 > maxVValue) { maxVValue = result.p99; } dataPerMetric.get(result.metric_name).push([ - new Date(result.timestamp), + result.timestamp ? new Date(result.timestamp) : null, result.p50, result.p95, result.p99, ]); }); - const options = { legend: { position: 'bottom' }, focusTarget: 'category', @@ -35,17 +47,18 @@ export function BenchmarkChart(testResults=[], height='500px') { }, vAxis: { title: 'latency (ms)', - maxValue: maxVValue, + maxValue: maxVValue > 0 ? maxVValue : undefined, }, title: '' }; const charts = []; dataPerMetric.forEach((data, metric) => { - const opt = Object.create(options); + const opt = { ...options }; opt.title = metric; charts.push( No chart data could be generated.

; + } - return (charts); + return <>{charts}; } \ No newline at end of file diff --git a/docs/src/components/frameworks.jsx b/docs/src/components/frameworks.jsx index ca402ec7ff..a514b8ed2f 100644 --- a/docs/src/components/frameworks.jsx +++ b/docs/src/components/frameworks.jsx @@ -2,20 +2,23 @@ import React from "react"; import { Tile } from "./tile"; import frameworks from "../../frameworks.json"; -export function Frameworks({}) { - return ( -
- {frameworks.map((framework) => { - return ( - - ); - })} -
- ); +export function Frameworks({ filter }) { + const filteredFrameworks = frameworks.filter((framework) => { + return filter ? filter(framework) : true; + }); + + return ( +
+ {filteredFrameworks.map((framework) => ( + + ))} +
+ ); } diff --git a/docs/src/components/pii_table.jsx b/docs/src/components/pii_table.jsx new file mode 100644 index 0000000000..5075e1d2ca --- /dev/null +++ b/docs/src/components/pii_table.jsx @@ -0,0 +1,106 @@ +import React from "react"; + +export function PiiTable() { + + const pii = [ + { + type: "Basic data", + examples: [ + 'Names', + 'Email addresses', + 'User names' + ], + subjects: "All users as uploaded by Customer." + }, + { + type: "Login data", + examples: [ + 'Randomly generated ID', + 'Passwords', + 'Public keys / certificates ("FIDO2", "U2F", "x509", ...)', + 'User names or identifiers of external login providers', + 'Phone numbers', + ], + subjects: "All users as uploaded and feature use by Customer." + }, + { + type: "Profile data", + examples: [ + 'Profile pictures', + 'Gender', + 'Languages', + 'Nicknames or Display names', + 'Phone numbers', + 'Metadata' + ], + subjects: "All users as uploaded by Customer" + }, + { + type: "Communication data", + examples: [ + 'Emails', + 'Chats', + 'Call metadata', + 'Call recording and transcripts', + 'Form submissions', + ], + subjects: "Customers and users who communicate with us directly (e.g. support, chat)." + }, + { + type: "Payment data", + examples: [ + 'Billing address', + 'Payment information', + 'Customer number', + 'Support Customer history', + 'Credit rating information', + ], + subjects: "Customers who use services that require payment. Credit rating information: Only customers who pay by invoice." + }, + { + type: "Analytics data", + examples: [ + 'Usage metrics', + 'User behavior', + 'User journeys (eg, Milestones)', + 'Telemetry data', + 'Client-side anonymized session replay', + ], + subjects: "Customers who use our services." + }, + { + type: "Usage meta data", + examples: [ + 'User agent', + 'IP addresses', + 'Operating system', + 'Time and date', + 'URL', + 'Referrer URL', + 'Accepted Language', + ], + subjects: "All users" + }, + ] + + return ( + + + + + + + { + pii.map((row, rowID) => { + return ( + + + + + + ) + }) + } +
Type of personal dataExamplesAffected data subjects
{row.type}
    {row.examples.map((example) => { return (
  • {example}
  • )})}
{row.subjects}
+ ); +} diff --git a/docs/src/components/subprocessors.jsx b/docs/src/components/subprocessors.jsx deleted file mode 100644 index a6bf10eee8..0000000000 --- a/docs/src/components/subprocessors.jsx +++ /dev/null @@ -1,162 +0,0 @@ -import React from "react"; - -export function SubProcessorTable() { - - const country_list = { - us: "USA", - eu: "EU", - ch: "Switzerland", - fr: "France", - in: "India", - de: "Germany", - ee: "Estonia", - nl: "Netherlands", - ro: "Romania", - } - const processors = [ - { - entity: "Google LLC", - purpose: "Cloud infrastructure provider (Google Cloud), business applications and collaboration (Workspace), Data warehouse services, Content delivery network, DDoS and bot prevention", - hosting: "Region designated by Customer, United States", - country: country_list.us, - enduserdata: "Yes" - }, - { - entity: "Datadog, Inc.", - purpose: "Infrastructure monitoring, log analytics, and alerting", - hosting: country_list.eu, - country: country_list.us, - enduserdata: "Yes (logs)" - }, - { - entity: "Github, Inc.", - purpose: "Source code management, code scanning, dependency management, security advisory, issue management, continuous integration", - hosting: country_list.us, - country: country_list.us, - enduserdata: false - }, - { - entity: "Stripe Payments Europe, Ltd.", - purpose: "Subscription management, payment process", - hosting: country_list.us, - country: country_list.us, - enduserdata: false - }, - { - entity: "Bexio AG", - purpose: "Customer management, payment process", - hosting: country_list.ch, - country: country_list.ch, - enduserdata: false - }, - { - entity: "Mailjet SAS", - purpose: "Marketing automation", - hosting: country_list.eu, - country: country_list.fr, - enduserdata: false - }, - { - entity: "Postmark (AC PM LLC)", - purpose: "Transactional mails, if no customer owned SMTP service is configured", - hosting: country_list.us, - country: country_list.us, - enduserdata: "Yes (opt-out)" - }, - { - entity: "Vercel, Inc.", - purpose: "Website hosting", - hosting: country_list.us, - country: country_list.us, - enduserdata: false - }, - { - entity: "Agolia SAS", - purpose: "Documentation search engine (zitadel.com/docs)", - hosting: country_list.us, - country: country_list.in, - enduserdata: false - }, - { - entity: "Discord Netherlands BV", - purpose: "Community chat (zitadel.com/chat)", - hosting: country_list.us, - country: country_list.us, - enduserdata: false - }, - { - entity: "Statuspal", - purpose: "ZITADEL Cloud service status announcements", - hosting: country_list.us, - country: country_list.de, - enduserdata: false - }, - { - entity: "Plausible Insights OÜ", - purpose: "Privacy-friendly web analytics", - hosting: country_list.de, - country: country_list.ee, - enduserdata: false, - dpa: 'https://plausible.io/dpa' - }, - { - entity: "Twillio Inc.", - purpose: "Messaging platform for SMS", - hosting: country_list.us, - country: country_list.us, - enduserdata: "Yes (opt-out)" - }, - { - entity: "Mohlmann Solutions SRL", - purpose: "Global payroll", - hosting: undefined, - country: country_list.ro, - enduserdata: false - }, - { - entity: "Remote Europe Holding, B.V.", - purpose: "Global payroll", - hosting: undefined, - country: country_list.nl, - enduserdata: false - }, - { - entity: "HubSpot Inc.", - purpose: "Customer and sales management, Marketing automation, Support requests", - hosting: country_list.eu, - country: country_list.us, - enduserdata: false - }, - ] - - return ( - - - - - - - - - { - processors - .sort((a, b) => { - if (a.entity < b.entity) return -1 - if (a.entity > b.entity) return 1 - else return 0 - }) - .map((processor, rowID) => { - return ( - - - - - - - - ) - }) - } -
Entity namePurposeEnd-user dataHosting locationCountry of registration
{processor.entity}{processor.purpose}{processor.enduserdata ? processor.enduserdata : 'No'}{processor.hosting ? processor.hosting : 'n/a'}{processor.country}
- ); -} diff --git a/docs/src/css/custom.css b/docs/src/css/custom.css index 5083c842e9..11d4208695 100644 --- a/docs/src/css/custom.css +++ b/docs/src/css/custom.css @@ -612,4 +612,36 @@ p strong { position: absolute; top: 0; left: 0; +} + +/* + The overview list components are enriched by swizzled DocCardList and DocSidebarItems wrappers. + Deprecated list item titles have the class zitadel-lifecycle-deprecated. + We strike them through, because this is well-known practice and reduces mental overhead for finding the right method to solve a task. + */ +.card:has(.zitadel-lifecycle-deprecated) { + position: relative; +} + +.card:has(.zitadel-lifecycle-deprecated)::after { + content: "DEPRECATED"; + position: absolute; + top: 10px; + right: 10px; + background-color: var(--ifm-color-danger-contrast-background); + color: var(--ifm-color-danger-contrast-foreground); + font-size: 12px; + font-weight: 600; + padding: 0.25rem 0.5rem; + border-radius: 0.25rem; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.zitadel-lifecycle-deprecated { + text-decoration: line-through; +} + +table#zitadel-versions td { + vertical-align: top; } \ No newline at end of file diff --git a/docs/src/theme/DocCardList/index.js b/docs/src/theme/DocCardList/index.js new file mode 100644 index 0000000000..c9c24e2b8f --- /dev/null +++ b/docs/src/theme/DocCardList/index.js @@ -0,0 +1,13 @@ +import React from 'react'; +import DocCardList from '@theme-original/DocCardList'; +import toCustomDeprecatedItemsProps from "../../utils/deprecated-items"; + +// The DocCardList component is used in generated index pages for API services. +// We customize it for deprecated items. +export default function DocCardListWrapper(props) { + return ( + <> + + + ); +} diff --git a/docs/src/theme/DocSidebarItems/index.js b/docs/src/theme/DocSidebarItems/index.js new file mode 100644 index 0000000000..35ad429fa2 --- /dev/null +++ b/docs/src/theme/DocSidebarItems/index.js @@ -0,0 +1,14 @@ +import React from 'react'; +import DocSidebarItems from '@theme-original/DocSidebarItems'; +import toCustomDeprecatedItemsProps from '../../utils/deprecated-items.js'; + +// The DocSidebarItems component is used in generated side navs for API services. +// We wrap the original to push deprecated items to the bottom and give them a CSS class. +// This lets us easily style them differently in docs/src/css/custom.css. +export default function DocSidebarItemsWrapper(props) { + return ( + <> + + + ); +} diff --git a/docs/src/utils/deprecated-items.js b/docs/src/utils/deprecated-items.js new file mode 100644 index 0000000000..353a619d30 --- /dev/null +++ b/docs/src/utils/deprecated-items.js @@ -0,0 +1,29 @@ +import React from "react"; + +// This function changes a ListComponents input properties. +// Deprecated items are pushed to the bottom of the list and its labels are given the CSS class zitadel-lifecycle-deprecated. +// They are styled in docs/src/css/custom.css. +export default function (props) { + const { items = [], ...rest } = props; + if (!Array.isArray(items)) { + // Do nothing if items is not an array + return props; + } + const withDeprecated = [...items].map(({className, label, ...itemRest}) => { + const zitadelLifecycleDeprecated = className?.indexOf('menu__list-item--deprecated') > -1 + const wrappedLabel = {label} + return { + zitadelLifecycleDeprecated: zitadelLifecycleDeprecated, + ...itemRest, + className, + label: wrappedLabel, + }; + }); + const sortedItems = [...withDeprecated].sort((a, b) => { + return a.zitadelLifecycleDeprecated - b.zitadelLifecycleDeprecated; + }); + return { + ...rest, + items: sortedItems, + }; +} diff --git a/docs/static/img/go/api-PAT_creation.png b/docs/static/img/go/api-PAT_creation.png new file mode 100644 index 0000000000..b8d2032f05 Binary files /dev/null and b/docs/static/img/go/api-PAT_creation.png differ diff --git a/docs/static/img/go/api-PAT_view.png b/docs/static/img/go/api-PAT_view.png new file mode 100644 index 0000000000..c516f2ae2f Binary files /dev/null and b/docs/static/img/go/api-PAT_view.png differ diff --git a/docs/static/img/go/api-app_details.png b/docs/static/img/go/api-app_details.png new file mode 100644 index 0000000000..730152e937 Binary files /dev/null and b/docs/static/img/go/api-app_details.png differ diff --git a/docs/static/img/go/api-create-auth.png b/docs/static/img/go/api-create-auth.png deleted file mode 100644 index f16980baa9..0000000000 Binary files a/docs/static/img/go/api-create-auth.png and /dev/null differ diff --git a/docs/static/img/go/api-create-key.png b/docs/static/img/go/api-create-key.png deleted file mode 100644 index 200b5f5d12..0000000000 Binary files a/docs/static/img/go/api-create-key.png and /dev/null differ diff --git a/docs/static/img/go/api-create.png b/docs/static/img/go/api-create.png deleted file mode 100644 index 1c21cf0706..0000000000 Binary files a/docs/static/img/go/api-create.png and /dev/null differ diff --git a/docs/static/img/go/api-create_application.png b/docs/static/img/go/api-create_application.png new file mode 100644 index 0000000000..c074d0687f Binary files /dev/null and b/docs/static/img/go/api-create_application.png differ diff --git a/docs/static/img/go/api-create_service_user.png b/docs/static/img/go/api-create_service_user.png new file mode 100644 index 0000000000..6af66bcbca Binary files /dev/null and b/docs/static/img/go/api-create_service_user.png differ diff --git a/docs/static/img/go/api-download_key.png b/docs/static/img/go/api-download_key.png new file mode 100644 index 0000000000..ce2a5fa8cf Binary files /dev/null and b/docs/static/img/go/api-download_key.png differ diff --git a/docs/static/img/go/api-expiration_date.png b/docs/static/img/go/api-expiration_date.png new file mode 100644 index 0000000000..826a87ecd6 Binary files /dev/null and b/docs/static/img/go/api-expiration_date.png differ diff --git a/docs/static/img/go/api-new_key.png b/docs/static/img/go/api-new_key.png new file mode 100644 index 0000000000..f499194b88 Binary files /dev/null and b/docs/static/img/go/api-new_key.png differ diff --git a/docs/static/img/go/api-select_framework.png b/docs/static/img/go/api-select_framework.png new file mode 100644 index 0000000000..aa7f112fe9 Binary files /dev/null and b/docs/static/img/go/api-select_framework.png differ diff --git a/docs/static/img/go/api-select_jwt.png b/docs/static/img/go/api-select_jwt.png new file mode 100644 index 0000000000..161c096b17 Binary files /dev/null and b/docs/static/img/go/api-select_jwt.png differ diff --git a/docs/static/img/go/api-service_user_panel.png b/docs/static/img/go/api-service_user_panel.png new file mode 100644 index 0000000000..d4e9b5beff Binary files /dev/null and b/docs/static/img/go/api-service_user_panel.png differ diff --git a/docs/static/img/product/release-cycle.png b/docs/static/img/product/release-cycle.png new file mode 100644 index 0000000000..7017566ab7 Binary files /dev/null and b/docs/static/img/product/release-cycle.png differ diff --git a/docs/static/img/tech/django.svg b/docs/static/img/tech/django.svg new file mode 100644 index 0000000000..b3e95f0ca9 --- /dev/null +++ b/docs/static/img/tech/django.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/docs/static/img/tech/flask.svg b/docs/static/img/tech/flask.svg index b8f3980529..942ea2143a 100644 --- a/docs/static/img/tech/flask.svg +++ b/docs/static/img/tech/flask.svg @@ -1 +1,4 @@ - \ No newline at end of file + + + + \ No newline at end of file diff --git a/docs/static/img/tech/golang.svg b/docs/static/img/tech/golang.svg index 8164d5589e..026829c2a6 100644 --- a/docs/static/img/tech/golang.svg +++ b/docs/static/img/tech/golang.svg @@ -1,55 +1,8 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + \ No newline at end of file diff --git a/docs/static/img/tech/java.svg b/docs/static/img/tech/java.svg index 80260a726d..9f0f599436 100644 --- a/docs/static/img/tech/java.svg +++ b/docs/static/img/tech/java.svg @@ -1,41 +1,11 @@ - - - - - - - - - - - - - - - - - - + + + + + + + + + + + \ No newline at end of file diff --git a/docs/static/img/tech/nodejs.svg b/docs/static/img/tech/nodejs.svg index 41d044ac6b..ec5b309699 100644 --- a/docs/static/img/tech/nodejs.svg +++ b/docs/static/img/tech/nodejs.svg @@ -1 +1,8 @@ - \ No newline at end of file + + + + + + + + \ No newline at end of file diff --git a/docs/static/img/tech/php.svg b/docs/static/img/tech/php.svg index e4f137cb4c..492cd85d1c 100644 --- a/docs/static/img/tech/php.svg +++ b/docs/static/img/tech/php.svg @@ -1,96 +1,2 @@ - - - Official PHP Logo - - - - image/svg+xml - - Official PHP Logo - - - Colin Viebrock - - - - - - - - - - - - Copyright Colin Viebrock 1997 - All rights reserved. - - - 1997 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + +file_type_php3 \ No newline at end of file diff --git a/docs/static/img/tech/python.svg b/docs/static/img/tech/python.svg index 05602a8956..25df8268f7 100644 --- a/docs/static/img/tech/python.svg +++ b/docs/static/img/tech/python.svg @@ -1 +1,15 @@ - \ No newline at end of file + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/static/img/tech/react.svg b/docs/static/img/tech/react.svg new file mode 100644 index 0000000000..d7cb33c2af --- /dev/null +++ b/docs/static/img/tech/react.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/docs/static/img/tech/ruby.svg b/docs/static/img/tech/ruby.svg new file mode 100644 index 0000000000..1083613cde --- /dev/null +++ b/docs/static/img/tech/ruby.svg @@ -0,0 +1,948 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/static/img/tech/spring.svg b/docs/static/img/tech/spring.svg new file mode 100644 index 0000000000..4f70f0b7e4 --- /dev/null +++ b/docs/static/img/tech/spring.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/docs/static/img/tech/symfony.svg b/docs/static/img/tech/symfony.svg new file mode 100644 index 0000000000..0841b4d3c6 --- /dev/null +++ b/docs/static/img/tech/symfony.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/docs/static/img/tech/vue.svg b/docs/static/img/tech/vue.svg index a1d285eb2a..ad3d675c24 100644 --- a/docs/static/img/tech/vue.svg +++ b/docs/static/img/tech/vue.svg @@ -1,2 +1,2 @@ - - + + \ No newline at end of file diff --git a/docs/turbo.json b/docs/turbo.json new file mode 100644 index 0000000000..0e1ef08025 --- /dev/null +++ b/docs/turbo.json @@ -0,0 +1,45 @@ +{ + "$schema": "https://turbo.build/schema.json", + "extends": ["//"], + "tasks": { + "generate": { + "dependsOn": ["^generate"], + "outputs": ["docs/api/**", "docs/self-hosting/manage/configure/*.yaml"], + "cache": true + }, + "generate:grpc": { + "dependsOn": ["^generate"], + "outputs": ["docs/api/**"], + "cache": true + }, + "generate:apidocs": { + "dependsOn": ["generate:grpc"], + "outputs": ["docs/api/**"], + "cache": true + }, + "generate:configdocs": { + "outputs": ["docs/self-hosting/manage/configure/*.yaml"], + "cache": true + }, + "build": { + "dependsOn": ["generate"], + "outputs": ["build/**"], + "cache": true + }, + "dev": { + "dependsOn": ["generate"], + "cache": false, + "persistent": true + }, + "start": { + "dependsOn": ["generate"], + "cache": false, + "persistent": true + }, + "start:api": { + "dependsOn": ["generate"], + "cache": false, + "persistent": true + } + } +} diff --git a/docs/vercel.json b/docs/vercel.json index 039dc02476..c4ee0ee6e6 100644 --- a/docs/vercel.json +++ b/docs/vercel.json @@ -1,64 +1,223 @@ { - "github": { - "enabled": true - }, - "cleanUrls": true, - "rewrites": [ - { - "source": "/docs/proxy/js/script.js", - "destination": "https://plausible.io/js/script.tagged-events.pageview-props.outbound-links.js" - }, - { - "source": "/docs/proxy/api/event", - "destination": "https://plausible.io/api/event" - }, - { - "source": "/docs/:match*", - "destination": "/:match*" - } - ], - "redirects": [ - { "source": "/", "destination": "/docs" }, - { "source": "/docs/category/apis/:slug*", "destination": "/docs/apis/:slug*", "permanent": true }, - { "source": "/docs/apis/mgmt/:slug*", "destination": "/docs/apis/resources/mgmt/:slug*", "permanent": true }, - { "source": "/docs/apis/auth/:slug*", "destination": "/docs/apis/resources/auth/:slug*", "permanent": true }, - { "source": "/docs/apis/system/:slug*", "destination": "/docs/apis/resources/system/:slug*", "permanent": true }, - { "source": "/docs/apis/admin/:slug*", "destination": "/docs/apis/resources/admin/:slug*", "permanent": true }, - { "source": "/docs/apis/actionsv2/introduction", "destination": "/docs/apis/actions/v2/usage", "permanent": true }, - { "source": "/docs/apis/actionsv2/execution-local", "destination": "/docs/apis/actions/v2/testing-locally", "permanent": true }, - { "source": "/docs/guides/integrate/human-users", "destination": "/docs/guides/integrate/login", "permanent": true }, - { "source": "/docs/guides/solution-scenarios/device-authorization", "destination": "/docs/guides/integrate/login/oidc/device-authorization", "permanent": true }, - { "source": "/docs/guides/integrate/oauth-recommended-flows", "destination": "/docs/guides/integrate/login/oidc/oauth-recommended-flows", "permanent": true }, - { "source": "/docs/guides/integrate/login-users", "destination": "/docs/guides/integrate/login/oidc/login-users", "permanent": true }, - { "source": "/docs/guides/integrate/logout", "destination": "/docs/guides/integrate/login/oidc/logout", "permanent": true }, - { "source": "/docs/guides/solution-scenarios/onboarding", "destination": "/docs/guides/integrate/onboarding", "permanent": true }, - { "source": "/docs/guides/solution-scenarios/onboarding/b2b", "destination": "/docs/guides/integrate/onboarding/b2b", "permanent": true }, - { "source": "/docs/guides/solution-scenarios/onboarding/end-users", "destination": "/docs/guides/integrate/onboarding/end-users", "permanent": true }, - { "source": "/docs/concepts/structure/jwt_idp", "destination": "/docs/guides/integrate/identity-providers/jwt-idp", "permanent": true }, - { "source": "/docs/guides/solution-scenarios/onboarding/end-users", "destination": "/docs/guides/integrate/onboarding/end-users", "permanent": true }, - { "source": "/docs/guides/integrate/serviceusers", "destination": "/docs/guides/integrate/service-users/authenticate-service-users", "permanent": true }, - { "source": "/docs/guides/integrate/private-key-jwt", "destination": "/docs/guides/integrate/service-users/private-key-jwt", "permanent": true }, - { "source": "/docs/guides/integrate/client-credentials", "destination": "/docs/guides/integrate/service-users/client-credentials", "permanent": true }, - { "source": "/docs/guides/integrate/pat", "destination": "/docs/guides/integrate/service-users/private-access-token", "permanent": true }, - { "source": "/docs/guides/integrate/access-zitadel-apis", "destination": "/docs/guides/integrate/zitadel-apis/access-zitadel-apis", "permanent": true }, - { "source": "/docs/guides/integrate/access-zitadel-system-api", "destination": "/docs/guides/integrate/zitadel-apis/access-zitadel-system-api", "permanent": true }, - { "source": "/docs/guides/integrate/event-api", "destination": "/docs/guides/integrate/zitadel-apis/event-api", "permanent": true }, - { "source": "/docs/examples/call-zitadel-api/go", "destination": "/docs/guides/integrate/zitadel-apis/example-zitadel-api-with-go", "permanent": true }, - { "source": "/docs/examples/call-zitadel-api/dot-net", "destination": "/docs/guides/integrate/zitadel-apis/example-zitadel-api-with-dot-net", "permanent": true }, - { "source": "/docs/guides/manage/terraform/basics", "destination": "/docs/guides/manage/terraform-provider", "permanent": true }, - { "source": "/docs/guides/integrate/identity-providers", "destination": "/docs/guides/integrate/identity-providers/introduction", "permanent": true }, - { "source": "/docs/guides/integrate/login/login-users#centralized-authentication-endpoint", "destination": "/docs/guides/integrate/login/hosted-login#centralized-authentication-endpoint", "permanent": true }, - { "source": "/docs/guides/integrate/login/login-users#security-and-compliance", "destination": "/docs/guides/integrate/login/hosted-login#security-and-compliance", "permanent": true }, - { "source": "/docs/guides/integrate/login/login-users#developer-friendly-integration", "destination": "/docs/guides/integrate/login/hosted-login#developer-friendly-integration", "permanent": true }, - { "source": "/docs/guides/integrate/login/login-users#key-features-of-the-hosted-login", "destination": "/docs/guides/integrate/login/hosted-login#key-features-of-the-hosted-login", "permanent": true }, - { "source": "/docs/guides/integrate/login/login-users#flexible-usernames", "destination": "/docs/guides/integrate/login/hosted-login#flexible-usernames", "permanent": true }, - { "source": "/docs/guides/integrate/login/login-users#support-for-multiple-authentication-methods", "destination": "/docs/guides/integrate/login/hosted-login#support-for-multiple-authentication-methods", "permanent": true }, - { "source": "/docs/guides/integrate/login/login-users#enterprise-single-sign-on", "destination": "/docs/guides/integrate/login/hosted-login#enterprise-single-sign-on", "permanent": true }, - { "source": "/docs/guides/integrate/login/login-users#multi-tenancy-authentication", "destination": "/docs/guides/integrate/login/hosted-login#multi-tenancy-authentication", "permanent": true }, - { "source": "/docs/guides/integrate/login/login-users#customization-options", "destination": "/docs/guides/integrate/login/hosted-login#customization-options", "permanent": true }, - { "source": "/docs/guides/integrate/login/login-users#fast-account-switching", "destination": "/docs/guides/integrate/login/hosted-login#fast-account-switching", "permanent": true }, - { "source": "/docs/guides/integrate/login/login-users#self-service-for-users", "destination": "/docs/guides/integrate/login/hosted-login#self-service-for-users", "permanent": true }, - { "source": "/docs/guides/integrate/login/login-users#password-reset", "destination": "/docs/guides/integrate/login/hosted-login#password-reset", "permanent": true } - ] + "github": { + "enabled": true + }, + "cleanUrls": true, + "rewrites": [ + { + "source": "/docs/proxy/js/script.js", + "destination": "https://plausible.io/js/script.tagged-events.pageview-props.outbound-links.js" + }, + { + "source": "/docs/proxy/api/event", + "destination": "https://plausible.io/api/event" + }, + { + "source": "/docs/:match*", + "destination": "/:match*" + } + ], + "redirects": [ + { "source": "/", "destination": "/docs" }, + { + "source": "/docs/category/apis/:slug*", + "destination": "/docs/apis/:slug*", + "permanent": true + }, + { + "source": "/docs/apis/mgmt/:slug*", + "destination": "/docs/apis/resources/mgmt/:slug*", + "permanent": true + }, + { + "source": "/docs/apis/auth/:slug*", + "destination": "/docs/apis/resources/auth/:slug*", + "permanent": true + }, + { + "source": "/docs/apis/system/:slug*", + "destination": "/docs/apis/resources/system/:slug*", + "permanent": true + }, + { + "source": "/docs/apis/admin/:slug*", + "destination": "/docs/apis/resources/admin/:slug*", + "permanent": true + }, + { + "source": "/docs/apis/actionsv2/introduction", + "destination": "/docs/apis/actions/v2/usage", + "permanent": true + }, + { + "source": "/docs/apis/actionsv2/execution-local", + "destination": "/docs/apis/actions/v2/testing-locally", + "permanent": true + }, + { + "source": "/docs/guides/integrate/human-users", + "destination": "/docs/guides/integrate/login", + "permanent": true + }, + { + "source": "/docs/guides/solution-scenarios/device-authorization", + "destination": "/docs/guides/integrate/login/oidc/device-authorization", + "permanent": true + }, + { + "source": "/docs/guides/integrate/oauth-recommended-flows", + "destination": "/docs/guides/integrate/login/oidc/oauth-recommended-flows", + "permanent": true + }, + { + "source": "/docs/guides/integrate/login-users", + "destination": "/docs/guides/integrate/login/oidc/login-users", + "permanent": true + }, + { + "source": "/docs/guides/integrate/logout", + "destination": "/docs/guides/integrate/login/oidc/logout", + "permanent": true + }, + { + "source": "/docs/guides/solution-scenarios/onboarding", + "destination": "/docs/guides/integrate/onboarding", + "permanent": true + }, + { + "source": "/docs/guides/solution-scenarios/onboarding/b2b", + "destination": "/docs/guides/integrate/onboarding/b2b", + "permanent": true + }, + { + "source": "/docs/guides/solution-scenarios/onboarding/end-users", + "destination": "/docs/guides/integrate/onboarding/end-users", + "permanent": true + }, + { + "source": "/docs/concepts/structure/jwt_idp", + "destination": "/docs/guides/integrate/identity-providers/jwt-idp", + "permanent": true + }, + { + "source": "/docs/guides/solution-scenarios/onboarding/end-users", + "destination": "/docs/guides/integrate/onboarding/end-users", + "permanent": true + }, + { + "source": "/docs/guides/integrate/serviceusers", + "destination": "/docs/guides/integrate/service-users/authenticate-service-users", + "permanent": true + }, + { + "source": "/docs/guides/integrate/private-key-jwt", + "destination": "/docs/guides/integrate/service-users/private-key-jwt", + "permanent": true + }, + { + "source": "/docs/guides/integrate/client-credentials", + "destination": "/docs/guides/integrate/service-users/client-credentials", + "permanent": true + }, + { + "source": "/docs/guides/integrate/pat", + "destination": "/docs/guides/integrate/service-users/private-access-token", + "permanent": true + }, + { + "source": "/docs/guides/integrate/access-zitadel-apis", + "destination": "/docs/guides/integrate/zitadel-apis/access-zitadel-apis", + "permanent": true + }, + { + "source": "/docs/guides/integrate/access-zitadel-system-api", + "destination": "/docs/guides/integrate/zitadel-apis/access-zitadel-system-api", + "permanent": true + }, + { + "source": "/docs/guides/integrate/event-api", + "destination": "/docs/guides/integrate/zitadel-apis/event-api", + "permanent": true + }, + { + "source": "/docs/examples/call-zitadel-api/go", + "destination": "/docs/guides/integrate/zitadel-apis/example-zitadel-api-with-go", + "permanent": true + }, + { + "source": "/docs/examples/call-zitadel-api/dot-net", + "destination": "/docs/guides/integrate/zitadel-apis/example-zitadel-api-with-dot-net", + "permanent": true + }, + { + "source": "/docs/guides/manage/terraform/basics", + "destination": "/docs/guides/manage/terraform-provider", + "permanent": true + }, + { + "source": "/docs/guides/integrate/identity-providers", + "destination": "/docs/guides/integrate/identity-providers/introduction", + "permanent": true + }, + { + "source": "/docs/guides/integrate/login/login-users#centralized-authentication-endpoint", + "destination": "/docs/guides/integrate/login/hosted-login#centralized-authentication-endpoint", + "permanent": true + }, + { + "source": "/docs/guides/integrate/login/login-users#security-and-compliance", + "destination": "/docs/guides/integrate/login/hosted-login#security-and-compliance", + "permanent": true + }, + { + "source": "/docs/guides/integrate/login/login-users#developer-friendly-integration", + "destination": "/docs/guides/integrate/login/hosted-login#developer-friendly-integration", + "permanent": true + }, + { + "source": "/docs/guides/integrate/login/login-users#key-features-of-the-hosted-login", + "destination": "/docs/guides/integrate/login/hosted-login#key-features-of-the-hosted-login", + "permanent": true + }, + { + "source": "/docs/guides/integrate/login/login-users#flexible-usernames", + "destination": "/docs/guides/integrate/login/hosted-login#flexible-usernames", + "permanent": true + }, + { + "source": "/docs/guides/integrate/login/login-users#support-for-multiple-authentication-methods", + "destination": "/docs/guides/integrate/login/hosted-login#support-for-multiple-authentication-methods", + "permanent": true + }, + { + "source": "/docs/guides/integrate/login/login-users#enterprise-single-sign-on", + "destination": "/docs/guides/integrate/login/hosted-login#enterprise-single-sign-on", + "permanent": true + }, + { + "source": "/docs/guides/integrate/login/login-users#multi-tenancy-authentication", + "destination": "/docs/guides/integrate/login/hosted-login#multi-tenancy-authentication", + "permanent": true + }, + { + "source": "/docs/guides/integrate/login/login-users#customization-options", + "destination": "/docs/guides/integrate/login/hosted-login#customization-options", + "permanent": true + }, + { + "source": "/docs/guides/integrate/login/login-users#fast-account-switching", + "destination": "/docs/guides/integrate/login/hosted-login#fast-account-switching", + "permanent": true + }, + { + "source": "/docs/guides/integrate/login/login-users#self-service-for-users", + "destination": "/docs/guides/integrate/login/hosted-login#self-service-for-users", + "permanent": true + }, + { + "source": "/docs/guides/integrate/login/login-users#password-reset", + "destination": "/docs/guides/integrate/login/hosted-login#password-reset", + "permanent": true + } + ] } - diff --git a/docs/yarn.lock b/docs/yarn.lock deleted file mode 100644 index ad31e03b5e..0000000000 --- a/docs/yarn.lock +++ /dev/null @@ -1,11838 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@algolia/autocomplete-core@1.9.3": - version "1.9.3" - resolved "https://registry.yarnpkg.com/@algolia/autocomplete-core/-/autocomplete-core-1.9.3.tgz#1d56482a768c33aae0868c8533049e02e8961be7" - integrity sha512-009HdfugtGCdC4JdXUbVJClA0q0zh24yyePn+KUGk3rP7j8FEe/m5Yo/z65gn6nP/cM39PxpzqKrL7A6fP6PPw== - dependencies: - "@algolia/autocomplete-plugin-algolia-insights" "1.9.3" - "@algolia/autocomplete-shared" "1.9.3" - -"@algolia/autocomplete-plugin-algolia-insights@1.9.3": - version "1.9.3" - resolved "https://registry.yarnpkg.com/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.9.3.tgz#9b7f8641052c8ead6d66c1623d444cbe19dde587" - integrity sha512-a/yTUkcO/Vyy+JffmAnTWbr4/90cLzw+CC3bRbhnULr/EM0fGNvM13oQQ14f2moLMcVDyAx/leczLlAOovhSZg== - dependencies: - "@algolia/autocomplete-shared" "1.9.3" - -"@algolia/autocomplete-preset-algolia@1.9.3": - version "1.9.3" - resolved "https://registry.yarnpkg.com/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.9.3.tgz#64cca4a4304cfcad2cf730e83067e0c1b2f485da" - integrity sha512-d4qlt6YmrLMYy95n5TB52wtNDr6EgAIPH81dvvvW8UmuWRgxEtY0NJiPwl/h95JtG2vmRM804M0DSwMCNZlzRA== - dependencies: - "@algolia/autocomplete-shared" "1.9.3" - -"@algolia/autocomplete-shared@1.9.3": - version "1.9.3" - resolved "https://registry.yarnpkg.com/@algolia/autocomplete-shared/-/autocomplete-shared-1.9.3.tgz#2e22e830d36f0a9cf2c0ccd3c7f6d59435b77dfa" - integrity sha512-Wnm9E4Ye6Rl6sTTqjoymD+l8DjSTHsHboVRYrKgEt8Q7UHm9nYbqhN/i0fhUYA3OAEH7WA8x3jfpnmJm3rKvaQ== - -"@algolia/cache-browser-local-storage@4.23.3": - version "4.23.3" - resolved "https://registry.yarnpkg.com/@algolia/cache-browser-local-storage/-/cache-browser-local-storage-4.23.3.tgz#0cc26b96085e1115dac5fcb9d826651ba57faabc" - integrity sha512-vRHXYCpPlTDE7i6UOy2xE03zHF2C8MEFjPN2v7fRbqVpcOvAUQK81x3Kc21xyb5aSIpYCjWCZbYZuz8Glyzyyg== - dependencies: - "@algolia/cache-common" "4.23.3" - -"@algolia/cache-common@4.23.3": - version "4.23.3" - resolved "https://registry.yarnpkg.com/@algolia/cache-common/-/cache-common-4.23.3.tgz#3bec79092d512a96c9bfbdeec7cff4ad36367166" - integrity sha512-h9XcNI6lxYStaw32pHpB1TMm0RuxphF+Ik4o7tcQiodEdpKK+wKufY6QXtba7t3k8eseirEMVB83uFFF3Nu54A== - -"@algolia/cache-in-memory@4.23.3": - version "4.23.3" - resolved "https://registry.yarnpkg.com/@algolia/cache-in-memory/-/cache-in-memory-4.23.3.tgz#3945f87cd21ffa2bec23890c85305b6b11192423" - integrity sha512-yvpbuUXg/+0rbcagxNT7un0eo3czx2Uf0y4eiR4z4SD7SiptwYTpbuS0IHxcLHG3lq22ukx1T6Kjtk/rT+mqNg== - dependencies: - "@algolia/cache-common" "4.23.3" - -"@algolia/client-account@4.23.3": - version "4.23.3" - resolved "https://registry.yarnpkg.com/@algolia/client-account/-/client-account-4.23.3.tgz#8751bbf636e6741c95e7c778488dee3ee430ac6f" - integrity sha512-hpa6S5d7iQmretHHF40QGq6hz0anWEHGlULcTIT9tbUssWUriN9AUXIFQ8Ei4w9azD0hc1rUok9/DeQQobhQMA== - dependencies: - "@algolia/client-common" "4.23.3" - "@algolia/client-search" "4.23.3" - "@algolia/transporter" "4.23.3" - -"@algolia/client-analytics@4.23.3": - version "4.23.3" - resolved "https://registry.yarnpkg.com/@algolia/client-analytics/-/client-analytics-4.23.3.tgz#f88710885278fe6fb6964384af59004a5a6f161d" - integrity sha512-LBsEARGS9cj8VkTAVEZphjxTjMVCci+zIIiRhpFun9jGDUlS1XmhCW7CTrnaWeIuCQS/2iPyRqSy1nXPjcBLRA== - dependencies: - "@algolia/client-common" "4.23.3" - "@algolia/client-search" "4.23.3" - "@algolia/requester-common" "4.23.3" - "@algolia/transporter" "4.23.3" - -"@algolia/client-common@4.23.3": - version "4.23.3" - resolved "https://registry.yarnpkg.com/@algolia/client-common/-/client-common-4.23.3.tgz#891116aa0db75055a7ecc107649f7f0965774704" - integrity sha512-l6EiPxdAlg8CYhroqS5ybfIczsGUIAC47slLPOMDeKSVXYG1n0qGiz4RjAHLw2aD0xzh2EXZ7aRguPfz7UKDKw== - dependencies: - "@algolia/requester-common" "4.23.3" - "@algolia/transporter" "4.23.3" - -"@algolia/client-personalization@4.23.3": - version "4.23.3" - resolved "https://registry.yarnpkg.com/@algolia/client-personalization/-/client-personalization-4.23.3.tgz#35fa8e5699b0295fbc400a8eb211dc711e5909db" - integrity sha512-3E3yF3Ocr1tB/xOZiuC3doHQBQ2zu2MPTYZ0d4lpfWads2WTKG7ZzmGnsHmm63RflvDeLK/UVx7j2b3QuwKQ2g== - dependencies: - "@algolia/client-common" "4.23.3" - "@algolia/requester-common" "4.23.3" - "@algolia/transporter" "4.23.3" - -"@algolia/client-search@4.23.3": - version "4.23.3" - resolved "https://registry.yarnpkg.com/@algolia/client-search/-/client-search-4.23.3.tgz#a3486e6af13a231ec4ab43a915a1f318787b937f" - integrity sha512-P4VAKFHqU0wx9O+q29Q8YVuaowaZ5EM77rxfmGnkHUJggh28useXQdopokgwMeYw2XUht49WX5RcTQ40rZIabw== - dependencies: - "@algolia/client-common" "4.23.3" - "@algolia/requester-common" "4.23.3" - "@algolia/transporter" "4.23.3" - -"@algolia/events@^4.0.1": - version "4.0.1" - resolved "https://registry.yarnpkg.com/@algolia/events/-/events-4.0.1.tgz#fd39e7477e7bc703d7f893b556f676c032af3950" - integrity sha512-FQzvOCgoFXAbf5Y6mYozw2aj5KCJoA3m4heImceldzPSMbdyS4atVjJzXKMsfX3wnZTFYwkkt8/z8UesLHlSBQ== - -"@algolia/logger-common@4.23.3": - version "4.23.3" - resolved "https://registry.yarnpkg.com/@algolia/logger-common/-/logger-common-4.23.3.tgz#35c6d833cbf41e853a4f36ba37c6e5864920bfe9" - integrity sha512-y9kBtmJwiZ9ZZ+1Ek66P0M68mHQzKRxkW5kAAXYN/rdzgDN0d2COsViEFufxJ0pb45K4FRcfC7+33YB4BLrZ+g== - -"@algolia/logger-console@4.23.3": - version "4.23.3" - resolved "https://registry.yarnpkg.com/@algolia/logger-console/-/logger-console-4.23.3.tgz#30f916781826c4db5f51fcd9a8a264a06e136985" - integrity sha512-8xoiseoWDKuCVnWP8jHthgaeobDLolh00KJAdMe9XPrWPuf1by732jSpgy2BlsLTaT9m32pHI8CRfrOqQzHv3A== - dependencies: - "@algolia/logger-common" "4.23.3" - -"@algolia/recommend@4.23.3": - version "4.23.3" - resolved "https://registry.yarnpkg.com/@algolia/recommend/-/recommend-4.23.3.tgz#53d4f194d22d9c72dc05f3f7514c5878f87c5890" - integrity sha512-9fK4nXZF0bFkdcLBRDexsnGzVmu4TSYZqxdpgBW2tEyfuSSY54D4qSRkLmNkrrz4YFvdh2GM1gA8vSsnZPR73w== - dependencies: - "@algolia/cache-browser-local-storage" "4.23.3" - "@algolia/cache-common" "4.23.3" - "@algolia/cache-in-memory" "4.23.3" - "@algolia/client-common" "4.23.3" - "@algolia/client-search" "4.23.3" - "@algolia/logger-common" "4.23.3" - "@algolia/logger-console" "4.23.3" - "@algolia/requester-browser-xhr" "4.23.3" - "@algolia/requester-common" "4.23.3" - "@algolia/requester-node-http" "4.23.3" - "@algolia/transporter" "4.23.3" - -"@algolia/requester-browser-xhr@4.23.3": - version "4.23.3" - resolved "https://registry.yarnpkg.com/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.23.3.tgz#9e47e76f60d540acc8b27b4ebc7a80d1b41938b9" - integrity sha512-jDWGIQ96BhXbmONAQsasIpTYWslyjkiGu0Quydjlowe+ciqySpiDUrJHERIRfELE5+wFc7hc1Q5hqjGoV7yghw== - dependencies: - "@algolia/requester-common" "4.23.3" - -"@algolia/requester-common@4.23.3": - version "4.23.3" - resolved "https://registry.yarnpkg.com/@algolia/requester-common/-/requester-common-4.23.3.tgz#7dbae896e41adfaaf1d1fa5f317f83a99afb04b3" - integrity sha512-xloIdr/bedtYEGcXCiF2muajyvRhwop4cMZo+K2qzNht0CMzlRkm8YsDdj5IaBhshqfgmBb3rTg4sL4/PpvLYw== - -"@algolia/requester-node-http@4.23.3": - version "4.23.3" - resolved "https://registry.yarnpkg.com/@algolia/requester-node-http/-/requester-node-http-4.23.3.tgz#c9f94a5cb96a15f48cea338ab6ef16bbd0ff989f" - integrity sha512-zgu++8Uj03IWDEJM3fuNl34s746JnZOWn1Uz5taV1dFyJhVM/kTNw9Ik7YJWiUNHJQXcaD8IXD1eCb0nq/aByA== - dependencies: - "@algolia/requester-common" "4.23.3" - -"@algolia/transporter@4.23.3": - version "4.23.3" - resolved "https://registry.yarnpkg.com/@algolia/transporter/-/transporter-4.23.3.tgz#545b045b67db3850ddf0bbecbc6c84ff1f3398b7" - integrity sha512-Wjl5gttqnf/gQKJA+dafnD0Y6Yw97yvfY8R9h0dQltX1GXTgNs1zWgvtWW0tHl1EgMdhAyw189uWiZMnL3QebQ== - dependencies: - "@algolia/cache-common" "4.23.3" - "@algolia/logger-common" "4.23.3" - "@algolia/requester-common" "4.23.3" - -"@alloc/quick-lru@^5.2.0": - version "5.2.0" - resolved "https://registry.yarnpkg.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz#7bf68b20c0a350f936915fcae06f58e32007ce30" - integrity sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw== - -"@ampproject/remapping@^2.2.0": - version "2.3.0" - resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4" - integrity sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw== - dependencies: - "@jridgewell/gen-mapping" "^0.3.5" - "@jridgewell/trace-mapping" "^0.3.24" - -"@apidevtools/json-schema-ref-parser@^11.5.4": - version "11.6.4" - resolved "https://registry.yarnpkg.com/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.6.4.tgz#0f3e02302f646471d621a8850e6a346d63c8ebd4" - integrity sha512-9K6xOqeevacvweLGik6LnZCb1fBtCOSIWQs8d096XGeqoLKC33UVMGz9+77Gw44KvbH4pKcQPWo4ZpxkXYj05w== - dependencies: - "@jsdevtools/ono" "^7.1.3" - "@types/json-schema" "^7.0.15" - js-yaml "^4.1.0" - -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.16.0", "@babel/code-frame@^7.24.7", "@babel/code-frame@^7.8.3": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.24.7.tgz#882fd9e09e8ee324e496bd040401c6f046ef4465" - integrity sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA== - dependencies: - "@babel/highlight" "^7.24.7" - picocolors "^1.0.0" - -"@babel/code-frame@^7.26.2": - version "7.26.2" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.26.2.tgz#4b5fab97d33338eff916235055f0ebc21e573a85" - integrity sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ== - dependencies: - "@babel/helper-validator-identifier" "^7.25.9" - js-tokens "^4.0.0" - picocolors "^1.0.0" - -"@babel/compat-data@^7.22.6", "@babel/compat-data@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.24.7.tgz#d23bbea508c3883ba8251fb4164982c36ea577ed" - integrity sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw== - -"@babel/core@^7.21.3", "@babel/core@^7.23.3": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.24.7.tgz#b676450141e0b52a3d43bc91da86aa608f950ac4" - integrity sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g== - dependencies: - "@ampproject/remapping" "^2.2.0" - "@babel/code-frame" "^7.24.7" - "@babel/generator" "^7.24.7" - "@babel/helper-compilation-targets" "^7.24.7" - "@babel/helper-module-transforms" "^7.24.7" - "@babel/helpers" "^7.24.7" - "@babel/parser" "^7.24.7" - "@babel/template" "^7.24.7" - "@babel/traverse" "^7.24.7" - "@babel/types" "^7.24.7" - convert-source-map "^2.0.0" - debug "^4.1.0" - gensync "^1.0.0-beta.2" - json5 "^2.2.3" - semver "^6.3.1" - -"@babel/generator@^7.23.3", "@babel/generator@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.24.7.tgz#1654d01de20ad66b4b4d99c135471bc654c55e6d" - integrity sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA== - dependencies: - "@babel/types" "^7.24.7" - "@jridgewell/gen-mapping" "^0.3.5" - "@jridgewell/trace-mapping" "^0.3.25" - jsesc "^2.5.1" - -"@babel/helper-annotate-as-pure@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz#5373c7bc8366b12a033b4be1ac13a206c6656aab" - integrity sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg== - dependencies: - "@babel/types" "^7.24.7" - -"@babel/helper-builder-binary-assignment-operator-visitor@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.24.7.tgz#37d66feb012024f2422b762b9b2a7cfe27c7fba3" - integrity sha512-xZeCVVdwb4MsDBkkyZ64tReWYrLRHlMN72vP7Bdm3OUOuyFZExhsHUUnuWnm2/XOlAJzR0LfPpB56WXZn0X/lA== - dependencies: - "@babel/traverse" "^7.24.7" - "@babel/types" "^7.24.7" - -"@babel/helper-compilation-targets@^7.22.6", "@babel/helper-compilation-targets@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.7.tgz#4eb6c4a80d6ffeac25ab8cd9a21b5dfa48d503a9" - integrity sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg== - dependencies: - "@babel/compat-data" "^7.24.7" - "@babel/helper-validator-option" "^7.24.7" - browserslist "^4.22.2" - lru-cache "^5.1.1" - semver "^6.3.1" - -"@babel/helper-create-class-features-plugin@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.7.tgz#2eaed36b3a1c11c53bdf80d53838b293c52f5b3b" - integrity sha512-kTkaDl7c9vO80zeX1rJxnuRpEsD5tA81yh11X1gQo+PhSti3JS+7qeZo9U4RHobKRiFPKaGK3svUAeb8D0Q7eg== - dependencies: - "@babel/helper-annotate-as-pure" "^7.24.7" - "@babel/helper-environment-visitor" "^7.24.7" - "@babel/helper-function-name" "^7.24.7" - "@babel/helper-member-expression-to-functions" "^7.24.7" - "@babel/helper-optimise-call-expression" "^7.24.7" - "@babel/helper-replace-supers" "^7.24.7" - "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" - "@babel/helper-split-export-declaration" "^7.24.7" - semver "^6.3.1" - -"@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.24.7.tgz#be4f435a80dc2b053c76eeb4b7d16dd22cfc89da" - integrity sha512-03TCmXy2FtXJEZfbXDTSqq1fRJArk7lX9DOFC/47VthYcxyIOx+eXQmdo6DOQvrbpIix+KfXwvuXdFDZHxt+rA== - dependencies: - "@babel/helper-annotate-as-pure" "^7.24.7" - regexpu-core "^5.3.1" - semver "^6.3.1" - -"@babel/helper-define-polyfill-provider@^0.6.1", "@babel/helper-define-polyfill-provider@^0.6.2": - version "0.6.2" - resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.2.tgz#18594f789c3594acb24cfdb4a7f7b7d2e8bd912d" - integrity sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ== - 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" - -"@babel/helper-environment-visitor@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz#4b31ba9551d1f90781ba83491dd59cf9b269f7d9" - integrity sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ== - dependencies: - "@babel/types" "^7.24.7" - -"@babel/helper-function-name@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz#75f1e1725742f39ac6584ee0b16d94513da38dd2" - integrity sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA== - dependencies: - "@babel/template" "^7.24.7" - "@babel/types" "^7.24.7" - -"@babel/helper-hoist-variables@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz#b4ede1cde2fd89436397f30dc9376ee06b0f25ee" - integrity sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ== - dependencies: - "@babel/types" "^7.24.7" - -"@babel/helper-member-expression-to-functions@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.7.tgz#67613d068615a70e4ed5101099affc7a41c5225f" - integrity sha512-LGeMaf5JN4hAT471eJdBs/GK1DoYIJ5GCtZN/EsL6KUiiDZOvO/eKE11AMZJa2zP4zk4qe9V2O/hxAmkRc8p6w== - dependencies: - "@babel/traverse" "^7.24.7" - "@babel/types" "^7.24.7" - -"@babel/helper-module-imports@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz#f2f980392de5b84c3328fc71d38bd81bbb83042b" - integrity sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA== - dependencies: - "@babel/traverse" "^7.24.7" - "@babel/types" "^7.24.7" - -"@babel/helper-module-transforms@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.24.7.tgz#31b6c9a2930679498db65b685b1698bfd6c7daf8" - integrity sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ== - dependencies: - "@babel/helper-environment-visitor" "^7.24.7" - "@babel/helper-module-imports" "^7.24.7" - "@babel/helper-simple-access" "^7.24.7" - "@babel/helper-split-export-declaration" "^7.24.7" - "@babel/helper-validator-identifier" "^7.24.7" - -"@babel/helper-optimise-call-expression@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.24.7.tgz#8b0a0456c92f6b323d27cfd00d1d664e76692a0f" - integrity sha512-jKiTsW2xmWwxT1ixIdfXUZp+P5yURx2suzLZr5Hi64rURpDYdMW0pv+Uf17EYk2Rd428Lx4tLsnjGJzYKDM/6A== - dependencies: - "@babel/types" "^7.24.7" - -"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.24.7", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.7.tgz#98c84fe6fe3d0d3ae7bfc3a5e166a46844feb2a0" - integrity sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg== - -"@babel/helper-remap-async-to-generator@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.24.7.tgz#b3f0f203628522713849d49403f1a414468be4c7" - integrity sha512-9pKLcTlZ92hNZMQfGCHImUpDOlAgkkpqalWEeftW5FBya75k8Li2ilerxkM/uBEj01iBZXcCIB/bwvDYgWyibA== - dependencies: - "@babel/helper-annotate-as-pure" "^7.24.7" - "@babel/helper-environment-visitor" "^7.24.7" - "@babel/helper-wrap-function" "^7.24.7" - -"@babel/helper-replace-supers@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.24.7.tgz#f933b7eed81a1c0265740edc91491ce51250f765" - integrity sha512-qTAxxBM81VEyoAY0TtLrx1oAEJc09ZK67Q9ljQToqCnA+55eNwCORaxlKyu+rNfX86o8OXRUSNUnrtsAZXM9sg== - dependencies: - "@babel/helper-environment-visitor" "^7.24.7" - "@babel/helper-member-expression-to-functions" "^7.24.7" - "@babel/helper-optimise-call-expression" "^7.24.7" - -"@babel/helper-simple-access@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz#bcade8da3aec8ed16b9c4953b74e506b51b5edb3" - integrity sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg== - dependencies: - "@babel/traverse" "^7.24.7" - "@babel/types" "^7.24.7" - -"@babel/helper-skip-transparent-expression-wrappers@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.24.7.tgz#5f8fa83b69ed5c27adc56044f8be2b3ea96669d9" - integrity sha512-IO+DLT3LQUElMbpzlatRASEyQtfhSE0+m465v++3jyyXeBTBUjtVZg28/gHeV5mrTJqvEKhKroBGAvhW+qPHiQ== - dependencies: - "@babel/traverse" "^7.24.7" - "@babel/types" "^7.24.7" - -"@babel/helper-split-export-declaration@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz#83949436890e07fa3d6873c61a96e3bbf692d856" - integrity sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA== - dependencies: - "@babel/types" "^7.24.7" - -"@babel/helper-string-parser@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz#4d2d0f14820ede3b9807ea5fc36dfc8cd7da07f2" - integrity sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg== - -"@babel/helper-string-parser@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz#1aabb72ee72ed35789b4bbcad3ca2862ce614e8c" - integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA== - -"@babel/helper-validator-identifier@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz#75b889cfaf9e35c2aaf42cf0d72c8e91719251db" - integrity sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w== - -"@babel/helper-validator-identifier@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7" - integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ== - -"@babel/helper-validator-option@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.24.7.tgz#24c3bb77c7a425d1742eec8fb433b5a1b38e62f6" - integrity sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw== - -"@babel/helper-wrap-function@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.24.7.tgz#52d893af7e42edca7c6d2c6764549826336aae1f" - integrity sha512-N9JIYk3TD+1vq/wn77YnJOqMtfWhNewNE+DJV4puD2X7Ew9J4JvrzrFDfTfyv5EgEXVy9/Wt8QiOErzEmv5Ifw== - dependencies: - "@babel/helper-function-name" "^7.24.7" - "@babel/template" "^7.24.7" - "@babel/traverse" "^7.24.7" - "@babel/types" "^7.24.7" - -"@babel/helpers@^7.24.7": - version "7.27.0" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.27.0.tgz#53d156098defa8243eab0f32fa17589075a1b808" - integrity sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg== - dependencies: - "@babel/template" "^7.27.0" - "@babel/types" "^7.27.0" - -"@babel/highlight@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.24.7.tgz#a05ab1df134b286558aae0ed41e6c5f731bf409d" - integrity sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw== - dependencies: - "@babel/helper-validator-identifier" "^7.24.7" - chalk "^2.4.2" - js-tokens "^4.0.0" - picocolors "^1.0.0" - -"@babel/parser@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.24.7.tgz#9a5226f92f0c5c8ead550b750f5608e766c8ce85" - integrity sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw== - -"@babel/parser@^7.27.0": - version "7.27.0" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.27.0.tgz#3d7d6ee268e41d2600091cbd4e145ffee85a44ec" - integrity sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg== - dependencies: - "@babel/types" "^7.27.0" - -"@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.24.7.tgz#fd059fd27b184ea2b4c7e646868a9a381bbc3055" - integrity sha512-TiT1ss81W80eQsN+722OaeQMY/G4yTb4G9JrqeiDADs3N8lbPMGldWi9x8tyqCW5NLx1Jh2AvkE6r6QvEltMMQ== - dependencies: - "@babel/helper-environment-visitor" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.24.7.tgz#468096ca44bbcbe8fcc570574e12eb1950e18107" - integrity sha512-unaQgZ/iRu/By6tsjMZzpeBZjChYfLYry6HrEXPoz3KmfF0sVBQ1l8zKMQ4xRGLWVsjuvB8nQfjNP/DcfEOCsg== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.24.7.tgz#e4eabdd5109acc399b38d7999b2ef66fc2022f89" - integrity sha512-+izXIbke1T33mY4MSNnrqhPXDz01WYhEf3yF5NbnUtkiNnm+XBZJl3kNfoK6NKmYlz/D07+l2GWVK/QfDkNCuQ== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" - "@babel/plugin-transform-optional-chaining" "^7.24.7" - -"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.24.7.tgz#71b21bb0286d5810e63a1538aa901c58e87375ec" - integrity sha512-utA4HuR6F4Vvcr+o4DnjL8fCOlgRFGbeeBEGNg3ZTrLFw6VWG5XmUrvcQ0FjIYMU2ST4XcR2Wsp7t9qOAPnxMg== - dependencies: - "@babel/helper-environment-visitor" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2": - version "7.21.0-placeholder-for-preset-env.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz#7844f9289546efa9febac2de4cfe358a050bd703" - integrity sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w== - -"@babel/plugin-syntax-async-generators@^7.8.4": - version "7.8.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" - integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-class-properties@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" - integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== - dependencies: - "@babel/helper-plugin-utils" "^7.12.13" - -"@babel/plugin-syntax-class-static-block@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz#195df89b146b4b78b3bf897fd7a257c84659d406" - integrity sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-syntax-dynamic-import@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3" - integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-export-namespace-from@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz#028964a9ba80dbc094c915c487ad7c4e7a66465a" - integrity sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - -"@babel/plugin-syntax-import-assertions@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.24.7.tgz#2a0b406b5871a20a841240586b1300ce2088a778" - integrity sha512-Ec3NRUMoi8gskrkBe3fNmEQfxDvY8bgfQpz6jlk/41kX9eUjvpyqWU7PBP/pLAvMaSQjbMNKJmvX57jP+M6bPg== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-syntax-import-attributes@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.7.tgz#b4f9ea95a79e6912480c4b626739f86a076624ca" - integrity sha512-hbX+lKKeUMGihnK8nvKqmXBInriT3GVjzXKFriV3YC6APGxMbP8RZNFwy91+hocLXq90Mta+HshoB31802bb8A== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-syntax-import-meta@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51" - integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-syntax-json-strings@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" - integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-jsx@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.7.tgz#39a1fa4a7e3d3d7f34e2acc6be585b718d30e02d" - integrity sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-syntax-logical-assignment-operators@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" - integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" - integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-numeric-separator@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" - integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-syntax-object-rest-spread@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" - integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-optional-catch-binding@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" - integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-optional-chaining@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" - integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-private-property-in-object@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz#0dc6671ec0ea22b6e94a1114f857970cd39de1ad" - integrity sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-syntax-top-level-await@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c" - integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-syntax-typescript@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.7.tgz#58d458271b4d3b6bb27ee6ac9525acbb259bad1c" - integrity sha512-c/+fVeJBB0FeKsFvwytYiUD+LBvhHjGSI0g446PRGdSVGZLRNArBUno2PETbAly3tpiNAQR5XaZ+JslxkotsbA== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-syntax-unicode-sets-regex@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz#d49a3b3e6b52e5be6740022317580234a6a47357" - integrity sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-arrow-functions@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.24.7.tgz#4f6886c11e423bd69f3ce51dbf42424a5f275514" - integrity sha512-Dt9LQs6iEY++gXUwY03DNFat5C2NbO48jj+j/bSAz6b3HgPs39qcPiYt77fDObIcFwj3/C2ICX9YMwGflUoSHQ== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-transform-async-generator-functions@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.24.7.tgz#7330a5c50e05181ca52351b8fd01642000c96cfd" - integrity sha512-o+iF77e3u7ZS4AoAuJvapz9Fm001PuD2V3Lp6OSE4FYQke+cSewYtnek+THqGRWyQloRCyvWL1OkyfNEl9vr/g== - dependencies: - "@babel/helper-environment-visitor" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/helper-remap-async-to-generator" "^7.24.7" - "@babel/plugin-syntax-async-generators" "^7.8.4" - -"@babel/plugin-transform-async-to-generator@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.24.7.tgz#72a3af6c451d575842a7e9b5a02863414355bdcc" - integrity sha512-SQY01PcJfmQ+4Ash7NE+rpbLFbmqA2GPIgqzxfFTL4t1FKRq4zTms/7htKpoCUI9OcFYgzqfmCdH53s6/jn5fA== - dependencies: - "@babel/helper-module-imports" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/helper-remap-async-to-generator" "^7.24.7" - -"@babel/plugin-transform-block-scoped-functions@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.24.7.tgz#a4251d98ea0c0f399dafe1a35801eaba455bbf1f" - integrity sha512-yO7RAz6EsVQDaBH18IDJcMB1HnrUn2FJ/Jslc/WtPPWcjhpUJXU/rjbwmluzp7v/ZzWcEhTMXELnnsz8djWDwQ== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-transform-block-scoping@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.24.7.tgz#42063e4deb850c7bd7c55e626bf4e7ab48e6ce02" - integrity sha512-Nd5CvgMbWc+oWzBsuaMcbwjJWAcp5qzrbg69SZdHSP7AMY0AbWFqFO0WTFCA1jxhMCwodRwvRec8k0QUbZk7RQ== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-transform-class-properties@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.24.7.tgz#256879467b57b0b68c7ddfc5b76584f398cd6834" - integrity sha512-vKbfawVYayKcSeSR5YYzzyXvsDFWU2mD8U5TFeXtbCPLFUqe7GyCgvO6XDHzje862ODrOwy6WCPmKeWHbCFJ4w== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-transform-class-static-block@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.7.tgz#c82027ebb7010bc33c116d4b5044fbbf8c05484d" - integrity sha512-HMXK3WbBPpZQufbMG4B46A90PkuuhN9vBCb5T8+VAHqvAqvcLi+2cKoukcpmUYkszLhScU3l1iudhrks3DggRQ== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/plugin-syntax-class-static-block" "^7.14.5" - -"@babel/plugin-transform-classes@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.24.7.tgz#4ae6ef43a12492134138c1e45913f7c46c41b4bf" - integrity sha512-CFbbBigp8ln4FU6Bpy6g7sE8B/WmCmzvivzUC6xDAdWVsjYTXijpuuGJmYkAaoWAzcItGKT3IOAbxRItZ5HTjw== - dependencies: - "@babel/helper-annotate-as-pure" "^7.24.7" - "@babel/helper-compilation-targets" "^7.24.7" - "@babel/helper-environment-visitor" "^7.24.7" - "@babel/helper-function-name" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/helper-replace-supers" "^7.24.7" - "@babel/helper-split-export-declaration" "^7.24.7" - globals "^11.1.0" - -"@babel/plugin-transform-computed-properties@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.24.7.tgz#4cab3214e80bc71fae3853238d13d097b004c707" - integrity sha512-25cS7v+707Gu6Ds2oY6tCkUwsJ9YIDbggd9+cu9jzzDgiNq7hR/8dkzxWfKWnTic26vsI3EsCXNd4iEB6e8esQ== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/template" "^7.24.7" - -"@babel/plugin-transform-destructuring@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.7.tgz#a097f25292defb6e6cc16d6333a4cfc1e3c72d9e" - integrity sha512-19eJO/8kdCQ9zISOf+SEUJM/bAUIsvY3YDnXZTupUCQ8LgrWnsG/gFB9dvXqdXnRXMAM8fvt7b0CBKQHNGy1mw== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-transform-dotall-regex@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.24.7.tgz#5f8bf8a680f2116a7207e16288a5f974ad47a7a0" - integrity sha512-ZOA3W+1RRTSWvyqcMJDLqbchh7U4NRGqwRfFSVbOLS/ePIP4vHB5e8T8eXcuqyN1QkgKyj5wuW0lcS85v4CrSw== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-transform-duplicate-keys@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.24.7.tgz#dd20102897c9a2324e5adfffb67ff3610359a8ee" - integrity sha512-JdYfXyCRihAe46jUIliuL2/s0x0wObgwwiGxw/UbgJBr20gQBThrokO4nYKgWkD7uBaqM7+9x5TU7NkExZJyzw== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-transform-dynamic-import@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.24.7.tgz#4d8b95e3bae2b037673091aa09cd33fecd6419f4" - integrity sha512-sc3X26PhZQDb3JhORmakcbvkeInvxz+A8oda99lj7J60QRuPZvNAk9wQlTBS1ZynelDrDmTU4pw1tyc5d5ZMUg== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/plugin-syntax-dynamic-import" "^7.8.3" - -"@babel/plugin-transform-exponentiation-operator@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.24.7.tgz#b629ee22645f412024297d5245bce425c31f9b0d" - integrity sha512-Rqe/vSc9OYgDajNIK35u7ot+KeCoetqQYFXM4Epf7M7ez3lWlOjrDjrwMei6caCVhfdw+mIKD4cgdGNy5JQotQ== - dependencies: - "@babel/helper-builder-binary-assignment-operator-visitor" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-transform-export-namespace-from@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.24.7.tgz#176d52d8d8ed516aeae7013ee9556d540c53f197" - integrity sha512-v0K9uNYsPL3oXZ/7F9NNIbAj2jv1whUEtyA6aujhekLs56R++JDQuzRcP2/z4WX5Vg/c5lE9uWZA0/iUoFhLTA== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/plugin-syntax-export-namespace-from" "^7.8.3" - -"@babel/plugin-transform-for-of@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.24.7.tgz#f25b33f72df1d8be76399e1b8f3f9d366eb5bc70" - integrity sha512-wo9ogrDG1ITTTBsy46oGiN1dS9A7MROBTcYsfS8DtsImMkHk9JXJ3EWQM6X2SUw4x80uGPlwj0o00Uoc6nEE3g== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" - -"@babel/plugin-transform-function-name@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.24.7.tgz#6d8601fbffe665c894440ab4470bc721dd9131d6" - integrity sha512-U9FcnA821YoILngSmYkW6FjyQe2TyZD5pHt4EVIhmcTkrJw/3KqcrRSxuOo5tFZJi7TE19iDyI1u+weTI7bn2w== - dependencies: - "@babel/helper-compilation-targets" "^7.24.7" - "@babel/helper-function-name" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-transform-json-strings@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.24.7.tgz#f3e9c37c0a373fee86e36880d45b3664cedaf73a" - integrity sha512-2yFnBGDvRuxAaE/f0vfBKvtnvvqU8tGpMHqMNpTN2oWMKIR3NqFkjaAgGwawhqK/pIN2T3XdjGPdaG0vDhOBGw== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/plugin-syntax-json-strings" "^7.8.3" - -"@babel/plugin-transform-literals@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.24.7.tgz#36b505c1e655151a9d7607799a9988fc5467d06c" - integrity sha512-vcwCbb4HDH+hWi8Pqenwnjy+UiklO4Kt1vfspcQYFhJdpthSnW8XvWGyDZWKNVrVbVViI/S7K9PDJZiUmP2fYQ== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-transform-logical-assignment-operators@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.24.7.tgz#a58fb6eda16c9dc8f9ff1c7b1ba6deb7f4694cb0" - integrity sha512-4D2tpwlQ1odXmTEIFWy9ELJcZHqrStlzK/dAOWYyxX3zT0iXQB6banjgeOJQXzEc4S0E0a5A+hahxPaEFYftsw== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" - -"@babel/plugin-transform-member-expression-literals@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.24.7.tgz#3b4454fb0e302e18ba4945ba3246acb1248315df" - integrity sha512-T/hRC1uqrzXMKLQ6UCwMT85S3EvqaBXDGf0FaMf4446Qx9vKwlghvee0+uuZcDUCZU5RuNi4781UQ7R308zzBw== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-transform-modules-amd@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.24.7.tgz#65090ed493c4a834976a3ca1cde776e6ccff32d7" - integrity sha512-9+pB1qxV3vs/8Hdmz/CulFB8w2tuu6EB94JZFsjdqxQokwGa9Unap7Bo2gGBGIvPmDIVvQrom7r5m/TCDMURhg== - dependencies: - "@babel/helper-module-transforms" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-transform-modules-commonjs@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.7.tgz#9fd5f7fdadee9085886b183f1ad13d1ab260f4ab" - integrity sha512-iFI8GDxtevHJ/Z22J5xQpVqFLlMNstcLXh994xifFwxxGslr2ZXXLWgtBeLctOD63UFDArdvN6Tg8RFw+aEmjQ== - dependencies: - "@babel/helper-module-transforms" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/helper-simple-access" "^7.24.7" - -"@babel/plugin-transform-modules-systemjs@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.24.7.tgz#f8012316c5098f6e8dee6ecd58e2bc6f003d0ce7" - integrity sha512-GYQE0tW7YoaN13qFh3O1NCY4MPkUiAH3fiF7UcV/I3ajmDKEdG3l+UOcbAm4zUE3gnvUU+Eni7XrVKo9eO9auw== - dependencies: - "@babel/helper-hoist-variables" "^7.24.7" - "@babel/helper-module-transforms" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/helper-validator-identifier" "^7.24.7" - -"@babel/plugin-transform-modules-umd@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.24.7.tgz#edd9f43ec549099620df7df24e7ba13b5c76efc8" - integrity sha512-3aytQvqJ/h9z4g8AsKPLvD4Zqi2qT+L3j7XoFFu1XBlZWEl2/1kWnhmAbxpLgPrHSY0M6UA02jyTiwUVtiKR6A== - dependencies: - "@babel/helper-module-transforms" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-transform-named-capturing-groups-regex@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.24.7.tgz#9042e9b856bc6b3688c0c2e4060e9e10b1460923" - integrity sha512-/jr7h/EWeJtk1U/uz2jlsCioHkZk1JJZVcc8oQsJ1dUlaJD83f4/6Zeh2aHt9BIFokHIsSeDfhUmju0+1GPd6g== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-transform-new-target@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.24.7.tgz#31ff54c4e0555cc549d5816e4ab39241dfb6ab00" - integrity sha512-RNKwfRIXg4Ls/8mMTza5oPF5RkOW8Wy/WgMAp1/F1yZ8mMbtwXW+HDoJiOsagWrAhI5f57Vncrmr9XeT4CVapA== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-transform-nullish-coalescing-operator@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.24.7.tgz#1de4534c590af9596f53d67f52a92f12db984120" - integrity sha512-Ts7xQVk1OEocqzm8rHMXHlxvsfZ0cEF2yomUqpKENHWMF4zKk175Y4q8H5knJes6PgYad50uuRmt3UJuhBw8pQ== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" - -"@babel/plugin-transform-numeric-separator@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.24.7.tgz#bea62b538c80605d8a0fac9b40f48e97efa7de63" - integrity sha512-e6q1TiVUzvH9KRvicuxdBTUj4AdKSRwzIyFFnfnezpCfP2/7Qmbb8qbU2j7GODbl4JMkblitCQjKYUaX/qkkwA== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/plugin-syntax-numeric-separator" "^7.10.4" - -"@babel/plugin-transform-object-rest-spread@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.7.tgz#d13a2b93435aeb8a197e115221cab266ba6e55d6" - integrity sha512-4QrHAr0aXQCEFni2q4DqKLD31n2DL+RxcwnNjDFkSG0eNQ/xCavnRkfCUjsyqGC2OviNJvZOF/mQqZBw7i2C5Q== - dependencies: - "@babel/helper-compilation-targets" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/plugin-syntax-object-rest-spread" "^7.8.3" - "@babel/plugin-transform-parameters" "^7.24.7" - -"@babel/plugin-transform-object-super@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.24.7.tgz#66eeaff7830bba945dd8989b632a40c04ed625be" - integrity sha512-A/vVLwN6lBrMFmMDmPPz0jnE6ZGx7Jq7d6sT/Ev4H65RER6pZ+kczlf1DthF5N0qaPHBsI7UXiE8Zy66nmAovg== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/helper-replace-supers" "^7.24.7" - -"@babel/plugin-transform-optional-catch-binding@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.24.7.tgz#00eabd883d0dd6a60c1c557548785919b6e717b4" - integrity sha512-uLEndKqP5BfBbC/5jTwPxLh9kqPWWgzN/f8w6UwAIirAEqiIVJWWY312X72Eub09g5KF9+Zn7+hT7sDxmhRuKA== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" - -"@babel/plugin-transform-optional-chaining@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.7.tgz#b8f6848a80cf2da98a8a204429bec04756c6d454" - integrity sha512-tK+0N9yd4j+x/4hxF3F0e0fu/VdcxU18y5SevtyM/PCFlQvXbR0Zmlo2eBrKtVipGNFzpq56o8WsIIKcJFUCRQ== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" - "@babel/plugin-syntax-optional-chaining" "^7.8.3" - -"@babel/plugin-transform-parameters@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.24.7.tgz#5881f0ae21018400e320fc7eb817e529d1254b68" - integrity sha512-yGWW5Rr+sQOhK0Ot8hjDJuxU3XLRQGflvT4lhlSY0DFvdb3TwKaY26CJzHtYllU0vT9j58hc37ndFPsqT1SrzA== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-transform-private-methods@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.24.7.tgz#e6318746b2ae70a59d023d5cc1344a2ba7a75f5e" - integrity sha512-COTCOkG2hn4JKGEKBADkA8WNb35TGkkRbI5iT845dB+NyqgO8Hn+ajPbSnIQznneJTa3d30scb6iz/DhH8GsJQ== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-transform-private-property-in-object@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.24.7.tgz#4eec6bc701288c1fab5f72e6a4bbc9d67faca061" - integrity sha512-9z76mxwnwFxMyxZWEgdgECQglF2Q7cFLm0kMf8pGwt+GSJsY0cONKj/UuO4bOH0w/uAel3ekS4ra5CEAyJRmDA== - dependencies: - "@babel/helper-annotate-as-pure" "^7.24.7" - "@babel/helper-create-class-features-plugin" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/plugin-syntax-private-property-in-object" "^7.14.5" - -"@babel/plugin-transform-property-literals@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.24.7.tgz#f0d2ed8380dfbed949c42d4d790266525d63bbdc" - integrity sha512-EMi4MLQSHfd2nrCqQEWxFdha2gBCqU4ZcCng4WBGZ5CJL4bBRW0ptdqqDdeirGZcpALazVVNJqRmsO8/+oNCBA== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-transform-react-constant-elements@^7.21.3": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.24.7.tgz#b85e8f240b14400277f106c9c9b585d9acf608a1" - integrity sha512-7LidzZfUXyfZ8/buRW6qIIHBY8wAZ1OrY9c/wTr8YhZ6vMPo+Uc/CVFLYY1spZrEQlD4w5u8wjqk5NQ3OVqQKA== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-transform-react-display-name@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.24.7.tgz#9caff79836803bc666bcfe210aeb6626230c293b" - integrity sha512-H/Snz9PFxKsS1JLI4dJLtnJgCJRoo0AUm3chP6NYr+9En1JMKloheEiLIhlp5MDVznWo+H3AAC1Mc8lmUEpsgg== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-transform-react-jsx-development@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.24.7.tgz#eaee12f15a93f6496d852509a850085e6361470b" - integrity sha512-QG9EnzoGn+Qar7rxuW+ZOsbWOt56FvvI93xInqsZDC5fsekx1AlIO4KIJ5M+D0p0SqSH156EpmZyXq630B8OlQ== - dependencies: - "@babel/plugin-transform-react-jsx" "^7.24.7" - -"@babel/plugin-transform-react-jsx@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.24.7.tgz#17cd06b75a9f0e2bd076503400e7c4b99beedac4" - integrity sha512-+Dj06GDZEFRYvclU6k4bme55GKBEWUmByM/eoKuqg4zTNQHiApWRhQph5fxQB2wAEFvRzL1tOEj1RJ19wJrhoA== - dependencies: - "@babel/helper-annotate-as-pure" "^7.24.7" - "@babel/helper-module-imports" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/plugin-syntax-jsx" "^7.24.7" - "@babel/types" "^7.24.7" - -"@babel/plugin-transform-react-pure-annotations@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.24.7.tgz#bdd9d140d1c318b4f28b29a00fb94f97ecab1595" - integrity sha512-PLgBVk3fzbmEjBJ/u8kFzOqS9tUeDjiaWud/rRym/yjCo/M9cASPlnrd2ZmmZpQT40fOOrvR8jh+n8jikrOhNA== - dependencies: - "@babel/helper-annotate-as-pure" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-transform-regenerator@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.24.7.tgz#021562de4534d8b4b1851759fd7af4e05d2c47f8" - integrity sha512-lq3fvXPdimDrlg6LWBoqj+r/DEWgONuwjuOuQCSYgRroXDH/IdM1C0IZf59fL5cHLpjEH/O6opIRBbqv7ELnuA== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - regenerator-transform "^0.15.2" - -"@babel/plugin-transform-reserved-words@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.24.7.tgz#80037fe4fbf031fc1125022178ff3938bb3743a4" - integrity sha512-0DUq0pHcPKbjFZCfTss/pGkYMfy3vFWydkUBd9r0GHpIyfs2eCDENvqadMycRS9wZCXR41wucAfJHJmwA0UmoQ== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-transform-runtime@^7.22.9": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.24.7.tgz#00a5bfaf8c43cf5c8703a8a6e82b59d9c58f38ca" - integrity sha512-YqXjrk4C+a1kZjewqt+Mmu2UuV1s07y8kqcUf4qYLnoqemhR4gRQikhdAhSVJioMjVTu6Mo6pAbaypEA3jY6fw== - dependencies: - "@babel/helper-module-imports" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" - babel-plugin-polyfill-corejs2 "^0.4.10" - babel-plugin-polyfill-corejs3 "^0.10.1" - babel-plugin-polyfill-regenerator "^0.6.1" - semver "^6.3.1" - -"@babel/plugin-transform-shorthand-properties@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.24.7.tgz#85448c6b996e122fa9e289746140aaa99da64e73" - integrity sha512-KsDsevZMDsigzbA09+vacnLpmPH4aWjcZjXdyFKGzpplxhbeB4wYtury3vglQkg6KM/xEPKt73eCjPPf1PgXBA== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-transform-spread@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.24.7.tgz#e8a38c0fde7882e0fb8f160378f74bd885cc7bb3" - integrity sha512-x96oO0I09dgMDxJaANcRyD4ellXFLLiWhuwDxKZX5g2rWP1bTPkBSwCYv96VDXVT1bD9aPj8tppr5ITIh8hBng== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" - -"@babel/plugin-transform-sticky-regex@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.24.7.tgz#96ae80d7a7e5251f657b5cf18f1ea6bf926f5feb" - integrity sha512-kHPSIJc9v24zEml5geKg9Mjx5ULpfncj0wRpYtxbvKyTtHCYDkVE3aHQ03FrpEo4gEe2vrJJS1Y9CJTaThA52g== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-transform-template-literals@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.24.7.tgz#a05debb4a9072ae8f985bcf77f3f215434c8f8c8" - integrity sha512-AfDTQmClklHCOLxtGoP7HkeMw56k1/bTQjwsfhL6pppo/M4TOBSq+jjBUBLmV/4oeFg4GWMavIl44ZeCtmmZTw== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-transform-typeof-symbol@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.7.tgz#f074be466580d47d6e6b27473a840c9f9ca08fb0" - integrity sha512-VtR8hDy7YLB7+Pet9IarXjg/zgCMSF+1mNS/EQEiEaUPoFXCVsHG64SIxcaaI2zJgRiv+YmgaQESUfWAdbjzgg== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-transform-typescript@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.24.7.tgz#b006b3e0094bf0813d505e0c5485679eeaf4a881" - integrity sha512-iLD3UNkgx2n/HrjBesVbYX6j0yqn/sJktvbtKKgcaLIQ4bTTQ8obAypc1VpyHPD2y4Phh9zHOaAt8e/L14wCpw== - dependencies: - "@babel/helper-annotate-as-pure" "^7.24.7" - "@babel/helper-create-class-features-plugin" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/plugin-syntax-typescript" "^7.24.7" - -"@babel/plugin-transform-unicode-escapes@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.7.tgz#2023a82ced1fb4971630a2e079764502c4148e0e" - integrity sha512-U3ap1gm5+4edc2Q/P+9VrBNhGkfnf+8ZqppY71Bo/pzZmXhhLdqgaUl6cuB07O1+AQJtCLfaOmswiNbSQ9ivhw== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-transform-unicode-property-regex@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.24.7.tgz#9073a4cd13b86ea71c3264659590ac086605bbcd" - integrity sha512-uH2O4OV5M9FZYQrwc7NdVmMxQJOCCzFeYudlZSzUAHRFeOujQefa92E74TQDVskNHCzOXoigEuoyzHDhaEaK5w== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-transform-unicode-regex@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.24.7.tgz#dfc3d4a51127108099b19817c0963be6a2adf19f" - integrity sha512-hlQ96MBZSAXUq7ltkjtu3FJCCSMx/j629ns3hA3pXnBXjanNP0LHi+JpPeA81zaWgVK1VGH95Xuy7u0RyQ8kMg== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-transform-unicode-sets-regex@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.24.7.tgz#d40705d67523803a576e29c63cef6e516b858ed9" - integrity sha512-2G8aAvF4wy1w/AGZkemprdGMRg5o6zPNhbHVImRz3lss55TYCBd6xStN19rt8XJHq20sqV0JbyWjOWwQRwV/wg== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/preset-env@^7.20.2", "@babel/preset-env@^7.22.9": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.24.7.tgz#ff067b4e30ba4a72f225f12f123173e77b987f37" - integrity sha512-1YZNsc+y6cTvWlDHidMBsQZrZfEFjRIo/BZCT906PMdzOyXtSLTgqGdrpcuTDCXyd11Am5uQULtDIcCfnTc8fQ== - dependencies: - "@babel/compat-data" "^7.24.7" - "@babel/helper-compilation-targets" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/helper-validator-option" "^7.24.7" - "@babel/plugin-bugfix-firefox-class-in-computed-class-key" "^7.24.7" - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.24.7" - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.24.7" - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly" "^7.24.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.24.7" - "@babel/plugin-syntax-import-attributes" "^7.24.7" - "@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.24.7" - "@babel/plugin-transform-async-generator-functions" "^7.24.7" - "@babel/plugin-transform-async-to-generator" "^7.24.7" - "@babel/plugin-transform-block-scoped-functions" "^7.24.7" - "@babel/plugin-transform-block-scoping" "^7.24.7" - "@babel/plugin-transform-class-properties" "^7.24.7" - "@babel/plugin-transform-class-static-block" "^7.24.7" - "@babel/plugin-transform-classes" "^7.24.7" - "@babel/plugin-transform-computed-properties" "^7.24.7" - "@babel/plugin-transform-destructuring" "^7.24.7" - "@babel/plugin-transform-dotall-regex" "^7.24.7" - "@babel/plugin-transform-duplicate-keys" "^7.24.7" - "@babel/plugin-transform-dynamic-import" "^7.24.7" - "@babel/plugin-transform-exponentiation-operator" "^7.24.7" - "@babel/plugin-transform-export-namespace-from" "^7.24.7" - "@babel/plugin-transform-for-of" "^7.24.7" - "@babel/plugin-transform-function-name" "^7.24.7" - "@babel/plugin-transform-json-strings" "^7.24.7" - "@babel/plugin-transform-literals" "^7.24.7" - "@babel/plugin-transform-logical-assignment-operators" "^7.24.7" - "@babel/plugin-transform-member-expression-literals" "^7.24.7" - "@babel/plugin-transform-modules-amd" "^7.24.7" - "@babel/plugin-transform-modules-commonjs" "^7.24.7" - "@babel/plugin-transform-modules-systemjs" "^7.24.7" - "@babel/plugin-transform-modules-umd" "^7.24.7" - "@babel/plugin-transform-named-capturing-groups-regex" "^7.24.7" - "@babel/plugin-transform-new-target" "^7.24.7" - "@babel/plugin-transform-nullish-coalescing-operator" "^7.24.7" - "@babel/plugin-transform-numeric-separator" "^7.24.7" - "@babel/plugin-transform-object-rest-spread" "^7.24.7" - "@babel/plugin-transform-object-super" "^7.24.7" - "@babel/plugin-transform-optional-catch-binding" "^7.24.7" - "@babel/plugin-transform-optional-chaining" "^7.24.7" - "@babel/plugin-transform-parameters" "^7.24.7" - "@babel/plugin-transform-private-methods" "^7.24.7" - "@babel/plugin-transform-private-property-in-object" "^7.24.7" - "@babel/plugin-transform-property-literals" "^7.24.7" - "@babel/plugin-transform-regenerator" "^7.24.7" - "@babel/plugin-transform-reserved-words" "^7.24.7" - "@babel/plugin-transform-shorthand-properties" "^7.24.7" - "@babel/plugin-transform-spread" "^7.24.7" - "@babel/plugin-transform-sticky-regex" "^7.24.7" - "@babel/plugin-transform-template-literals" "^7.24.7" - "@babel/plugin-transform-typeof-symbol" "^7.24.7" - "@babel/plugin-transform-unicode-escapes" "^7.24.7" - "@babel/plugin-transform-unicode-property-regex" "^7.24.7" - "@babel/plugin-transform-unicode-regex" "^7.24.7" - "@babel/plugin-transform-unicode-sets-regex" "^7.24.7" - "@babel/preset-modules" "0.1.6-no-external-plugins" - babel-plugin-polyfill-corejs2 "^0.4.10" - babel-plugin-polyfill-corejs3 "^0.10.4" - babel-plugin-polyfill-regenerator "^0.6.1" - core-js-compat "^3.31.0" - semver "^6.3.1" - -"@babel/preset-modules@0.1.6-no-external-plugins": - version "0.1.6-no-external-plugins" - resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz#ccb88a2c49c817236861fee7826080573b8a923a" - integrity sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA== - dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - "@babel/types" "^7.4.4" - esutils "^2.0.2" - -"@babel/preset-react@^7.18.6", "@babel/preset-react@^7.22.5": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.24.7.tgz#480aeb389b2a798880bf1f889199e3641cbb22dc" - integrity sha512-AAH4lEkpmzFWrGVlHaxJB7RLH21uPQ9+He+eFLWHmF9IuFQVugz8eAsamaW0DXRrTfco5zj1wWtpdcXJUOfsag== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/helper-validator-option" "^7.24.7" - "@babel/plugin-transform-react-display-name" "^7.24.7" - "@babel/plugin-transform-react-jsx" "^7.24.7" - "@babel/plugin-transform-react-jsx-development" "^7.24.7" - "@babel/plugin-transform-react-pure-annotations" "^7.24.7" - -"@babel/preset-typescript@^7.21.0", "@babel/preset-typescript@^7.22.5": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.24.7.tgz#66cd86ea8f8c014855671d5ea9a737139cbbfef1" - integrity sha512-SyXRe3OdWwIwalxDg5UtJnJQO+YPcTfwiIY2B0Xlddh9o7jpWLvv8X1RthIeDOxQ+O1ML5BLPCONToObyVQVuQ== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/helper-validator-option" "^7.24.7" - "@babel/plugin-syntax-jsx" "^7.24.7" - "@babel/plugin-transform-modules-commonjs" "^7.24.7" - "@babel/plugin-transform-typescript" "^7.24.7" - -"@babel/regjsgen@^0.8.0": - version "0.8.0" - resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310" - integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== - -"@babel/runtime-corejs3@^7.22.6": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.24.7.tgz#65a99097e4c28e6c3a174825591700cc5abd710e" - integrity sha512-eytSX6JLBY6PVAeQa2bFlDx/7Mmln/gaEpsit5a3WEvjGfiIytEsgAwuIXCPM0xvw0v0cJn3ilq0/TvXrW0kgA== - dependencies: - core-js-pure "^3.30.2" - regenerator-runtime "^0.14.0" - -"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.22.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": - version "7.27.0" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.27.0.tgz#fbee7cf97c709518ecc1f590984481d5460d4762" - integrity sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw== - dependencies: - regenerator-runtime "^0.14.0" - -"@babel/template@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.24.7.tgz#02efcee317d0609d2c07117cb70ef8fb17ab7315" - integrity sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig== - dependencies: - "@babel/code-frame" "^7.24.7" - "@babel/parser" "^7.24.7" - "@babel/types" "^7.24.7" - -"@babel/template@^7.27.0": - version "7.27.0" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.27.0.tgz#b253e5406cc1df1c57dcd18f11760c2dbf40c0b4" - integrity sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA== - dependencies: - "@babel/code-frame" "^7.26.2" - "@babel/parser" "^7.27.0" - "@babel/types" "^7.27.0" - -"@babel/traverse@^7.22.8", "@babel/traverse@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.24.7.tgz#de2b900163fa741721ba382163fe46a936c40cf5" - integrity sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA== - dependencies: - "@babel/code-frame" "^7.24.7" - "@babel/generator" "^7.24.7" - "@babel/helper-environment-visitor" "^7.24.7" - "@babel/helper-function-name" "^7.24.7" - "@babel/helper-hoist-variables" "^7.24.7" - "@babel/helper-split-export-declaration" "^7.24.7" - "@babel/parser" "^7.24.7" - "@babel/types" "^7.24.7" - debug "^4.3.1" - globals "^11.1.0" - -"@babel/types@^7.21.3", "@babel/types@^7.24.7", "@babel/types@^7.4.4": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.24.7.tgz#6027fe12bc1aa724cd32ab113fb7f1988f1f66f2" - integrity sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q== - dependencies: - "@babel/helper-string-parser" "^7.24.7" - "@babel/helper-validator-identifier" "^7.24.7" - to-fast-properties "^2.0.0" - -"@babel/types@^7.27.0": - version "7.27.0" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.27.0.tgz#ef9acb6b06c3173f6632d993ecb6d4ae470b4559" - integrity sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg== - dependencies: - "@babel/helper-string-parser" "^7.25.9" - "@babel/helper-validator-identifier" "^7.25.9" - -"@braintree/sanitize-url@^6.0.1": - version "6.0.4" - resolved "https://registry.yarnpkg.com/@braintree/sanitize-url/-/sanitize-url-6.0.4.tgz#923ca57e173c6b232bbbb07347b1be982f03e783" - integrity sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A== - -"@bufbuild/buf-darwin-arm64@1.33.0": - version "1.33.0" - resolved "https://registry.yarnpkg.com/@bufbuild/buf-darwin-arm64/-/buf-darwin-arm64-1.33.0.tgz#9d68f4cbbe1fcf43e4ca8e791a6977836310e4a4" - integrity sha512-h2CKZUS3apPDxuB/HQHIvbzh+xXVsRik7w/AbeYO1r9PKjdet/8F53t3KrdQa4NFF948JGxQ/0/1VvVowjjZUA== - -"@bufbuild/buf-darwin-x64@1.33.0": - version "1.33.0" - resolved "https://registry.yarnpkg.com/@bufbuild/buf-darwin-x64/-/buf-darwin-x64-1.33.0.tgz#842495c18553c897346adb40abe49b47cd5405ad" - integrity sha512-DMBkAJVwwRHF0gwiCWyqToOVfcNAU8WYOkE3M2NYvQDIdeiSKw0+OjD0l+CYfppMOGh9MJWQV7di96ugDO+VbA== - -"@bufbuild/buf-linux-aarch64@1.33.0": - version "1.33.0" - resolved "https://registry.yarnpkg.com/@bufbuild/buf-linux-aarch64/-/buf-linux-aarch64-1.33.0.tgz#7516dbc6467cc64988b6b1191e1dcbdef9506ec0" - integrity sha512-68OxC000rHtXzV2zxHXJ54ZkkPy/3yrm9HLALo8vUk4p5/fupwzjkGMQqD6BmL/ThDO8AX3jUvhJUAg+kS1+Hw== - -"@bufbuild/buf-linux-x64@1.33.0": - version "1.33.0" - resolved "https://registry.yarnpkg.com/@bufbuild/buf-linux-x64/-/buf-linux-x64-1.33.0.tgz#413b20c6222c662033f35c421ff6ad7f9524ee03" - integrity sha512-KsED6sanzwZCviSCMlq/P2yuuTVbgWGeUmS9CGsVn28sdjFCJb7bNhB6CKpUhox617XwqTPYZzEulQ1fF/sqJg== - -"@bufbuild/buf-win32-arm64@1.33.0": - version "1.33.0" - resolved "https://registry.yarnpkg.com/@bufbuild/buf-win32-arm64/-/buf-win32-arm64-1.33.0.tgz#5e73ed4bf652047018fc63af692fe42328af568b" - integrity sha512-QEGjUNEDI2zfajtHlbjZsM05ya9dtza5X3q95IeUJmy+7NOhV9OOLqtWnHv2iCh1zvyzefKSXWfmQhNs+M/0qA== - -"@bufbuild/buf-win32-x64@1.33.0": - version "1.33.0" - resolved "https://registry.yarnpkg.com/@bufbuild/buf-win32-x64/-/buf-win32-x64-1.33.0.tgz#167f7e600c1384b885bc003cd5df4f1eb3b1fbd0" - integrity sha512-JdPxQc7m5L6BsMzrqecKGmgJSYn3k3389dFtEXIEB2D7u2G3AWoRbPluzggo6udBYtjq+ZMdErdPjDDknNQvEg== - -"@bufbuild/buf@^1.14.0": - version "1.33.0" - resolved "https://registry.yarnpkg.com/@bufbuild/buf/-/buf-1.33.0.tgz#0f9e3f8c8674707981312c02295a0838066972e4" - integrity sha512-VnXdRDsfr1aO7gqy/geVK4/r9w4K6lj3ypsZwu75gdXMq9QXtOmdEO6QqlXO/BpjYG4Aw/Z6Bt/WEezA6hqJow== - optionalDependencies: - "@bufbuild/buf-darwin-arm64" "1.33.0" - "@bufbuild/buf-darwin-x64" "1.33.0" - "@bufbuild/buf-linux-aarch64" "1.33.0" - "@bufbuild/buf-linux-x64" "1.33.0" - "@bufbuild/buf-win32-arm64" "1.33.0" - "@bufbuild/buf-win32-x64" "1.33.0" - -"@colors/colors@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" - integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== - -"@discoveryjs/json-ext@0.5.7": - version "0.5.7" - resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" - integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== - -"@docsearch/css@3.6.0": - version "3.6.0" - resolved "https://registry.yarnpkg.com/@docsearch/css/-/css-3.6.0.tgz#0e9f56f704b3a34d044d15fd9962ebc1536ba4fb" - integrity sha512-+sbxb71sWre+PwDK7X2T8+bhS6clcVMLwBPznX45Qu6opJcgRjAp7gYSDzVFp187J+feSj5dNBN1mJoi6ckkUQ== - -"@docsearch/react@^3.5.2": - version "3.6.0" - resolved "https://registry.yarnpkg.com/@docsearch/react/-/react-3.6.0.tgz#b4f25228ecb7fc473741aefac592121e86dd2958" - integrity sha512-HUFut4ztcVNmqy9gp/wxNbC7pTOHhgVVkHVGCACTuLhUKUhKAF9KYHJtMiLUJxEqiFLQiuri1fWF8zqwM/cu1w== - dependencies: - "@algolia/autocomplete-core" "1.9.3" - "@algolia/autocomplete-preset-algolia" "1.9.3" - "@docsearch/css" "3.6.0" - algoliasearch "^4.19.1" - -"@docusaurus/core@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/core/-/core-3.4.0.tgz#bdbf1af4b2f25d1bf4a5b62ec6137d84c821cb3c" - integrity sha512-g+0wwmN2UJsBqy2fQRQ6fhXruoEa62JDeEa5d8IdTJlMoaDaEDfHh7WjwGRn4opuTQWpjAwP/fbcgyHKlE+64w== - dependencies: - "@babel/core" "^7.23.3" - "@babel/generator" "^7.23.3" - "@babel/plugin-syntax-dynamic-import" "^7.8.3" - "@babel/plugin-transform-runtime" "^7.22.9" - "@babel/preset-env" "^7.22.9" - "@babel/preset-react" "^7.22.5" - "@babel/preset-typescript" "^7.22.5" - "@babel/runtime" "^7.22.6" - "@babel/runtime-corejs3" "^7.22.6" - "@babel/traverse" "^7.22.8" - "@docusaurus/cssnano-preset" "3.4.0" - "@docusaurus/logger" "3.4.0" - "@docusaurus/mdx-loader" "3.4.0" - "@docusaurus/utils" "3.4.0" - "@docusaurus/utils-common" "3.4.0" - "@docusaurus/utils-validation" "3.4.0" - autoprefixer "^10.4.14" - babel-loader "^9.1.3" - babel-plugin-dynamic-import-node "^2.3.3" - boxen "^6.2.1" - chalk "^4.1.2" - chokidar "^3.5.3" - clean-css "^5.3.2" - cli-table3 "^0.6.3" - combine-promises "^1.1.0" - commander "^5.1.0" - copy-webpack-plugin "^11.0.0" - core-js "^3.31.1" - css-loader "^6.8.1" - css-minimizer-webpack-plugin "^5.0.1" - cssnano "^6.1.2" - del "^6.1.1" - detect-port "^1.5.1" - escape-html "^1.0.3" - eta "^2.2.0" - eval "^0.1.8" - file-loader "^6.2.0" - fs-extra "^11.1.1" - html-minifier-terser "^7.2.0" - html-tags "^3.3.1" - html-webpack-plugin "^5.5.3" - leven "^3.1.0" - lodash "^4.17.21" - mini-css-extract-plugin "^2.7.6" - p-map "^4.0.0" - postcss "^8.4.26" - postcss-loader "^7.3.3" - prompts "^2.4.2" - react-dev-utils "^12.0.1" - react-helmet-async "^1.3.0" - react-loadable "npm:@docusaurus/react-loadable@6.0.0" - react-loadable-ssr-addon-v5-slorber "^1.0.1" - react-router "^5.3.4" - react-router-config "^5.1.1" - react-router-dom "^5.3.4" - rtl-detect "^1.0.4" - semver "^7.5.4" - serve-handler "^6.1.5" - shelljs "^0.8.5" - terser-webpack-plugin "^5.3.9" - tslib "^2.6.0" - update-notifier "^6.0.2" - url-loader "^4.1.1" - webpack "^5.88.1" - webpack-bundle-analyzer "^4.9.0" - webpack-dev-server "^4.15.1" - webpack-merge "^5.9.0" - webpackbar "^5.0.2" - -"@docusaurus/cssnano-preset@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/cssnano-preset/-/cssnano-preset-3.4.0.tgz#dc7922b3bbeabcefc9b60d0161680d81cf72c368" - integrity sha512-qwLFSz6v/pZHy/UP32IrprmH5ORce86BGtN0eBtG75PpzQJAzp9gefspox+s8IEOr0oZKuQ/nhzZ3xwyc3jYJQ== - dependencies: - cssnano-preset-advanced "^6.1.2" - postcss "^8.4.38" - postcss-sort-media-queries "^5.2.0" - tslib "^2.6.0" - -"@docusaurus/logger@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/logger/-/logger-3.4.0.tgz#8b0ac05c7f3dac2009066e2f964dee8209a77403" - integrity sha512-bZwkX+9SJ8lB9kVRkXw+xvHYSMGG4bpYHKGXeXFvyVc79NMeeBSGgzd4TQLHH+DYeOJoCdl8flrFJVxlZ0wo/Q== - dependencies: - chalk "^4.1.2" - tslib "^2.6.0" - -"@docusaurus/mdx-loader@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/mdx-loader/-/mdx-loader-3.4.0.tgz#483d7ab57928fdbb5c8bd1678098721a930fc5f6" - integrity sha512-kSSbrrk4nTjf4d+wtBA9H+FGauf2gCax89kV8SUSJu3qaTdSIKdWERlngsiHaCFgZ7laTJ8a67UFf+xlFPtuTw== - dependencies: - "@docusaurus/logger" "3.4.0" - "@docusaurus/utils" "3.4.0" - "@docusaurus/utils-validation" "3.4.0" - "@mdx-js/mdx" "^3.0.0" - "@slorber/remark-comment" "^1.0.0" - escape-html "^1.0.3" - estree-util-value-to-estree "^3.0.1" - file-loader "^6.2.0" - fs-extra "^11.1.1" - image-size "^1.0.2" - mdast-util-mdx "^3.0.0" - mdast-util-to-string "^4.0.0" - rehype-raw "^7.0.0" - remark-directive "^3.0.0" - remark-emoji "^4.0.0" - remark-frontmatter "^5.0.0" - remark-gfm "^4.0.0" - stringify-object "^3.3.0" - tslib "^2.6.0" - unified "^11.0.3" - unist-util-visit "^5.0.0" - url-loader "^4.1.1" - vfile "^6.0.1" - webpack "^5.88.1" - -"@docusaurus/module-type-aliases@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/module-type-aliases/-/module-type-aliases-3.4.0.tgz#2653bde58fc1aa3dbc626a6c08cfb63a37ae1bb8" - integrity sha512-A1AyS8WF5Bkjnb8s+guTDuYmUiwJzNrtchebBHpc0gz0PyHJNMaybUlSrmJjHVcGrya0LKI4YcR3lBDQfXRYLw== - dependencies: - "@docusaurus/types" "3.4.0" - "@types/history" "^4.7.11" - "@types/react" "*" - "@types/react-router-config" "*" - "@types/react-router-dom" "*" - react-helmet-async "*" - react-loadable "npm:@docusaurus/react-loadable@6.0.0" - -"@docusaurus/plugin-content-blog@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.4.0.tgz#6373632fdbababbda73a13c4a08f907d7de8f007" - integrity sha512-vv6ZAj78ibR5Jh7XBUT4ndIjmlAxkijM3Sx5MAAzC1gyv0vupDQNhzuFg1USQmQVj3P5I6bquk12etPV3LJ+Xw== - dependencies: - "@docusaurus/core" "3.4.0" - "@docusaurus/logger" "3.4.0" - "@docusaurus/mdx-loader" "3.4.0" - "@docusaurus/types" "3.4.0" - "@docusaurus/utils" "3.4.0" - "@docusaurus/utils-common" "3.4.0" - "@docusaurus/utils-validation" "3.4.0" - cheerio "^1.0.0-rc.12" - feed "^4.2.2" - fs-extra "^11.1.1" - lodash "^4.17.21" - reading-time "^1.5.0" - srcset "^4.0.0" - tslib "^2.6.0" - unist-util-visit "^5.0.0" - utility-types "^3.10.0" - webpack "^5.88.1" - -"@docusaurus/plugin-content-docs@3.4.0", "@docusaurus/plugin-content-docs@^3.0.1": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.4.0.tgz#3088973f72169a2a6d533afccec7153c8720d332" - integrity sha512-HkUCZffhBo7ocYheD9oZvMcDloRnGhBMOZRyVcAQRFmZPmNqSyISlXA1tQCIxW+r478fty97XXAGjNYzBjpCsg== - dependencies: - "@docusaurus/core" "3.4.0" - "@docusaurus/logger" "3.4.0" - "@docusaurus/mdx-loader" "3.4.0" - "@docusaurus/module-type-aliases" "3.4.0" - "@docusaurus/types" "3.4.0" - "@docusaurus/utils" "3.4.0" - "@docusaurus/utils-common" "3.4.0" - "@docusaurus/utils-validation" "3.4.0" - "@types/react-router-config" "^5.0.7" - combine-promises "^1.1.0" - fs-extra "^11.1.1" - js-yaml "^4.1.0" - lodash "^4.17.21" - tslib "^2.6.0" - utility-types "^3.10.0" - webpack "^5.88.1" - -"@docusaurus/plugin-content-pages@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.4.0.tgz#1846172ca0355c7d32a67ef8377750ce02bbb8ad" - integrity sha512-h2+VN/0JjpR8fIkDEAoadNjfR3oLzB+v1qSXbIAKjQ46JAHx3X22n9nqS+BWSQnTnp1AjkjSvZyJMekmcwxzxg== - dependencies: - "@docusaurus/core" "3.4.0" - "@docusaurus/mdx-loader" "3.4.0" - "@docusaurus/types" "3.4.0" - "@docusaurus/utils" "3.4.0" - "@docusaurus/utils-validation" "3.4.0" - fs-extra "^11.1.1" - tslib "^2.6.0" - webpack "^5.88.1" - -"@docusaurus/plugin-debug@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-debug/-/plugin-debug-3.4.0.tgz#74e4ec5686fa314c26f3ac150bacadbba7f06948" - integrity sha512-uV7FDUNXGyDSD3PwUaf5YijX91T5/H9SX4ErEcshzwgzWwBtK37nUWPU3ZLJfeTavX3fycTOqk9TglpOLaWkCg== - dependencies: - "@docusaurus/core" "3.4.0" - "@docusaurus/types" "3.4.0" - "@docusaurus/utils" "3.4.0" - fs-extra "^11.1.1" - react-json-view-lite "^1.2.0" - tslib "^2.6.0" - -"@docusaurus/plugin-google-analytics@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-3.4.0.tgz#5f59fc25329a59decc231936f6f9fb5663da3c55" - integrity sha512-mCArluxEGi3cmYHqsgpGGt3IyLCrFBxPsxNZ56Mpur0xSlInnIHoeLDH7FvVVcPJRPSQ9/MfRqLsainRw+BojA== - dependencies: - "@docusaurus/core" "3.4.0" - "@docusaurus/types" "3.4.0" - "@docusaurus/utils-validation" "3.4.0" - tslib "^2.6.0" - -"@docusaurus/plugin-google-gtag@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-3.4.0.tgz#42489ac5fe1c83b5523ceedd5ef74f9aa8bc251b" - integrity sha512-Dsgg6PLAqzZw5wZ4QjUYc8Z2KqJqXxHxq3vIoyoBWiLEEfigIs7wHR+oiWUQy3Zk9MIk6JTYj7tMoQU0Jm3nqA== - dependencies: - "@docusaurus/core" "3.4.0" - "@docusaurus/types" "3.4.0" - "@docusaurus/utils-validation" "3.4.0" - "@types/gtag.js" "^0.0.12" - tslib "^2.6.0" - -"@docusaurus/plugin-google-tag-manager@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-3.4.0.tgz#cebb03a5ffa1e70b37d95601442babea251329ff" - integrity sha512-O9tX1BTwxIhgXpOLpFDueYA9DWk69WCbDRrjYoMQtFHSkTyE7RhNgyjSPREUWJb9i+YUg3OrsvrBYRl64FCPCQ== - dependencies: - "@docusaurus/core" "3.4.0" - "@docusaurus/types" "3.4.0" - "@docusaurus/utils-validation" "3.4.0" - tslib "^2.6.0" - -"@docusaurus/plugin-sitemap@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-sitemap/-/plugin-sitemap-3.4.0.tgz#b091d64d1e3c6c872050189999580187537bcbc6" - integrity sha512-+0VDvx9SmNrFNgwPoeoCha+tRoAjopwT0+pYO1xAbyLcewXSemq+eLxEa46Q1/aoOaJQ0qqHELuQM7iS2gp33Q== - dependencies: - "@docusaurus/core" "3.4.0" - "@docusaurus/logger" "3.4.0" - "@docusaurus/types" "3.4.0" - "@docusaurus/utils" "3.4.0" - "@docusaurus/utils-common" "3.4.0" - "@docusaurus/utils-validation" "3.4.0" - fs-extra "^11.1.1" - sitemap "^7.1.1" - tslib "^2.6.0" - -"@docusaurus/preset-classic@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/preset-classic/-/preset-classic-3.4.0.tgz#6082a32fbb465b0cb2c2a50ebfc277cff2c0f139" - integrity sha512-Ohj6KB7siKqZaQhNJVMBBUzT3Nnp6eTKqO+FXO3qu/n1hJl3YLwVKTWBg28LF7MWrKu46UuYavwMRxud0VyqHg== - dependencies: - "@docusaurus/core" "3.4.0" - "@docusaurus/plugin-content-blog" "3.4.0" - "@docusaurus/plugin-content-docs" "3.4.0" - "@docusaurus/plugin-content-pages" "3.4.0" - "@docusaurus/plugin-debug" "3.4.0" - "@docusaurus/plugin-google-analytics" "3.4.0" - "@docusaurus/plugin-google-gtag" "3.4.0" - "@docusaurus/plugin-google-tag-manager" "3.4.0" - "@docusaurus/plugin-sitemap" "3.4.0" - "@docusaurus/theme-classic" "3.4.0" - "@docusaurus/theme-common" "3.4.0" - "@docusaurus/theme-search-algolia" "3.4.0" - "@docusaurus/types" "3.4.0" - -"@docusaurus/theme-classic@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/theme-classic/-/theme-classic-3.4.0.tgz#1b0f48edec3e3ec8927843554b9f11e5927b0e52" - integrity sha512-0IPtmxsBYv2adr1GnZRdMkEQt1YW6tpzrUPj02YxNpvJ5+ju4E13J5tB4nfdaen/tfR1hmpSPlTFPvTf4kwy8Q== - dependencies: - "@docusaurus/core" "3.4.0" - "@docusaurus/mdx-loader" "3.4.0" - "@docusaurus/module-type-aliases" "3.4.0" - "@docusaurus/plugin-content-blog" "3.4.0" - "@docusaurus/plugin-content-docs" "3.4.0" - "@docusaurus/plugin-content-pages" "3.4.0" - "@docusaurus/theme-common" "3.4.0" - "@docusaurus/theme-translations" "3.4.0" - "@docusaurus/types" "3.4.0" - "@docusaurus/utils" "3.4.0" - "@docusaurus/utils-common" "3.4.0" - "@docusaurus/utils-validation" "3.4.0" - "@mdx-js/react" "^3.0.0" - clsx "^2.0.0" - copy-text-to-clipboard "^3.2.0" - infima "0.2.0-alpha.43" - lodash "^4.17.21" - nprogress "^0.2.0" - postcss "^8.4.26" - prism-react-renderer "^2.3.0" - prismjs "^1.29.0" - react-router-dom "^5.3.4" - rtlcss "^4.1.0" - tslib "^2.6.0" - utility-types "^3.10.0" - -"@docusaurus/theme-common@3.4.0", "@docusaurus/theme-common@^3.0.1": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/theme-common/-/theme-common-3.4.0.tgz#01f2b728de6cb57f6443f52fc30675cf12a5d49f" - integrity sha512-0A27alXuv7ZdCg28oPE8nH/Iz73/IUejVaCazqu9elS4ypjiLhK3KfzdSQBnL/g7YfHSlymZKdiOHEo8fJ0qMA== - dependencies: - "@docusaurus/mdx-loader" "3.4.0" - "@docusaurus/module-type-aliases" "3.4.0" - "@docusaurus/plugin-content-blog" "3.4.0" - "@docusaurus/plugin-content-docs" "3.4.0" - "@docusaurus/plugin-content-pages" "3.4.0" - "@docusaurus/utils" "3.4.0" - "@docusaurus/utils-common" "3.4.0" - "@types/history" "^4.7.11" - "@types/react" "*" - "@types/react-router-config" "*" - clsx "^2.0.0" - parse-numeric-range "^1.3.0" - prism-react-renderer "^2.3.0" - tslib "^2.6.0" - utility-types "^3.10.0" - -"@docusaurus/theme-mermaid@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/theme-mermaid/-/theme-mermaid-3.4.0.tgz#ef1d2231d0858767f67538b4fafd7d0ce2a3e845" - integrity sha512-3w5QW0HEZ2O6x2w6lU3ZvOe1gNXP2HIoKDMJBil1VmLBc9PmpAG17VmfhI/p3L2etNmOiVs5GgniUqvn8AFEGQ== - dependencies: - "@docusaurus/core" "3.4.0" - "@docusaurus/module-type-aliases" "3.4.0" - "@docusaurus/theme-common" "3.4.0" - "@docusaurus/types" "3.4.0" - "@docusaurus/utils-validation" "3.4.0" - mermaid "^10.4.0" - tslib "^2.6.0" - -"@docusaurus/theme-search-algolia@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.4.0.tgz#c499bad71d668df0d0f15b0e5e33e2fc4e330fcc" - integrity sha512-aiHFx7OCw4Wck1z6IoShVdUWIjntC8FHCw9c5dR8r3q4Ynh+zkS8y2eFFunN/DL6RXPzpnvKCg3vhLQYJDmT9Q== - dependencies: - "@docsearch/react" "^3.5.2" - "@docusaurus/core" "3.4.0" - "@docusaurus/logger" "3.4.0" - "@docusaurus/plugin-content-docs" "3.4.0" - "@docusaurus/theme-common" "3.4.0" - "@docusaurus/theme-translations" "3.4.0" - "@docusaurus/utils" "3.4.0" - "@docusaurus/utils-validation" "3.4.0" - algoliasearch "^4.18.0" - algoliasearch-helper "^3.13.3" - clsx "^2.0.0" - eta "^2.2.0" - fs-extra "^11.1.1" - lodash "^4.17.21" - tslib "^2.6.0" - utility-types "^3.10.0" - -"@docusaurus/theme-translations@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/theme-translations/-/theme-translations-3.4.0.tgz#e6355d01352886c67e38e848b2542582ea3070af" - integrity sha512-zSxCSpmQCCdQU5Q4CnX/ID8CSUUI3fvmq4hU/GNP/XoAWtXo9SAVnM3TzpU8Gb//H3WCsT8mJcTfyOk3d9ftNg== - dependencies: - fs-extra "^11.1.1" - tslib "^2.6.0" - -"@docusaurus/types@3.4.0", "@docusaurus/types@^3.0.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/types/-/types-3.4.0.tgz#237c3f737e9db3f7c1a5935a3ef48d6eadde8292" - integrity sha512-4jcDO8kXi5Cf9TcyikB/yKmz14f2RZ2qTRerbHAsS+5InE9ZgSLBNLsewtFTcTOXSVcbU3FoGOzcNWAmU1TR0A== - dependencies: - "@mdx-js/mdx" "^3.0.0" - "@types/history" "^4.7.11" - "@types/react" "*" - commander "^5.1.0" - joi "^17.9.2" - react-helmet-async "^1.3.0" - utility-types "^3.10.0" - webpack "^5.88.1" - webpack-merge "^5.9.0" - -"@docusaurus/utils-common@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/utils-common/-/utils-common-3.4.0.tgz#2a43fefd35b85ab9fcc6833187e66c15f8bfbbc6" - integrity sha512-NVx54Wr4rCEKsjOH5QEVvxIqVvm+9kh7q8aYTU5WzUU9/Hctd6aTrcZ3G0Id4zYJ+AeaG5K5qHA4CY5Kcm2iyQ== - dependencies: - tslib "^2.6.0" - -"@docusaurus/utils-validation@3.4.0", "@docusaurus/utils-validation@^3.0.1": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/utils-validation/-/utils-validation-3.4.0.tgz#0176f6e503ff45f4390ec2ecb69550f55e0b5eb7" - integrity sha512-hYQ9fM+AXYVTWxJOT1EuNaRnrR2WGpRdLDQG07O8UOpsvCPWUVOeo26Rbm0JWY2sGLfzAb+tvJ62yF+8F+TV0g== - dependencies: - "@docusaurus/logger" "3.4.0" - "@docusaurus/utils" "3.4.0" - "@docusaurus/utils-common" "3.4.0" - fs-extra "^11.2.0" - joi "^17.9.2" - js-yaml "^4.1.0" - lodash "^4.17.21" - tslib "^2.6.0" - -"@docusaurus/utils@3.4.0", "@docusaurus/utils@^3.0.1": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@docusaurus/utils/-/utils-3.4.0.tgz#c508e20627b7a55e2b541e4a28c95e0637d6a204" - integrity sha512-fRwnu3L3nnWaXOgs88BVBmG1yGjcQqZNHG+vInhEa2Sz2oQB+ZjbEMO5Rh9ePFpZ0YDiDUhpaVjwmS+AU2F14g== - dependencies: - "@docusaurus/logger" "3.4.0" - "@docusaurus/utils-common" "3.4.0" - "@svgr/webpack" "^8.1.0" - escape-string-regexp "^4.0.0" - file-loader "^6.2.0" - fs-extra "^11.1.1" - github-slugger "^1.5.0" - globby "^11.1.0" - gray-matter "^4.0.3" - jiti "^1.20.0" - js-yaml "^4.1.0" - lodash "^4.17.21" - micromatch "^4.0.5" - prompts "^2.4.2" - resolve-pathname "^3.0.0" - shelljs "^0.8.5" - tslib "^2.6.0" - url-loader "^4.1.1" - utility-types "^3.10.0" - webpack "^5.88.1" - -"@exodus/schemasafe@^1.0.0-rc.2": - version "1.3.0" - resolved "https://registry.yarnpkg.com/@exodus/schemasafe/-/schemasafe-1.3.0.tgz#731656abe21e8e769a7f70a4d833e6312fe59b7f" - integrity sha512-5Aap/GaRupgNx/feGBwLLTVv8OQFfv3pq2lPRzPg9R+IOBnDgghTGW7l7EuVXOvg5cc/xSAlRW8rBrjIC3Nvqw== - -"@faker-js/faker@5.5.3": - version "5.5.3" - resolved "https://registry.yarnpkg.com/@faker-js/faker/-/faker-5.5.3.tgz#18e3af6b8eae7984072bbeb0c0858474d7c4cefe" - integrity sha512-R11tGE6yIFwqpaIqcfkcg7AICXzFg14+5h5v0TfF/9+RMDL6jhzCy/pxHVOfbALGdtVYdt6JdR21tuxEgl34dw== - -"@hapi/hoek@^9.0.0", "@hapi/hoek@^9.3.0": - version "9.3.0" - resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.3.0.tgz#8368869dcb735be2e7f5cb7647de78e167a251fb" - integrity sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ== - -"@hapi/topo@^5.1.0": - version "5.1.0" - resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-5.1.0.tgz#dc448e332c6c6e37a4dc02fd84ba8d44b9afb012" - integrity sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg== - dependencies: - "@hapi/hoek" "^9.0.0" - -"@headlessui/react@^1.7.4": - version "1.7.19" - resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-1.7.19.tgz#91c78cf5fcb254f4a0ebe96936d48421caf75f40" - integrity sha512-Ll+8q3OlMJfJbAKM/+/Y2q6PPYbryqNTXDbryx7SXLIDamkF6iQFbriYHga0dY44PvDhvvBWCx1Xj4U5+G4hOw== - dependencies: - "@tanstack/react-virtual" "^3.0.0-beta.60" - client-only "^0.0.1" - -"@heroicons/react@^2.0.13": - version "2.1.3" - resolved "https://registry.yarnpkg.com/@heroicons/react/-/react-2.1.3.tgz#78a2a7f504a7370283d07eabcddc7fec04f503db" - integrity sha512-fEcPfo4oN345SoqdlCDdSa4ivjaKbk0jTd+oubcgNxnNgAfzysfwWfQUr+51wigiWHQQRiZNd1Ao0M5Y3M2EGg== - -"@hookform/error-message@^2.0.1": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@hookform/error-message/-/error-message-2.0.1.tgz#6a37419106e13664ad6a29c9dae699ae6cd276b8" - integrity sha512-U410sAr92xgxT1idlu9WWOVjndxLdgPUHEB8Schr27C9eh7/xUnITWpCMF93s+lGiG++D4JnbSnrb5A21AdSNg== - -"@isaacs/cliui@^8.0.2": - version "8.0.2" - resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" - integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== - dependencies: - string-width "^5.1.2" - string-width-cjs "npm:string-width@^4.2.0" - strip-ansi "^7.0.1" - strip-ansi-cjs "npm:strip-ansi@^6.0.1" - wrap-ansi "^8.1.0" - wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" - -"@jest/schemas@^29.6.3": - version "29.6.3" - resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.6.3.tgz#430b5ce8a4e0044a7e3819663305a7b3091c8e03" - integrity sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA== - dependencies: - "@sinclair/typebox" "^0.27.8" - -"@jest/types@^29.6.3": - version "29.6.3" - resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.6.3.tgz#1131f8cf634e7e84c5e77bab12f052af585fba59" - integrity sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw== - 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" - -"@jridgewell/gen-mapping@^0.3.2", "@jridgewell/gen-mapping@^0.3.5": - version "0.3.5" - resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz#dcce6aff74bdf6dad1a95802b69b04a2fcb1fb36" - integrity sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg== - dependencies: - "@jridgewell/set-array" "^1.2.1" - "@jridgewell/sourcemap-codec" "^1.4.10" - "@jridgewell/trace-mapping" "^0.3.24" - -"@jridgewell/resolve-uri@^3.1.0": - version "3.1.2" - resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" - integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== - -"@jridgewell/set-array@^1.2.1": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280" - integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== - -"@jridgewell/source-map@^0.3.3": - version "0.3.6" - resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.6.tgz#9d71ca886e32502eb9362c9a74a46787c36df81a" - integrity sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ== - dependencies: - "@jridgewell/gen-mapping" "^0.3.5" - "@jridgewell/trace-mapping" "^0.3.25" - -"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": - version "1.4.15" - resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" - integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== - -"@jridgewell/trace-mapping@^0.3.18", "@jridgewell/trace-mapping@^0.3.20", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": - version "0.3.25" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" - integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== - dependencies: - "@jridgewell/resolve-uri" "^3.1.0" - "@jridgewell/sourcemap-codec" "^1.4.14" - -"@jsdevtools/ono@^7.1.3": - version "7.1.3" - resolved "https://registry.yarnpkg.com/@jsdevtools/ono/-/ono-7.1.3.tgz#9df03bbd7c696a5c58885c34aa06da41c8543796" - integrity sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg== - -"@leichtgewicht/ip-codec@^2.0.1": - version "2.0.5" - resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz#4fc56c15c580b9adb7dc3c333a134e540b44bfb1" - integrity sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw== - -"@mdx-js/mdx@^3.0.0": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@mdx-js/mdx/-/mdx-3.0.1.tgz#617bd2629ae561fdca1bb88e3badd947f5a82191" - integrity sha512-eIQ4QTrOWyL3LWEe/bu6Taqzq2HQvHcyTMaOrI95P2/LmJE7AsfPfgJGuFLPVqBUE1BC1rik3VIhU+s9u72arA== - dependencies: - "@types/estree" "^1.0.0" - "@types/estree-jsx" "^1.0.0" - "@types/hast" "^3.0.0" - "@types/mdx" "^2.0.0" - collapse-white-space "^2.0.0" - devlop "^1.0.0" - estree-util-build-jsx "^3.0.0" - estree-util-is-identifier-name "^3.0.0" - estree-util-to-js "^2.0.0" - estree-walker "^3.0.0" - hast-util-to-estree "^3.0.0" - hast-util-to-jsx-runtime "^2.0.0" - markdown-extensions "^2.0.0" - periscopic "^3.0.0" - remark-mdx "^3.0.0" - remark-parse "^11.0.0" - remark-rehype "^11.0.0" - source-map "^0.7.0" - unified "^11.0.0" - unist-util-position-from-estree "^2.0.0" - unist-util-stringify-position "^4.0.0" - unist-util-visit "^5.0.0" - vfile "^6.0.0" - -"@mdx-js/react@^3.0.0": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@mdx-js/react/-/react-3.0.1.tgz#997a19b3a5b783d936c75ae7c47cfe62f967f746" - integrity sha512-9ZrPIU4MGf6et1m1ov3zKf+q9+deetI51zprKB1D/z3NOb+rUxxtEl3mCjW5wTGh6VhRdwPueh1oRzi6ezkA8A== - dependencies: - "@types/mdx" "^2.0.0" - -"@nodelib/fs.scandir@2.1.5": - version "2.1.5" - resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" - integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== - dependencies: - "@nodelib/fs.stat" "2.0.5" - run-parallel "^1.1.9" - -"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": - version "2.0.5" - resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" - integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== - -"@nodelib/fs.walk@^1.2.3": - version "1.2.8" - resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" - integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== - dependencies: - "@nodelib/fs.scandir" "2.1.5" - fastq "^1.6.0" - -"@pkgjs/parseargs@^0.11.0": - version "0.11.0" - resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" - integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== - -"@pnpm/config.env-replace@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz#ab29da53df41e8948a00f2433f085f54de8b3a4c" - integrity sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w== - -"@pnpm/network.ca-file@^1.0.1": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz#2ab05e09c1af0cdf2fcf5035bea1484e222f7983" - integrity sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA== - dependencies: - graceful-fs "4.2.10" - -"@pnpm/npm-conf@^2.1.0": - version "2.2.2" - resolved "https://registry.yarnpkg.com/@pnpm/npm-conf/-/npm-conf-2.2.2.tgz#0058baf1c26cbb63a828f0193795401684ac86f0" - integrity sha512-UA91GwWPhFExt3IizW6bOeY/pQ0BkuNwKjk9iQW9KqxluGCrg4VenZ0/L+2Y0+ZOtme72EVvg6v0zo3AMQRCeA== - dependencies: - "@pnpm/config.env-replace" "^1.1.0" - "@pnpm/network.ca-file" "^1.0.1" - config-chain "^1.1.11" - -"@polka/url@^1.0.0-next.24": - version "1.0.0-next.25" - resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.25.tgz#f077fdc0b5d0078d30893396ff4827a13f99e817" - integrity sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ== - -"@redocly/ajv@^8.11.0": - version "8.11.0" - resolved "https://registry.yarnpkg.com/@redocly/ajv/-/ajv-8.11.0.tgz#2fad322888dc0113af026e08fceb3e71aae495ae" - integrity sha512-9GWx27t7xWhDIR02PA18nzBdLcKQRgc46xNQvjFkrYk4UOmvKhJ/dawwiX0cCOeetN5LcaaiqQbVOWYK62SGHw== - dependencies: - fast-deep-equal "^3.1.1" - json-schema-traverse "^1.0.0" - require-from-string "^2.0.2" - uri-js "^4.2.2" - -"@redocly/config@^0.6.0": - version "0.6.0" - resolved "https://registry.yarnpkg.com/@redocly/config/-/config-0.6.0.tgz#dbee2d5b181791b16b847565657f85092ff4c975" - integrity sha512-hNVN3eTxFj2nHYX0gGzZxxXwdE0DXWeWou1TIK3HYf0S9VKVxTxjO9EZbMB7iVUqaHkeqy4PSjlBQcEgD0Ftjg== - -"@redocly/openapi-core@^1.10.5": - version "1.15.0" - resolved "https://registry.yarnpkg.com/@redocly/openapi-core/-/openapi-core-1.15.0.tgz#dfe02cb65960d7d43935f2e55731f215d9fa741a" - integrity sha512-ac+3nn9y/dE+cgIVgIdq7eIisjZlBEJptLsCbOVLIsR2jb+O1SznXeyqy2MkTHMSs6zM/KHP4bMQy0DGmi7K0Q== - dependencies: - "@redocly/ajv" "^8.11.0" - "@redocly/config" "^0.6.0" - colorette "^1.2.0" - js-levenshtein "^1.1.6" - js-yaml "^4.1.0" - lodash.isequal "^4.5.0" - minimatch "^5.0.1" - node-fetch "^2.6.1" - pluralize "^8.0.0" - yaml-ast-parser "0.0.43" - -"@reduxjs/toolkit@^1.7.1": - version "1.9.7" - resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.9.7.tgz#7fc07c0b0ebec52043f8cb43510cf346405f78a6" - integrity sha512-t7v8ZPxhhKgOKtU+uyJT13lu4vL7az5aFi4IdoDs/eS548edn2M8Ik9h8fxgvMjGoAUVFSt6ZC1P5cWmQ014QQ== - dependencies: - immer "^9.0.21" - redux "^4.2.1" - redux-thunk "^2.4.2" - reselect "^4.1.8" - -"@sideway/address@^4.1.5": - version "4.1.5" - resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.5.tgz#4bc149a0076623ced99ca8208ba780d65a99b9d5" - integrity sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q== - dependencies: - "@hapi/hoek" "^9.0.0" - -"@sideway/formula@^3.0.1": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.1.tgz#80fcbcbaf7ce031e0ef2dd29b1bfc7c3f583611f" - integrity sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg== - -"@sideway/pinpoint@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@sideway/pinpoint/-/pinpoint-2.0.0.tgz#cff8ffadc372ad29fd3f78277aeb29e632cc70df" - integrity sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ== - -"@sinclair/typebox@^0.27.8": - version "0.27.8" - resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" - integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA== - -"@sindresorhus/is@^4.6.0": - version "4.6.0" - resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.6.0.tgz#3c7c9c46e678feefe7a2e5bb609d3dbd665ffb3f" - integrity sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw== - -"@sindresorhus/is@^5.2.0": - version "5.6.0" - resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-5.6.0.tgz#41dd6093d34652cddb5d5bdeee04eafc33826668" - integrity sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g== - -"@slorber/remark-comment@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@slorber/remark-comment/-/remark-comment-1.0.0.tgz#2a020b3f4579c89dec0361673206c28d67e08f5a" - integrity sha512-RCE24n7jsOj1M0UPvIQCHTe7fI0sFL4S2nwKVWwHyVr/wI/H8GosgsJGyhnsZoGFnD/P2hLf1mSbrrgSLN93NA== - dependencies: - micromark-factory-space "^1.0.0" - micromark-util-character "^1.1.0" - micromark-util-symbol "^1.0.1" - -"@svgr/babel-plugin-add-jsx-attribute@8.0.0": - version "8.0.0" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz#4001f5d5dd87fa13303e36ee106e3ff3a7eb8b22" - integrity sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g== - -"@svgr/babel-plugin-remove-jsx-attribute@8.0.0": - version "8.0.0" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-8.0.0.tgz#69177f7937233caca3a1afb051906698f2f59186" - integrity sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA== - -"@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0": - version "8.0.0" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-8.0.0.tgz#c2c48104cfd7dcd557f373b70a56e9e3bdae1d44" - integrity sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA== - -"@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0": - version "8.0.0" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-8.0.0.tgz#8fbb6b2e91fa26ac5d4aa25c6b6e4f20f9c0ae27" - integrity sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ== - -"@svgr/babel-plugin-svg-dynamic-title@8.0.0": - version "8.0.0" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-8.0.0.tgz#1d5ba1d281363fc0f2f29a60d6d936f9bbc657b0" - integrity sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og== - -"@svgr/babel-plugin-svg-em-dimensions@8.0.0": - version "8.0.0" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-8.0.0.tgz#35e08df300ea8b1d41cb8f62309c241b0369e501" - integrity sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g== - -"@svgr/babel-plugin-transform-react-native-svg@8.1.0": - version "8.1.0" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-8.1.0.tgz#90a8b63998b688b284f255c6a5248abd5b28d754" - integrity sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q== - -"@svgr/babel-plugin-transform-svg-component@8.0.0": - version "8.0.0" - resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-8.0.0.tgz#013b4bfca88779711f0ed2739f3f7efcefcf4f7e" - integrity sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw== - -"@svgr/babel-preset@8.1.0": - version "8.1.0" - resolved "https://registry.yarnpkg.com/@svgr/babel-preset/-/babel-preset-8.1.0.tgz#0e87119aecdf1c424840b9d4565b7137cabf9ece" - integrity sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug== - dependencies: - "@svgr/babel-plugin-add-jsx-attribute" "8.0.0" - "@svgr/babel-plugin-remove-jsx-attribute" "8.0.0" - "@svgr/babel-plugin-remove-jsx-empty-expression" "8.0.0" - "@svgr/babel-plugin-replace-jsx-attribute-value" "8.0.0" - "@svgr/babel-plugin-svg-dynamic-title" "8.0.0" - "@svgr/babel-plugin-svg-em-dimensions" "8.0.0" - "@svgr/babel-plugin-transform-react-native-svg" "8.1.0" - "@svgr/babel-plugin-transform-svg-component" "8.0.0" - -"@svgr/core@8.1.0": - version "8.1.0" - resolved "https://registry.yarnpkg.com/@svgr/core/-/core-8.1.0.tgz#41146f9b40b1a10beaf5cc4f361a16a3c1885e88" - integrity sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA== - dependencies: - "@babel/core" "^7.21.3" - "@svgr/babel-preset" "8.1.0" - camelcase "^6.2.0" - cosmiconfig "^8.1.3" - snake-case "^3.0.4" - -"@svgr/hast-util-to-babel-ast@8.0.0": - version "8.0.0" - resolved "https://registry.yarnpkg.com/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-8.0.0.tgz#6952fd9ce0f470e1aded293b792a2705faf4ffd4" - integrity sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q== - dependencies: - "@babel/types" "^7.21.3" - entities "^4.4.0" - -"@svgr/plugin-jsx@8.1.0": - version "8.1.0" - resolved "https://registry.yarnpkg.com/@svgr/plugin-jsx/-/plugin-jsx-8.1.0.tgz#96969f04a24b58b174ee4cd974c60475acbd6928" - integrity sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA== - dependencies: - "@babel/core" "^7.21.3" - "@svgr/babel-preset" "8.1.0" - "@svgr/hast-util-to-babel-ast" "8.0.0" - svg-parser "^2.0.4" - -"@svgr/plugin-svgo@8.1.0": - version "8.1.0" - resolved "https://registry.yarnpkg.com/@svgr/plugin-svgo/-/plugin-svgo-8.1.0.tgz#b115b7b967b564f89ac58feae89b88c3decd0f00" - integrity sha512-Ywtl837OGO9pTLIN/onoWLmDQ4zFUycI1g76vuKGEz6evR/ZTJlJuz3G/fIkb6OVBJ2g0o6CGJzaEjfmEo3AHA== - dependencies: - cosmiconfig "^8.1.3" - deepmerge "^4.3.1" - svgo "^3.0.2" - -"@svgr/webpack@^8.1.0": - version "8.1.0" - resolved "https://registry.yarnpkg.com/@svgr/webpack/-/webpack-8.1.0.tgz#16f1b5346f102f89fda6ec7338b96a701d8be0c2" - integrity sha512-LnhVjMWyMQV9ZmeEy26maJk+8HTIbd59cH4F2MJ439k9DqejRisfFNGAPvRYlKETuh9LrImlS8aKsBgKjMA8WA== - dependencies: - "@babel/core" "^7.21.3" - "@babel/plugin-transform-react-constant-elements" "^7.21.3" - "@babel/preset-env" "^7.20.2" - "@babel/preset-react" "^7.18.6" - "@babel/preset-typescript" "^7.21.0" - "@svgr/core" "8.1.0" - "@svgr/plugin-jsx" "8.1.0" - "@svgr/plugin-svgo" "8.1.0" - -"@swc/core-darwin-arm64@1.6.1": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.6.1.tgz#72d861fb7094b7a0004f4f300e2c5d4ea1549d9e" - integrity sha512-u6GdwOXsOEdNAdSI6nWq6G2BQw5HiSNIZVcBaH1iSvBnxZvWbnIKyDiZKaYnDwTLHLzig2GuUjjE2NaCJPy4jg== - -"@swc/core-darwin-x64@1.6.1": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.6.1.tgz#8b7070fcee4a4570d0af245c4614ca4e492dfd5b" - integrity sha512-/tXwQibkDNLVbAtr7PUQI0iQjoB708fjhDDDfJ6WILSBVZ3+qs/LHjJ7jHwumEYxVq1XA7Fv2Q7SE/ZSQoWHcQ== - -"@swc/core-linux-arm-gnueabihf@1.6.1": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.6.1.tgz#bea6d2e75127bbc65a664284f012ffa90c8325d5" - integrity sha512-aDgipxhJTms8iH78emHVutFR2c16LNhO+NTRCdYi+X4PyIn58/DyYTH6VDZ0AeEcS5f132ZFldU5AEgExwihXA== - -"@swc/core-linux-arm64-gnu@1.6.1": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.6.1.tgz#5c84d804ec23cf54b31c0bc0b4bdd30ec5d43ce8" - integrity sha512-XkJ+eO4zUKG5g458RyhmKPyBGxI0FwfWFgpfIj5eDybxYJ6s4HBT5MoxyBLorB5kMlZ0XoY/usUMobPVY3nL0g== - -"@swc/core-linux-arm64-musl@1.6.1": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.6.1.tgz#e167a350bec12caebc97304068c3ffbad6c398ce" - integrity sha512-dr6YbLBg/SsNxs1hDqJhxdcrS8dGMlOXJwXIrUvACiA8jAd6S5BxYCaqsCefLYXtaOmu0bbx1FB/evfodqB70Q== - -"@swc/core-linux-x64-gnu@1.6.1": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.6.1.tgz#fdd4e1d63b3e53d195e2ddcb9cb5ad9f31995796" - integrity sha512-A0b/3V+yFy4LXh3O9umIE7LXPC7NBWdjl6AQYqymSMcMu0EOb1/iygA6s6uWhz9y3e172Hpb9b/CGsuD8Px/bg== - -"@swc/core-linux-x64-musl@1.6.1": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.6.1.tgz#81a312dd9e62da5f4c48e3cd23b6c6d28a31ac42" - integrity sha512-5dJjlzZXhC87nZZZWbpiDP8kBIO0ibis893F/rtPIQBI5poH+iJuA32EU3wN4/WFHeK4et8z6SGSVghPtWyk4g== - -"@swc/core-win32-arm64-msvc@1.6.1": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.6.1.tgz#e131f579a69c5d807013e54ccb311e10caa27bcb" - integrity sha512-HBi1ZlwvfcUibLtT3g/lP57FaDPC799AD6InolB2KSgkqyBbZJ9wAXM8/CcH67GLIP0tZ7FqblrJTzGXxetTJQ== - -"@swc/core-win32-ia32-msvc@1.6.1": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.6.1.tgz#9f3d88cf0e826aa8222a695177a065ed2899eb21" - integrity sha512-AKqHohlWERclexar5y6ux4sQ8yaMejEXNxeKXm7xPhXrp13/1p4/I3E5bPVX/jMnvpm4HpcKSP0ee2WsqmhhPw== - -"@swc/core-win32-x64-msvc@1.6.1": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.6.1.tgz#b2082710bc46c484a2c9f2e33a15973806e5031d" - integrity sha512-0dLdTLd+ONve8kgC5T6VQ2Y5G+OZ7y0ujjapnK66wpvCBM6BKYGdT/OKhZKZydrC5gUKaxFN6Y5oOt9JOFUrOQ== - -"@swc/core@^1.3.74": - version "1.6.1" - resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.6.1.tgz#a899a205cfaa8e23f805451ef4787987e03b8920" - integrity sha512-Yz5uj5hNZpS5brLtBvKY0L4s2tBAbQ4TjmW8xF1EC3YLFxQRrUjMP49Zm1kp/KYyYvTkSaG48Ffj2YWLu9nChw== - dependencies: - "@swc/counter" "^0.1.3" - "@swc/types" "^0.1.8" - optionalDependencies: - "@swc/core-darwin-arm64" "1.6.1" - "@swc/core-darwin-x64" "1.6.1" - "@swc/core-linux-arm-gnueabihf" "1.6.1" - "@swc/core-linux-arm64-gnu" "1.6.1" - "@swc/core-linux-arm64-musl" "1.6.1" - "@swc/core-linux-x64-gnu" "1.6.1" - "@swc/core-linux-x64-musl" "1.6.1" - "@swc/core-win32-arm64-msvc" "1.6.1" - "@swc/core-win32-ia32-msvc" "1.6.1" - "@swc/core-win32-x64-msvc" "1.6.1" - -"@swc/counter@^0.1.3": - version "0.1.3" - resolved "https://registry.yarnpkg.com/@swc/counter/-/counter-0.1.3.tgz#cc7463bd02949611c6329596fccd2b0ec782b0e9" - integrity sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ== - -"@swc/types@^0.1.8": - version "0.1.8" - resolved "https://registry.yarnpkg.com/@swc/types/-/types-0.1.8.tgz#2c81d107c86cfbd0c3a05ecf7bb54c50dfa58a95" - integrity sha512-RNFA3+7OJFNYY78x0FYwi1Ow+iF1eF5WvmfY1nXPOEH4R2p/D4Cr1vzje7dNAI2aLFqpv8Wyz4oKSWqIZArpQA== - dependencies: - "@swc/counter" "^0.1.3" - -"@szmarczak/http-timer@^5.0.1": - version "5.0.1" - resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-5.0.1.tgz#c7c1bf1141cdd4751b0399c8fc7b8b664cd5be3a" - integrity sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw== - dependencies: - defer-to-connect "^2.0.1" - -"@tanstack/react-virtual@^3.0.0-beta.60": - version "3.5.1" - resolved "https://registry.yarnpkg.com/@tanstack/react-virtual/-/react-virtual-3.5.1.tgz#1ce466f530a10f781871360ed2bf7ff83e664f85" - integrity sha512-jIsuhfgy8GqA67PdWqg73ZB2LFE+HD9hjWL1L6ifEIZVyZVAKpYmgUG4WsKQ005aEyImJmbuimPiEvc57IY0Aw== - dependencies: - "@tanstack/virtual-core" "3.5.1" - -"@tanstack/virtual-core@3.5.1": - version "3.5.1" - resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.5.1.tgz#f519149bce9156d0e7954b9531df15f446f2fc12" - integrity sha512-046+AUSiDru/V9pajE1du8WayvBKeCvJ2NmKPy/mR8/SbKKrqmSbj7LJBfXE+nSq4f5TBXvnCzu0kcYebI9WdQ== - -"@trysound/sax@0.2.0": - version "0.2.0" - resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" - integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA== - -"@types/acorn@^4.0.0": - version "4.0.6" - resolved "https://registry.yarnpkg.com/@types/acorn/-/acorn-4.0.6.tgz#d61ca5480300ac41a7d973dd5b84d0a591154a22" - integrity sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ== - dependencies: - "@types/estree" "*" - -"@types/body-parser@*": - version "1.19.5" - resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.5.tgz#04ce9a3b677dc8bd681a17da1ab9835dc9d3ede4" - integrity sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg== - dependencies: - "@types/connect" "*" - "@types/node" "*" - -"@types/bonjour@^3.5.9": - version "3.5.13" - resolved "https://registry.yarnpkg.com/@types/bonjour/-/bonjour-3.5.13.tgz#adf90ce1a105e81dd1f9c61fdc5afda1bfb92956" - integrity sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ== - dependencies: - "@types/node" "*" - -"@types/connect-history-api-fallback@^1.3.5": - version "1.5.4" - resolved "https://registry.yarnpkg.com/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz#7de71645a103056b48ac3ce07b3520b819c1d5b3" - integrity sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw== - dependencies: - "@types/express-serve-static-core" "*" - "@types/node" "*" - -"@types/connect@*": - version "3.4.38" - resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858" - integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug== - dependencies: - "@types/node" "*" - -"@types/d3-scale-chromatic@^3.0.0": - version "3.0.3" - resolved "https://registry.yarnpkg.com/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.3.tgz#fc0db9c10e789c351f4c42d96f31f2e4df8f5644" - integrity sha512-laXM4+1o5ImZv3RpFAsTRn3TEkzqkytiOY0Dz0sq5cnd1dtNlk6sHLon4OvqaiJb28T0S/TdsBI3Sjsy+keJrw== - -"@types/d3-scale@^4.0.3": - version "4.0.8" - resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-4.0.8.tgz#d409b5f9dcf63074464bf8ddfb8ee5a1f95945bb" - integrity sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ== - dependencies: - "@types/d3-time" "*" - -"@types/d3-time@*": - version "3.0.3" - resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-3.0.3.tgz#3c186bbd9d12b9d84253b6be6487ca56b54f88be" - integrity sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw== - -"@types/debug@^4.0.0": - version "4.1.12" - resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.12.tgz#a155f21690871953410df4b6b6f53187f0500917" - integrity sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ== - dependencies: - "@types/ms" "*" - -"@types/eslint-scope@^3.7.3": - version "3.7.7" - resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.7.tgz#3108bd5f18b0cdb277c867b3dd449c9ed7079ac5" - integrity sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg== - dependencies: - "@types/eslint" "*" - "@types/estree" "*" - -"@types/eslint@*": - version "8.56.10" - resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.56.10.tgz#eb2370a73bf04a901eeba8f22595c7ee0f7eb58d" - integrity sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ== - dependencies: - "@types/estree" "*" - "@types/json-schema" "*" - -"@types/estree-jsx@^1.0.0": - version "1.0.5" - resolved "https://registry.yarnpkg.com/@types/estree-jsx/-/estree-jsx-1.0.5.tgz#858a88ea20f34fe65111f005a689fa1ebf70dc18" - integrity sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg== - dependencies: - "@types/estree" "*" - -"@types/estree@*", "@types/estree@^1.0.0", "@types/estree@^1.0.5": - version "1.0.5" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" - integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== - -"@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.33": - version "4.19.3" - resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.3.tgz#e469a13e4186c9e1c0418fb17be8bc8ff1b19a7a" - integrity sha512-KOzM7MhcBFlmnlr/fzISFF5vGWVSvN6fTd4T+ExOt08bA/dA5kpSzY52nMsI1KDFmUREpJelPYyuslLRSjjgCg== - dependencies: - "@types/node" "*" - "@types/qs" "*" - "@types/range-parser" "*" - "@types/send" "*" - -"@types/express@*", "@types/express@^4.17.13": - version "4.17.21" - resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.21.tgz#c26d4a151e60efe0084b23dc3369ebc631ed192d" - integrity sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ== - dependencies: - "@types/body-parser" "*" - "@types/express-serve-static-core" "^4.17.33" - "@types/qs" "*" - "@types/serve-static" "*" - -"@types/gtag.js@^0.0.12": - version "0.0.12" - resolved "https://registry.yarnpkg.com/@types/gtag.js/-/gtag.js-0.0.12.tgz#095122edca896689bdfcdd73b057e23064d23572" - integrity sha512-YQV9bUsemkzG81Ea295/nF/5GijnD2Af7QhEofh7xu+kvCN6RdodgNwwGWXB5GMI3NoyvQo0odNctoH/qLMIpg== - -"@types/hast@^2.0.0": - version "2.3.10" - resolved "https://registry.yarnpkg.com/@types/hast/-/hast-2.3.10.tgz#5c9d9e0b304bbb8879b857225c5ebab2d81d7643" - integrity sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw== - dependencies: - "@types/unist" "^2" - -"@types/hast@^3.0.0": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@types/hast/-/hast-3.0.4.tgz#1d6b39993b82cea6ad783945b0508c25903e15aa" - integrity sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ== - dependencies: - "@types/unist" "*" - -"@types/history@^4.7.11": - version "4.7.11" - resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.11.tgz#56588b17ae8f50c53983a524fc3cc47437969d64" - integrity sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA== - -"@types/hoist-non-react-statics@^3.3.0": - version "3.3.5" - resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz#dab7867ef789d87e2b4b0003c9d65c49cc44a494" - integrity sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg== - dependencies: - "@types/react" "*" - hoist-non-react-statics "^3.3.0" - -"@types/html-minifier-terser@^6.0.0": - version "6.1.0" - resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz#4fc33a00c1d0c16987b1a20cf92d20614c55ac35" - integrity sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg== - -"@types/http-cache-semantics@^4.0.2": - version "4.0.4" - resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz#b979ebad3919799c979b17c72621c0bc0a31c6c4" - integrity sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA== - -"@types/http-errors@*": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.4.tgz#7eb47726c391b7345a6ec35ad7f4de469cf5ba4f" - integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA== - -"@types/http-proxy@^1.17.8": - version "1.17.14" - resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.14.tgz#57f8ccaa1c1c3780644f8a94f9c6b5000b5e2eec" - integrity sha512-SSrD0c1OQzlFX7pGu1eXxSEjemej64aaNPRhhVYUGqXh0BtldAAx37MG8btcumvpgKyZp1F5Gn3JkktdxiFv6w== - dependencies: - "@types/node" "*" - -"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0": - version "2.0.6" - resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz#7739c232a1fee9b4d3ce8985f314c0c6d33549d7" - integrity sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w== - -"@types/istanbul-lib-report@*": - version "3.0.3" - resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz#53047614ae72e19fc0401d872de3ae2b4ce350bf" - integrity sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA== - dependencies: - "@types/istanbul-lib-coverage" "*" - -"@types/istanbul-reports@^3.0.0": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz#0f03e3d2f670fbdac586e34b433783070cc16f54" - integrity sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ== - dependencies: - "@types/istanbul-lib-report" "*" - -"@types/json-schema@*", "@types/json-schema@^7.0.15", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": - version "7.0.15" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" - integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== - -"@types/mdast@^3.0.0": - version "3.0.15" - resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.15.tgz#49c524a263f30ffa28b71ae282f813ed000ab9f5" - integrity sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ== - dependencies: - "@types/unist" "^2" - -"@types/mdast@^4.0.0", "@types/mdast@^4.0.2": - version "4.0.4" - resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-4.0.4.tgz#7ccf72edd2f1aa7dd3437e180c64373585804dd6" - integrity sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA== - dependencies: - "@types/unist" "*" - -"@types/mdx@^2.0.0": - version "2.0.13" - resolved "https://registry.yarnpkg.com/@types/mdx/-/mdx-2.0.13.tgz#68f6877043d377092890ff5b298152b0a21671bd" - integrity sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw== - -"@types/mime@^1": - version "1.3.5" - resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690" - integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== - -"@types/ms@*": - version "0.7.34" - resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.34.tgz#10964ba0dee6ac4cd462e2795b6bebd407303433" - integrity sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g== - -"@types/node-forge@^1.3.0": - version "1.3.11" - resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-1.3.11.tgz#0972ea538ddb0f4d9c2fa0ec5db5724773a604da" - integrity sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ== - dependencies: - "@types/node" "*" - -"@types/node@*": - version "20.14.2" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.2.tgz#a5f4d2bcb4b6a87bffcaa717718c5a0f208f4a18" - integrity sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q== - dependencies: - undici-types "~5.26.4" - -"@types/node@^17.0.5": - version "17.0.45" - resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.45.tgz#2c0fafd78705e7a18b7906b5201a522719dc5190" - integrity sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw== - -"@types/parse-json@^4.0.0": - version "4.0.2" - resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.2.tgz#5950e50960793055845e956c427fc2b0d70c5239" - integrity sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw== - -"@types/parse5@^6.0.0": - version "6.0.3" - resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-6.0.3.tgz#705bb349e789efa06f43f128cef51240753424cb" - integrity sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g== - -"@types/prismjs@^1.26.0": - version "1.26.4" - resolved "https://registry.yarnpkg.com/@types/prismjs/-/prismjs-1.26.4.tgz#1a9e1074619ce1d7322669e5b46fbe823925103a" - integrity sha512-rlAnzkW2sZOjbqZ743IHUhFcvzaGbqijwOu8QZnZCjfQzBqFE3s4lOTJEsxikImav9uzz/42I+O7YUs1mWgMlg== - -"@types/prop-types@*", "@types/prop-types@^15.0.0": - version "15.7.12" - resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.12.tgz#12bb1e2be27293c1406acb6af1c3f3a1481d98c6" - integrity sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q== - -"@types/qs@*": - version "6.9.15" - resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.15.tgz#adde8a060ec9c305a82de1babc1056e73bd64dce" - integrity sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg== - -"@types/range-parser@*": - version "1.2.7" - resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb" - integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== - -"@types/react-redux@^7.1.20": - version "7.1.33" - resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.33.tgz#53c5564f03f1ded90904e3c90f77e4bd4dc20b15" - 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" - -"@types/react-router-config@*", "@types/react-router-config@^5.0.7": - version "5.0.11" - resolved "https://registry.yarnpkg.com/@types/react-router-config/-/react-router-config-5.0.11.tgz#2761a23acc7905a66a94419ee40294a65aaa483a" - integrity sha512-WmSAg7WgqW7m4x8Mt4N6ZyKz0BubSj/2tVUMsAHp+Yd2AMwcSbeFq9WympT19p5heCFmF97R9eD5uUR/t4HEqw== - dependencies: - "@types/history" "^4.7.11" - "@types/react" "*" - "@types/react-router" "^5.1.0" - -"@types/react-router-dom@*": - version "5.3.3" - resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.3.3.tgz#e9d6b4a66fcdbd651a5f106c2656a30088cc1e83" - integrity sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw== - dependencies: - "@types/history" "^4.7.11" - "@types/react" "*" - "@types/react-router" "*" - -"@types/react-router@*", "@types/react-router@^5.1.0": - version "5.1.20" - resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.20.tgz#88eccaa122a82405ef3efbcaaa5dcdd9f021387c" - integrity sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q== - dependencies: - "@types/history" "^4.7.11" - "@types/react" "*" - -"@types/react@*": - version "18.3.3" - resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.3.tgz#9679020895318b0915d7a3ab004d92d33375c45f" - integrity sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw== - dependencies: - "@types/prop-types" "*" - csstype "^3.0.2" - -"@types/retry@0.12.0": - version "0.12.0" - resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" - integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== - -"@types/sax@^1.2.1": - version "1.2.7" - resolved "https://registry.yarnpkg.com/@types/sax/-/sax-1.2.7.tgz#ba5fe7df9aa9c89b6dff7688a19023dd2963091d" - integrity sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A== - dependencies: - "@types/node" "*" - -"@types/send@*": - version "0.17.4" - resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.4.tgz#6619cd24e7270793702e4e6a4b958a9010cfc57a" - integrity sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA== - dependencies: - "@types/mime" "^1" - "@types/node" "*" - -"@types/serve-index@^1.9.1": - version "1.9.4" - resolved "https://registry.yarnpkg.com/@types/serve-index/-/serve-index-1.9.4.tgz#e6ae13d5053cb06ed36392110b4f9a49ac4ec898" - integrity sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug== - dependencies: - "@types/express" "*" - -"@types/serve-static@*", "@types/serve-static@^1.13.10": - version "1.15.7" - resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.7.tgz#22174bbd74fb97fe303109738e9b5c2f3064f714" - integrity sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw== - dependencies: - "@types/http-errors" "*" - "@types/node" "*" - "@types/send" "*" - -"@types/sockjs@^0.3.33": - version "0.3.36" - resolved "https://registry.yarnpkg.com/@types/sockjs/-/sockjs-0.3.36.tgz#ce322cf07bcc119d4cbf7f88954f3a3bd0f67535" - integrity sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q== - dependencies: - "@types/node" "*" - -"@types/unist@*", "@types/unist@^3.0.0": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@types/unist/-/unist-3.0.2.tgz#6dd61e43ef60b34086287f83683a5c1b2dc53d20" - integrity sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ== - -"@types/unist@^2", "@types/unist@^2.0.0": - version "2.0.10" - resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.10.tgz#04ffa7f406ab628f7f7e97ca23e290cd8ab15efc" - integrity sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA== - -"@types/ws@^8.5.5": - version "8.5.10" - resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.10.tgz#4acfb517970853fa6574a3a6886791d04a396787" - integrity sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A== - dependencies: - "@types/node" "*" - -"@types/yargs-parser@*": - version "21.0.3" - resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.3.tgz#815e30b786d2e8f0dcd85fd5bcf5e1a04d008f15" - integrity sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ== - -"@types/yargs@^17.0.8": - version "17.0.32" - resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.32.tgz#030774723a2f7faafebf645f4e5a48371dca6229" - integrity sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog== - dependencies: - "@types/yargs-parser" "*" - -"@types/yauzl@^2.9.1": - version "2.10.3" - resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.10.3.tgz#e9b2808b4f109504a03cda958259876f61017999" - integrity sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q== - dependencies: - "@types/node" "*" - -"@ungap/structured-clone@^1.0.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" - integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== - -"@webassemblyjs/ast@1.12.1", "@webassemblyjs/ast@^1.12.1": - version "1.12.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.12.1.tgz#bb16a0e8b1914f979f45864c23819cc3e3f0d4bb" - integrity sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg== - dependencies: - "@webassemblyjs/helper-numbers" "1.11.6" - "@webassemblyjs/helper-wasm-bytecode" "1.11.6" - -"@webassemblyjs/floating-point-hex-parser@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz#dacbcb95aff135c8260f77fa3b4c5fea600a6431" - integrity sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw== - -"@webassemblyjs/helper-api-error@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz#6132f68c4acd59dcd141c44b18cbebbd9f2fa768" - integrity sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q== - -"@webassemblyjs/helper-buffer@1.12.1": - version "1.12.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz#6df20d272ea5439bf20ab3492b7fb70e9bfcb3f6" - integrity sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw== - -"@webassemblyjs/helper-numbers@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz#cbce5e7e0c1bd32cf4905ae444ef64cea919f1b5" - integrity sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g== - dependencies: - "@webassemblyjs/floating-point-hex-parser" "1.11.6" - "@webassemblyjs/helper-api-error" "1.11.6" - "@xtuc/long" "4.2.2" - -"@webassemblyjs/helper-wasm-bytecode@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz#bb2ebdb3b83aa26d9baad4c46d4315283acd51e9" - integrity sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA== - -"@webassemblyjs/helper-wasm-section@1.12.1": - version "1.12.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz#3da623233ae1a60409b509a52ade9bc22a37f7bf" - integrity sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g== - 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" - -"@webassemblyjs/ieee754@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz#bb665c91d0b14fffceb0e38298c329af043c6e3a" - integrity sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg== - dependencies: - "@xtuc/ieee754" "^1.2.0" - -"@webassemblyjs/leb128@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.6.tgz#70e60e5e82f9ac81118bc25381a0b283893240d7" - integrity sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ== - dependencies: - "@xtuc/long" "4.2.2" - -"@webassemblyjs/utf8@1.11.6": - version "1.11.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.6.tgz#90f8bc34c561595fe156603be7253cdbcd0fab5a" - integrity sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA== - -"@webassemblyjs/wasm-edit@^1.12.1": - version "1.12.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz#9f9f3ff52a14c980939be0ef9d5df9ebc678ae3b" - integrity sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g== - 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" - -"@webassemblyjs/wasm-gen@1.12.1": - version "1.12.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz#a6520601da1b5700448273666a71ad0a45d78547" - integrity sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w== - 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" - -"@webassemblyjs/wasm-opt@1.12.1": - version "1.12.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz#9e6e81475dfcfb62dab574ac2dda38226c232bc5" - integrity sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg== - dependencies: - "@webassemblyjs/ast" "1.12.1" - "@webassemblyjs/helper-buffer" "1.12.1" - "@webassemblyjs/wasm-gen" "1.12.1" - "@webassemblyjs/wasm-parser" "1.12.1" - -"@webassemblyjs/wasm-parser@1.12.1", "@webassemblyjs/wasm-parser@^1.12.1": - version "1.12.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz#c47acb90e6f083391e3fa61d113650eea1e95937" - integrity sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ== - 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" - -"@webassemblyjs/wast-printer@1.12.1": - version "1.12.1" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz#bcecf661d7d1abdaf989d8341a4833e33e2b31ac" - integrity sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA== - dependencies: - "@webassemblyjs/ast" "1.12.1" - "@xtuc/long" "4.2.2" - -"@xtuc/ieee754@^1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" - integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== - -"@xtuc/long@4.2.2": - version "4.2.2" - resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" - integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== - -abort-controller@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" - integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== - dependencies: - event-target-shim "^5.0.0" - -accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8: - version "1.3.8" - resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" - integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== - dependencies: - mime-types "~2.1.34" - negotiator "0.6.3" - -acorn-import-attributes@^1.9.5: - version "1.9.5" - resolved "https://registry.yarnpkg.com/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz#7eb1557b1ba05ef18b5ed0ec67591bfab04688ef" - integrity sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ== - -acorn-jsx@^5.0.0: - version "5.3.2" - resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" - integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== - -acorn-walk@^8.0.0: - version "8.3.3" - resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.3.tgz#9caeac29eefaa0c41e3d4c65137de4d6f34df43e" - integrity sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw== - dependencies: - acorn "^8.11.0" - -acorn@^8.0.0, acorn@^8.0.4, acorn@^8.11.0, acorn@^8.7.1, acorn@^8.8.2: - version "8.12.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.0.tgz#1627bfa2e058148036133b8d9b51a700663c294c" - integrity sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw== - -address@^1.0.1, address@^1.1.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/address/-/address-1.2.2.tgz#2b5248dac5485a6390532c6a517fda2e3faac89e" - integrity sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA== - -agent-base@6: - version "6.0.2" - resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" - integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== - dependencies: - debug "4" - -aggregate-error@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" - integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== - dependencies: - clean-stack "^2.0.0" - indent-string "^4.0.0" - -ajv-draft-04@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz#3b64761b268ba0b9e668f0b41ba53fce0ad77fc8" - integrity sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw== - -ajv-formats@2.1.1, ajv-formats@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" - integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA== - dependencies: - ajv "^8.0.0" - -ajv-keywords@^3.4.1, ajv-keywords@^3.5.2: - version "3.5.2" - resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" - integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== - -ajv-keywords@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz#69d4d385a4733cdbeab44964a1170a88f87f0e16" - integrity sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw== - dependencies: - fast-deep-equal "^3.1.3" - -ajv@8.11.0: - version "8.11.0" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.11.0.tgz#977e91dd96ca669f54a11e23e378e33b884a565f" - integrity sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg== - dependencies: - fast-deep-equal "^3.1.1" - json-schema-traverse "^1.0.0" - require-from-string "^2.0.2" - uri-js "^4.2.2" - -ajv@^6.12.2, ajv@^6.12.5: - version "6.12.6" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" - integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== - dependencies: - fast-deep-equal "^3.1.1" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.4.1" - uri-js "^4.2.2" - -ajv@^8.0.0, ajv@^8.9.0: - version "8.16.0" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.16.0.tgz#22e2a92b94f005f7e0f9c9d39652ef0b8f6f0cb4" - integrity sha512-F0twR8U1ZU67JIEtekUcLkXkoO5mMMmgGD8sK/xUFzJ805jxHQl92hImFAqqXMyMYjSPOyUPAwHYhB72g5sTXw== - dependencies: - fast-deep-equal "^3.1.3" - json-schema-traverse "^1.0.0" - require-from-string "^2.0.2" - uri-js "^4.4.1" - -algoliasearch-helper@^3.13.3: - version "3.21.0" - resolved "https://registry.yarnpkg.com/algoliasearch-helper/-/algoliasearch-helper-3.21.0.tgz#d28fdb61199b5c229714788bfb812376b18aaf28" - integrity sha512-hjVOrL15I3Y3K8xG0icwG1/tWE+MocqBrhW6uVBWpU+/kVEMK0BnM2xdssj6mZM61eJ4iRxHR0djEI3ENOpR8w== - dependencies: - "@algolia/events" "^4.0.1" - -algoliasearch@^4.18.0, algoliasearch@^4.19.1: - version "4.23.3" - resolved "https://registry.yarnpkg.com/algoliasearch/-/algoliasearch-4.23.3.tgz#e09011d0a3b0651444916a3e6bbcba064ec44b60" - integrity sha512-Le/3YgNvjW9zxIQMRhUHuhiUjAlKY/zsdZpfq4dlLqg6mEm0nL6yk+7f2hDOtLpxsgE4jSzDmvHL7nXdBp5feg== - dependencies: - "@algolia/cache-browser-local-storage" "4.23.3" - "@algolia/cache-common" "4.23.3" - "@algolia/cache-in-memory" "4.23.3" - "@algolia/client-account" "4.23.3" - "@algolia/client-analytics" "4.23.3" - "@algolia/client-common" "4.23.3" - "@algolia/client-personalization" "4.23.3" - "@algolia/client-search" "4.23.3" - "@algolia/logger-common" "4.23.3" - "@algolia/logger-console" "4.23.3" - "@algolia/recommend" "4.23.3" - "@algolia/requester-browser-xhr" "4.23.3" - "@algolia/requester-common" "4.23.3" - "@algolia/requester-node-http" "4.23.3" - "@algolia/transporter" "4.23.3" - -ansi-align@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-3.0.1.tgz#0cdf12e111ace773a86e9a1fad1225c43cb19a59" - integrity sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w== - dependencies: - string-width "^4.1.0" - -ansi-html-community@^0.0.8: - version "0.0.8" - resolved "https://registry.yarnpkg.com/ansi-html-community/-/ansi-html-community-0.0.8.tgz#69fbc4d6ccbe383f9736934ae34c3f8290f1bf41" - integrity sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw== - -ansi-regex@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" - integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== - -ansi-regex@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a" - integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA== - -ansi-styles@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" - integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== - dependencies: - color-convert "^1.9.0" - -ansi-styles@^4.0.0, ansi-styles@^4.1.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" - integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== - dependencies: - color-convert "^2.0.1" - -ansi-styles@^6.1.0: - version "6.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" - integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== - -any-promise@^1.0.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" - integrity sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A== - -anymatch@~3.1.2: - version "3.1.3" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" - integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== - dependencies: - normalize-path "^3.0.0" - picomatch "^2.0.4" - -arg@^5.0.0, arg@^5.0.2: - version "5.0.2" - resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.2.tgz#c81433cc427c92c4dcf4865142dbca6f15acd59c" - integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg== - -argparse@^1.0.7: - version "1.0.10" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" - integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== - dependencies: - sprintf-js "~1.0.2" - -argparse@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" - integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== - -array-flatten@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" - integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== - -array-union@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" - integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== - -asn1.js@^4.10.1: - version "4.10.1" - resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.10.1.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0" - integrity sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw== - dependencies: - bn.js "^4.0.0" - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - -assert@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/assert/-/assert-2.1.0.tgz#6d92a238d05dc02e7427c881fb8be81c8448b2dd" - integrity sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw== - dependencies: - call-bind "^1.0.2" - is-nan "^1.3.2" - object-is "^1.1.5" - object.assign "^4.1.4" - util "^0.12.5" - -astring@^1.8.0: - version "1.8.6" - resolved "https://registry.yarnpkg.com/astring/-/astring-1.8.6.tgz#2c9c157cf1739d67561c56ba896e6948f6b93731" - integrity sha512-ISvCdHdlTDlH5IpxQJIex7BWBywFWgjJSVdwst+/iQCoEYnyOaQ95+X1JGshuBjGp6nxKUy1jMgE3zPqN7fQdg== - -async@3.2.2: - version "3.2.2" - resolved "https://registry.yarnpkg.com/async/-/async-3.2.2.tgz#2eb7671034bb2194d45d30e31e24ec7e7f9670cd" - integrity sha512-H0E+qZaDEfx/FY4t7iLRv1W2fFI6+pyCeTw1uN20AQPiwqwM6ojPxHxdLv4z8hi2DtnW9BOckSspLucW7pIE5g== - -async@3.2.4: - version "3.2.4" - resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c" - integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ== - -at-least-node@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" - integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== - -autoprefixer@^10.4.13, autoprefixer@^10.4.14, autoprefixer@^10.4.19: - version "10.4.19" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.19.tgz#ad25a856e82ee9d7898c59583c1afeb3fa65f89f" - integrity sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew== - dependencies: - browserslist "^4.23.0" - caniuse-lite "^1.0.30001599" - fraction.js "^4.3.7" - normalize-range "^0.1.2" - picocolors "^1.0.0" - postcss-value-parser "^4.2.0" - -available-typed-arrays@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846" - integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ== - dependencies: - possible-typed-array-names "^1.0.0" - -axios@^0.25.0: - version "0.25.0" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.25.0.tgz#349cfbb31331a9b4453190791760a8d35b093e0a" - integrity sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g== - dependencies: - follow-redirects "^1.14.7" - -babel-loader@^9.1.3: - version "9.1.3" - resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-9.1.3.tgz#3d0e01b4e69760cc694ee306fe16d358aa1c6f9a" - integrity sha512-xG3ST4DglodGf8qSwv0MdeWLhrDsw/32QMdTO5T1ZIp9gQur0HkCyFs7Awskr10JKXFXwpAhiCuYX5oGXnRGbw== - dependencies: - find-cache-dir "^4.0.0" - schema-utils "^4.0.0" - -babel-plugin-dynamic-import-node@^2.3.3: - version "2.3.3" - resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz#84fda19c976ec5c6defef57f9427b3def66e17a3" - integrity sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ== - dependencies: - object.assign "^4.1.0" - -babel-plugin-polyfill-corejs2@^0.4.10: - version "0.4.11" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.11.tgz#30320dfe3ffe1a336c15afdcdafd6fd615b25e33" - integrity sha512-sMEJ27L0gRHShOh5G54uAAPaiCOygY/5ratXuiyb2G46FmlSpc9eFCzYVyDiPxfNbwzA7mYahmjQc5q+CZQ09Q== - dependencies: - "@babel/compat-data" "^7.22.6" - "@babel/helper-define-polyfill-provider" "^0.6.2" - semver "^6.3.1" - -babel-plugin-polyfill-corejs3@^0.10.1, babel-plugin-polyfill-corejs3@^0.10.4: - version "0.10.4" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.4.tgz#789ac82405ad664c20476d0233b485281deb9c77" - integrity sha512-25J6I8NGfa5YkCDogHRID3fVCadIR8/pGl1/spvCkzb6lVn6SR3ojpx9nOn9iEBcUsjY24AmdKm5khcfKdylcg== - dependencies: - "@babel/helper-define-polyfill-provider" "^0.6.1" - core-js-compat "^3.36.1" - -babel-plugin-polyfill-regenerator@^0.6.1: - version "0.6.2" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.2.tgz#addc47e240edd1da1058ebda03021f382bba785e" - integrity sha512-2R25rQZWP63nGwaAswvDazbPXfrM3HwVoBXK6HcqeKrSrL/JqcC/rDcf95l4r7LXLyxDXc8uQDa064GubtCABg== - dependencies: - "@babel/helper-define-polyfill-provider" "^0.6.2" - -bail@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/bail/-/bail-2.0.2.tgz#d26f5cd8fe5d6f832a31517b9f7c356040ba6d5d" - integrity sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw== - -balanced-match@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" - integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== - -base64-js@^1.3.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" - integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== - -batch@0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" - integrity sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw== - -big.js@^5.2.2: - version "5.2.2" - resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" - integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== - -binary-extensions@^2.0.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" - integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== - -bl@^4.0.3: - version "4.1.0" - resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" - integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== - dependencies: - buffer "^5.5.0" - inherits "^2.0.4" - readable-stream "^3.4.0" - -bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.9: - version "4.12.0" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" - integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== - -bn.js@^5.0.0, bn.js@^5.2.1: - version "5.2.1" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70" - integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ== - -body-parser@1.20.2: - version "1.20.2" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd" - integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA== - dependencies: - bytes "3.1.2" - content-type "~1.0.5" - debug "2.6.9" - depd "2.0.0" - destroy "1.2.0" - http-errors "2.0.0" - iconv-lite "0.4.24" - on-finished "2.4.1" - qs "6.11.0" - raw-body "2.5.2" - type-is "~1.6.18" - unpipe "1.0.0" - -bonjour-service@^1.0.11: - version "1.2.1" - resolved "https://registry.yarnpkg.com/bonjour-service/-/bonjour-service-1.2.1.tgz#eb41b3085183df3321da1264719fbada12478d02" - integrity sha512-oSzCS2zV14bh2kji6vNe7vrpJYCHGvcZnlffFQ1MEoX/WOeQ/teD8SYWKR942OI3INjq8OMNJlbPK5LLLUxFDw== - dependencies: - fast-deep-equal "^3.1.3" - multicast-dns "^7.2.5" - -boolbase@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" - integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== - -boxen@^6.2.1: - version "6.2.1" - resolved "https://registry.yarnpkg.com/boxen/-/boxen-6.2.1.tgz#b098a2278b2cd2845deef2dff2efc38d329b434d" - integrity sha512-H4PEsJXfFI/Pt8sjDWbHlQPx4zL/bvSQjcilJmaulGt5mLDorHOHpmdXAJcBcmru7PhYSp/cDMWRko4ZUMFkSw== - dependencies: - ansi-align "^3.0.1" - camelcase "^6.2.0" - chalk "^4.1.2" - cli-boxes "^3.0.0" - string-width "^5.0.1" - type-fest "^2.5.0" - widest-line "^4.0.1" - wrap-ansi "^8.0.1" - -boxen@^7.0.0: - version "7.1.1" - resolved "https://registry.yarnpkg.com/boxen/-/boxen-7.1.1.tgz#f9ba525413c2fec9cdb88987d835c4f7cad9c8f4" - integrity sha512-2hCgjEmP8YLWQ130n2FerGv7rYpfBmnmp9Uy2Le1vge6X3gZIfSmEzP5QTDElFxcvVcXlEn8Aq6MU/PZygIOog== - dependencies: - ansi-align "^3.0.1" - camelcase "^7.0.1" - chalk "^5.2.0" - cli-boxes "^3.0.0" - string-width "^5.1.2" - type-fest "^2.13.0" - widest-line "^4.0.1" - wrap-ansi "^8.1.0" - -brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -brace-expansion@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" - integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== - dependencies: - balanced-match "^1.0.0" - -braces@^3.0.3, braces@~3.0.2: - version "3.0.3" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" - integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== - dependencies: - fill-range "^7.1.1" - -brorand@^1.0.1, brorand@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" - integrity sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w== - -browserify-aes@^1.0.4, browserify-aes@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48" - integrity sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA== - dependencies: - buffer-xor "^1.0.3" - cipher-base "^1.0.0" - create-hash "^1.1.0" - evp_bytestokey "^1.0.3" - inherits "^2.0.1" - safe-buffer "^5.0.1" - -browserify-cipher@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.1.tgz#8d6474c1b870bfdabcd3bcfcc1934a10e94f15f0" - integrity sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w== - dependencies: - browserify-aes "^1.0.4" - browserify-des "^1.0.0" - evp_bytestokey "^1.0.0" - -browserify-des@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.2.tgz#3af4f1f59839403572f1c66204375f7a7f703e9c" - integrity sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A== - dependencies: - cipher-base "^1.0.1" - des.js "^1.0.0" - inherits "^2.0.1" - safe-buffer "^5.1.2" - -browserify-rsa@^4.0.0, browserify-rsa@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.1.0.tgz#b2fd06b5b75ae297f7ce2dc651f918f5be158c8d" - integrity sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog== - dependencies: - bn.js "^5.0.0" - randombytes "^2.0.1" - -browserify-sign@^4.0.0: - version "4.2.3" - resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.2.3.tgz#7afe4c01ec7ee59a89a558a4b75bd85ae62d4208" - integrity sha512-JWCZW6SKhfhjJxO8Tyiiy+XYB7cqd2S5/+WeYHsKdNKFlCBhKbblba1A/HN/90YwtxKc8tCErjffZl++UNmGiw== - dependencies: - bn.js "^5.2.1" - browserify-rsa "^4.1.0" - create-hash "^1.2.0" - create-hmac "^1.1.7" - elliptic "^6.5.5" - hash-base "~3.0" - inherits "^2.0.4" - parse-asn1 "^5.1.7" - readable-stream "^2.3.8" - safe-buffer "^5.2.1" - -browserify-zlib@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f" - integrity sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA== - dependencies: - pako "~1.0.5" - -browserslist@^4.0.0, browserslist@^4.18.1, browserslist@^4.21.10, browserslist@^4.22.2, browserslist@^4.23.0: - version "4.23.1" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.1.tgz#ce4af0534b3d37db5c1a4ca98b9080f985041e96" - integrity sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw== - dependencies: - caniuse-lite "^1.0.30001629" - electron-to-chromium "^1.4.796" - node-releases "^2.0.14" - update-browserslist-db "^1.0.16" - -buffer-crc32@~0.2.3: - version "0.2.13" - resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" - integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ== - -buffer-from@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" - integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== - -buffer-xor@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" - integrity sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ== - -buffer@^5.2.1, buffer@^5.5.0: - version "5.7.1" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" - integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== - dependencies: - base64-js "^1.3.1" - ieee754 "^1.1.13" - -buffer@^6.0.3: - version "6.0.3" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" - integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== - dependencies: - base64-js "^1.3.1" - ieee754 "^1.2.1" - -builtin-status-codes@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" - integrity sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ== - -bytes@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" - integrity sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw== - -bytes@3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" - integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== - -cacheable-lookup@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz#3476a8215d046e5a3202a9209dd13fec1f933a27" - integrity sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w== - -cacheable-request@^10.2.8: - version "10.2.14" - resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-10.2.14.tgz#eb915b665fda41b79652782df3f553449c406b9d" - integrity sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ== - dependencies: - "@types/http-cache-semantics" "^4.0.2" - get-stream "^6.0.1" - http-cache-semantics "^4.1.1" - keyv "^4.5.3" - mimic-response "^4.0.0" - normalize-url "^8.0.0" - responselike "^3.0.0" - -call-bind@^1.0.0, call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" - integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== - dependencies: - 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" - -call-me-maybe@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.2.tgz#03f964f19522ba643b1b0693acb9152fe2074baa" - integrity sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ== - -callsites@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" - integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== - -camel-case@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-4.1.2.tgz#9728072a954f805228225a6deea6b38461e1bd5a" - integrity sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw== - dependencies: - pascal-case "^3.1.2" - tslib "^2.0.3" - -camelcase-css@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/camelcase-css/-/camelcase-css-2.0.1.tgz#ee978f6947914cc30c6b44741b6ed1df7f043fd5" - integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA== - -camelcase@^6.2.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" - integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== - -camelcase@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-7.0.1.tgz#f02e50af9fd7782bc8b88a3558c32fd3a388f048" - integrity sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw== - -caniuse-api@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-3.0.0.tgz#5e4d90e2274961d46291997df599e3ed008ee4c0" - integrity sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw== - dependencies: - browserslist "^4.0.0" - caniuse-lite "^1.0.0" - lodash.memoize "^4.1.2" - lodash.uniq "^4.5.0" - -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001599, caniuse-lite@^1.0.30001629: - version "1.0.30001702" - resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001702.tgz" - integrity sha512-LoPe/D7zioC0REI5W73PeR1e1MLCipRGq/VkovJnd6Df+QVqT+vT33OXCp8QUd7kA7RZrHWxb1B36OQKI/0gOA== - -ccount@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/ccount/-/ccount-2.0.1.tgz#17a3bf82302e0870d6da43a01311a8bc02a3ecf5" - integrity sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg== - -chalk@^2.4.2: - version "2.4.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" - 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" - -chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" - integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - -chalk@^5.0.1, chalk@^5.2.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.3.0.tgz#67c20a7ebef70e7f3970a01f90fa210cb6860385" - integrity sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w== - -char-regex@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" - integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== - -character-entities-html4@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/character-entities-html4/-/character-entities-html4-2.1.0.tgz#1f1adb940c971a4b22ba39ddca6b618dc6e56b2b" - integrity sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA== - -character-entities-legacy@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz#76bc83a90738901d7bc223a9e93759fdd560125b" - integrity sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ== - -character-entities@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-2.0.2.tgz#2d09c2e72cd9523076ccb21157dff66ad43fcc22" - integrity sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ== - -character-reference-invalid@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz#85c66b041e43b47210faf401278abf808ac45cb9" - integrity sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw== - -charset@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/charset/-/charset-1.0.1.tgz#8d59546c355be61049a8fa9164747793319852bd" - integrity sha512-6dVyOOYjpfFcL1Y4qChrAoQLRHvj2ziyhcm0QJlhOcAhykL/k1kTUPbeo+87MNRTRdk2OIIsIXbuF3x2wi5EXg== - -cheerio-select@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/cheerio-select/-/cheerio-select-2.1.0.tgz#4d8673286b8126ca2a8e42740d5e3c4884ae21b4" - integrity sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g== - dependencies: - boolbase "^1.0.0" - css-select "^5.1.0" - css-what "^6.1.0" - domelementtype "^2.3.0" - domhandler "^5.0.3" - domutils "^3.0.1" - -cheerio@^1.0.0-rc.12: - version "1.0.0-rc.12" - resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.12.tgz#788bf7466506b1c6bf5fae51d24a2c4d62e47683" - integrity sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q== - dependencies: - cheerio-select "^2.1.0" - dom-serializer "^2.0.0" - domhandler "^5.0.3" - domutils "^3.0.1" - htmlparser2 "^8.0.1" - parse5 "^7.0.0" - parse5-htmlparser2-tree-adapter "^7.0.0" - -"chokidar@>=3.0.0 <4.0.0", chokidar@^3.4.2, chokidar@^3.5.3: - version "3.6.0" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" - integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== - dependencies: - anymatch "~3.1.2" - braces "~3.0.2" - glob-parent "~5.1.2" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.6.0" - optionalDependencies: - fsevents "~2.3.2" - -chownr@^1.1.1: - version "1.1.4" - resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" - integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== - -chrome-trace-event@^1.0.2: - version "1.0.4" - resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz#05bffd7ff928465093314708c93bdfa9bd1f0f5b" - integrity sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ== - -ci-info@^3.2.0: - version "3.9.0" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4" - integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== - -cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" - integrity sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q== - dependencies: - inherits "^2.0.1" - safe-buffer "^5.0.1" - -clean-css@^5.2.2, clean-css@^5.3.2, clean-css@~5.3.2: - version "5.3.3" - resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-5.3.3.tgz#b330653cd3bd6b75009cc25c714cae7b93351ccd" - integrity sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg== - dependencies: - source-map "~0.6.0" - -clean-stack@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" - integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== - -cli-boxes@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-3.0.0.tgz#71a10c716feeba005e4504f36329ef0b17cf3145" - integrity sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g== - -cli-table3@^0.6.3: - version "0.6.5" - resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.5.tgz#013b91351762739c16a9567c21a04632e449bf2f" - integrity sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ== - dependencies: - string-width "^4.2.0" - optionalDependencies: - "@colors/colors" "1.5.0" - -client-only@^0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1" - integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA== - -cliui@^8.0.1: - version "8.0.1" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" - 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" - -clone-deep@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" - integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ== - dependencies: - is-plain-object "^2.0.4" - kind-of "^6.0.2" - shallow-clone "^3.0.0" - -clsx@^1.1.1, clsx@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12" - integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== - -clsx@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999" - integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== - -collapse-white-space@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/collapse-white-space/-/collapse-white-space-2.1.0.tgz#640257174f9f42c740b40f3b55ee752924feefca" - integrity sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw== - -color-convert@^1.9.0: - version "1.9.3" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" - integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== - dependencies: - color-name "1.1.3" - -color-convert@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" - integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== - dependencies: - color-name "~1.1.4" - -color-name@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" - integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== - -color-name@~1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" - integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== - -colord@^2.9.3: - version "2.9.3" - resolved "https://registry.yarnpkg.com/colord/-/colord-2.9.3.tgz#4f8ce919de456f1d5c1c368c307fe20f3e59fb43" - integrity sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw== - -colorette@^1.2.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.4.0.tgz#5190fbb87276259a86ad700bff2c6d6faa3fca40" - integrity sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g== - -colorette@^2.0.10: - version "2.0.20" - resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" - integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== - -combine-promises@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/combine-promises/-/combine-promises-1.2.0.tgz#5f2e68451862acf85761ded4d9e2af7769c2ca6a" - integrity sha512-VcQB1ziGD0NXrhKxiwyNbCDmRzs/OShMs2GqW2DlU2A/Sd0nQxE1oWDAE5O0ygSx5mgQOn9eIFh7yKPgFRVkPQ== - -comma-separated-tokens@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz#4e89c9458acb61bc8fef19f4529973b2392839ee" - integrity sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg== - -commander@2.20.3, commander@^2.20.0: - version "2.20.3" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" - integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== - -commander@7, commander@^7.2.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" - integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== - -commander@^10.0.0: - version "10.0.1" - resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06" - integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== - -commander@^4.0.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" - integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== - -commander@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" - integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== - -commander@^8.3.0: - version "8.3.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" - integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== - -common-path-prefix@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/common-path-prefix/-/common-path-prefix-3.0.0.tgz#7d007a7e07c58c4b4d5f433131a19141b29f11e0" - integrity sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w== - -compressible@~2.0.16: - version "2.0.18" - resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" - integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== - dependencies: - mime-db ">= 1.43.0 < 2" - -compression@^1.7.4: - version "1.7.4" - resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f" - integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ== - 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" - -compute-gcd@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/compute-gcd/-/compute-gcd-1.2.1.tgz#34d639f3825625e1357ce81f0e456a6249d8c77f" - integrity sha512-TwMbxBNz0l71+8Sc4czv13h4kEqnchV9igQZBi6QUaz09dnz13juGnnaWWJTRsP3brxOoxeB4SA2WELLw1hCtg== - dependencies: - validate.io-array "^1.0.3" - validate.io-function "^1.0.2" - validate.io-integer-array "^1.0.0" - -compute-lcm@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/compute-lcm/-/compute-lcm-1.1.2.tgz#9107c66b9dca28cefb22b4ab4545caac4034af23" - integrity sha512-OFNPdQAXnQhDSKioX8/XYT6sdUlXwpeMjfd6ApxMJfyZ4GxmLR1xvMERctlYhlHwIiz6CSpBc2+qYKjHGZw4TQ== - dependencies: - compute-gcd "^1.2.1" - validate.io-array "^1.0.3" - validate.io-function "^1.0.2" - validate.io-integer-array "^1.0.0" - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== - -config-chain@^1.1.11: - version "1.1.13" - resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.13.tgz#fad0795aa6a6cdaff9ed1b68e9dff94372c232f4" - integrity sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ== - dependencies: - ini "^1.3.4" - proto-list "~1.2.1" - -configstore@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/configstore/-/configstore-6.0.0.tgz#49eca2ebc80983f77e09394a1a56e0aca8235566" - integrity sha512-cD31W1v3GqUlQvbBCGcXmd2Nj9SvLDOP1oQ0YFuLETufzSPaKp11rYBsSOm7rCsW3OnIRAFM3OxRhceaXNYHkA== - dependencies: - dot-prop "^6.0.1" - graceful-fs "^4.2.6" - unique-string "^3.0.0" - write-file-atomic "^3.0.3" - xdg-basedir "^5.0.1" - -connect-history-api-fallback@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz#647264845251a0daf25b97ce87834cace0f5f1c8" - integrity sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA== - -consola@^2.15.3: - version "2.15.3" - resolved "https://registry.yarnpkg.com/consola/-/consola-2.15.3.tgz#2e11f98d6a4be71ff72e0bdf07bd23e12cb61550" - integrity sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw== - -console-browserify@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336" - integrity sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA== - -constants-browserify@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" - integrity sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ== - -content-disposition@0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" - integrity sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA== - -content-disposition@0.5.4: - version "0.5.4" - resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" - integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== - dependencies: - safe-buffer "5.2.1" - -content-type@~1.0.4, content-type@~1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" - integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== - -convert-source-map@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" - integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== - -cookie-signature@1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" - integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== - -cookie@0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051" - integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw== - -copy-text-to-clipboard@^3.1.0, copy-text-to-clipboard@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/copy-text-to-clipboard/-/copy-text-to-clipboard-3.2.0.tgz#0202b2d9bdae30a49a53f898626dcc3b49ad960b" - integrity sha512-RnJFp1XR/LOBDckxTib5Qjr/PMfkatD0MUCQgdpqS8MdKiNUzBjAQBEN6oUy+jW7LI93BBG3DtMB2KOOKpGs2Q== - -copy-to-clipboard@^3.3.1: - version "3.3.3" - resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz#55ac43a1db8ae639a4bd99511c148cdd1b83a1b0" - integrity sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA== - dependencies: - toggle-selection "^1.0.6" - -copy-webpack-plugin@^11.0.0: - version "11.0.0" - resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz#96d4dbdb5f73d02dd72d0528d1958721ab72e04a" - integrity sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ== - dependencies: - fast-glob "^3.2.11" - glob-parent "^6.0.1" - globby "^13.1.1" - normalize-path "^3.0.0" - schema-utils "^4.0.0" - serialize-javascript "^6.0.0" - -core-js-compat@^3.31.0, core-js-compat@^3.36.1: - version "3.37.1" - resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.37.1.tgz#c844310c7852f4bdf49b8d339730b97e17ff09ee" - integrity sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg== - dependencies: - browserslist "^4.23.0" - -core-js-pure@^3.30.2: - version "3.37.1" - resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.37.1.tgz#2b4b34281f54db06c9a9a5bd60105046900553bd" - integrity sha512-J/r5JTHSmzTxbiYYrzXg9w1VpqrYt+gexenBE9pugeyhwPZTAEJddyiReJWsLO6uNQ8xJZFbod6XC7KKwatCiA== - -core-js@^3.31.1: - version "3.37.1" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.37.1.tgz#d21751ddb756518ac5a00e4d66499df981a62db9" - integrity sha512-Xn6qmxrQZyB0FFY8E3bgRXei3lWDJHhvI+u0q9TKIYM49G8pAr0FgnnrFRAmsbptZL1yxRADVXn+x5AGsbBfyw== - -core-util-is@~1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" - integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== - -cose-base@^1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/cose-base/-/cose-base-1.0.3.tgz#650334b41b869578a543358b80cda7e0abe0a60a" - integrity sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg== - dependencies: - layout-base "^1.0.0" - -cosmiconfig@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-6.0.0.tgz#da4fee853c52f6b1e6935f41c1a2fc50bd4a9982" - integrity sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg== - dependencies: - "@types/parse-json" "^4.0.0" - import-fresh "^3.1.0" - parse-json "^5.0.0" - path-type "^4.0.0" - yaml "^1.7.2" - -cosmiconfig@^8.1.3, cosmiconfig@^8.3.5: - version "8.3.6" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.3.6.tgz#060a2b871d66dba6c8538ea1118ba1ac16f5fae3" - integrity sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA== - dependencies: - import-fresh "^3.3.0" - js-yaml "^4.1.0" - parse-json "^5.2.0" - path-type "^4.0.0" - -create-ecdh@^4.0.0: - version "4.0.4" - resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.4.tgz#d6e7f4bffa66736085a0762fd3a632684dabcc4e" - integrity sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A== - dependencies: - bn.js "^4.1.0" - elliptic "^6.5.3" - -create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196" - integrity sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg== - dependencies: - cipher-base "^1.0.1" - inherits "^2.0.1" - md5.js "^1.3.4" - ripemd160 "^2.0.1" - sha.js "^2.4.0" - -create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: - version "1.1.7" - resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff" - integrity sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg== - dependencies: - cipher-base "^1.0.3" - create-hash "^1.1.0" - inherits "^2.0.1" - ripemd160 "^2.0.0" - safe-buffer "^5.0.1" - sha.js "^2.4.8" - -cross-fetch@3.1.5: - version "3.1.5" - resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f" - integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw== - dependencies: - node-fetch "2.6.7" - -cross-spawn@^7.0.0, cross-spawn@^7.0.3: - version "7.0.6" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" - integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== - dependencies: - path-key "^3.1.0" - shebang-command "^2.0.0" - which "^2.0.1" - -crypto-browserify@^3.12.0: - version "3.12.0" - resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" - integrity sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg== - 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" - -crypto-js@^4.1.1: - version "4.2.0" - resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.2.0.tgz#4d931639ecdfd12ff80e8186dba6af2c2e856631" - integrity sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q== - -crypto-random-string@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-4.0.0.tgz#5a3cc53d7dd86183df5da0312816ceeeb5bb1fc2" - integrity sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA== - dependencies: - type-fest "^1.0.1" - -css-declaration-sorter@^7.2.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-7.2.0.tgz#6dec1c9523bc4a643e088aab8f09e67a54961024" - integrity sha512-h70rUM+3PNFuaBDTLe8wF/cdWu+dOZmb7pJt8Z2sedYbAcQVQV/tEchueg3GWxwqS0cxtbxmaHEdkNACqcvsow== - -css-loader@^6.8.1: - version "6.11.0" - resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.11.0.tgz#33bae3bf6363d0a7c2cf9031c96c744ff54d85ba" - integrity sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g== - dependencies: - icss-utils "^5.1.0" - postcss "^8.4.33" - postcss-modules-extract-imports "^3.1.0" - postcss-modules-local-by-default "^4.0.5" - postcss-modules-scope "^3.2.0" - postcss-modules-values "^4.0.0" - postcss-value-parser "^4.2.0" - semver "^7.5.4" - -css-minimizer-webpack-plugin@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-5.0.1.tgz#33effe662edb1a0bf08ad633c32fa75d0f7ec565" - integrity sha512-3caImjKFQkS+ws1TGcFn0V1HyDJFq1Euy589JlD6/3rV2kj+w7r5G9WDMgSHvpvXHNZ2calVypZWuEDQd9wfLg== - dependencies: - "@jridgewell/trace-mapping" "^0.3.18" - cssnano "^6.0.1" - jest-worker "^29.4.3" - postcss "^8.4.24" - schema-utils "^4.0.1" - serialize-javascript "^6.0.1" - -css-select@^4.1.3: - version "4.3.0" - resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.3.0.tgz#db7129b2846662fd8628cfc496abb2b59e41529b" - integrity sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ== - dependencies: - boolbase "^1.0.0" - css-what "^6.0.1" - domhandler "^4.3.1" - domutils "^2.8.0" - nth-check "^2.0.1" - -css-select@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/css-select/-/css-select-5.1.0.tgz#b8ebd6554c3637ccc76688804ad3f6a6fdaea8a6" - integrity sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg== - dependencies: - boolbase "^1.0.0" - css-what "^6.1.0" - domhandler "^5.0.2" - domutils "^3.0.1" - nth-check "^2.0.1" - -css-tree@^2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-2.3.1.tgz#10264ce1e5442e8572fc82fbe490644ff54b5c20" - integrity sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw== - dependencies: - mdn-data "2.0.30" - source-map-js "^1.0.1" - -css-tree@~2.2.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-2.2.1.tgz#36115d382d60afd271e377f9c5f67d02bd48c032" - integrity sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA== - dependencies: - mdn-data "2.0.28" - source-map-js "^1.0.1" - -css-what@^6.0.1, css-what@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" - integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== - -cssesc@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" - integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== - -cssnano-preset-advanced@^6.1.2: - version "6.1.2" - resolved "https://registry.yarnpkg.com/cssnano-preset-advanced/-/cssnano-preset-advanced-6.1.2.tgz#82b090872b8f98c471f681d541c735acf8b94d3f" - integrity sha512-Nhao7eD8ph2DoHolEzQs5CfRpiEP0xa1HBdnFZ82kvqdmbwVBUr2r1QuQ4t1pi+D1ZpqpcO4T+wy/7RxzJ/WPQ== - dependencies: - autoprefixer "^10.4.19" - browserslist "^4.23.0" - cssnano-preset-default "^6.1.2" - postcss-discard-unused "^6.0.5" - postcss-merge-idents "^6.0.3" - postcss-reduce-idents "^6.0.3" - postcss-zindex "^6.0.2" - -cssnano-preset-default@^6.1.2: - version "6.1.2" - resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-6.1.2.tgz#adf4b89b975aa775f2750c89dbaf199bbd9da35e" - integrity sha512-1C0C+eNaeN8OcHQa193aRgYexyJtU8XwbdieEjClw+J9d94E41LwT6ivKH0WT+fYwYWB0Zp3I3IZ7tI/BbUbrg== - dependencies: - browserslist "^4.23.0" - css-declaration-sorter "^7.2.0" - cssnano-utils "^4.0.2" - postcss-calc "^9.0.1" - postcss-colormin "^6.1.0" - postcss-convert-values "^6.1.0" - postcss-discard-comments "^6.0.2" - postcss-discard-duplicates "^6.0.3" - postcss-discard-empty "^6.0.3" - postcss-discard-overridden "^6.0.2" - postcss-merge-longhand "^6.0.5" - postcss-merge-rules "^6.1.1" - postcss-minify-font-values "^6.1.0" - postcss-minify-gradients "^6.0.3" - postcss-minify-params "^6.1.0" - postcss-minify-selectors "^6.0.4" - postcss-normalize-charset "^6.0.2" - postcss-normalize-display-values "^6.0.2" - postcss-normalize-positions "^6.0.2" - postcss-normalize-repeat-style "^6.0.2" - postcss-normalize-string "^6.0.2" - postcss-normalize-timing-functions "^6.0.2" - postcss-normalize-unicode "^6.1.0" - postcss-normalize-url "^6.0.2" - postcss-normalize-whitespace "^6.0.2" - postcss-ordered-values "^6.0.2" - postcss-reduce-initial "^6.1.0" - postcss-reduce-transforms "^6.0.2" - postcss-svgo "^6.0.3" - postcss-unique-selectors "^6.0.4" - -cssnano-utils@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/cssnano-utils/-/cssnano-utils-4.0.2.tgz#56f61c126cd0f11f2eef1596239d730d9fceff3c" - integrity sha512-ZR1jHg+wZ8o4c3zqf1SIUSTIvm/9mU343FMR6Obe/unskbvpGhZOo1J6d/r8D1pzkRQYuwbcH3hToOuoA2G7oQ== - -cssnano@^6.0.1, cssnano@^6.1.2: - version "6.1.2" - resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-6.1.2.tgz#4bd19e505bd37ee7cf0dc902d3d869f6d79c66b8" - integrity sha512-rYk5UeX7VAM/u0lNqewCdasdtPK81CgX8wJFLEIXHbV2oldWRgJAsZrdhRXkV1NJzA2g850KiFm9mMU2HxNxMA== - dependencies: - cssnano-preset-default "^6.1.2" - lilconfig "^3.1.1" - -csso@^5.0.5: - version "5.0.5" - resolved "https://registry.yarnpkg.com/csso/-/csso-5.0.5.tgz#f9b7fe6cc6ac0b7d90781bb16d5e9874303e2ca6" - integrity sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ== - dependencies: - css-tree "~2.2.0" - -csstype@^3.0.2: - version "3.1.3" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" - integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== - -cytoscape-cose-bilkent@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz#762fa121df9930ffeb51a495d87917c570ac209b" - integrity sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ== - dependencies: - cose-base "^1.0.0" - -cytoscape@^3.28.1: - version "3.29.2" - resolved "https://registry.yarnpkg.com/cytoscape/-/cytoscape-3.29.2.tgz#c99f42513c80a75e2e94858add32896c860202ac" - integrity sha512-2G1ycU28Nh7OHT9rkXRLpCDP30MKH1dXJORZuBhtEhEW7pKwgPi77ImqlCWinouyE1PNepIOGZBOrE84DG7LyQ== - -"d3-array@1 - 2": - version "2.12.1" - resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-2.12.1.tgz#e20b41aafcdffdf5d50928004ececf815a465e81" - integrity sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ== - dependencies: - internmap "^1.0.0" - -"d3-array@2 - 3", "d3-array@2.10.0 - 3", "d3-array@2.5.0 - 3", d3-array@3, d3-array@^3.2.0: - version "3.2.4" - resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.2.4.tgz#15fec33b237f97ac5d7c986dc77da273a8ed0bb5" - integrity sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg== - dependencies: - internmap "1 - 2" - -d3-axis@3: - version "3.0.0" - resolved "https://registry.yarnpkg.com/d3-axis/-/d3-axis-3.0.0.tgz#c42a4a13e8131d637b745fc2973824cfeaf93322" - integrity sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw== - -d3-brush@3: - version "3.0.0" - resolved "https://registry.yarnpkg.com/d3-brush/-/d3-brush-3.0.0.tgz#6f767c4ed8dcb79de7ede3e1c0f89e63ef64d31c" - integrity sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ== - dependencies: - d3-dispatch "1 - 3" - d3-drag "2 - 3" - d3-interpolate "1 - 3" - d3-selection "3" - d3-transition "3" - -d3-chord@3: - version "3.0.1" - resolved "https://registry.yarnpkg.com/d3-chord/-/d3-chord-3.0.1.tgz#d156d61f485fce8327e6abf339cb41d8cbba6966" - integrity sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g== - dependencies: - d3-path "1 - 3" - -"d3-color@1 - 3", d3-color@3: - version "3.1.0" - resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2" - integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA== - -d3-contour@4: - version "4.0.2" - resolved "https://registry.yarnpkg.com/d3-contour/-/d3-contour-4.0.2.tgz#bb92063bc8c5663acb2422f99c73cbb6c6ae3bcc" - integrity sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA== - dependencies: - d3-array "^3.2.0" - -d3-delaunay@6: - version "6.0.4" - resolved "https://registry.yarnpkg.com/d3-delaunay/-/d3-delaunay-6.0.4.tgz#98169038733a0a5babbeda55054f795bb9e4a58b" - integrity sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A== - dependencies: - delaunator "5" - -"d3-dispatch@1 - 3", d3-dispatch@3: - version "3.0.1" - resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-3.0.1.tgz#5fc75284e9c2375c36c839411a0cf550cbfc4d5e" - integrity sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg== - -"d3-drag@2 - 3", d3-drag@3: - version "3.0.0" - resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-3.0.0.tgz#994aae9cd23c719f53b5e10e3a0a6108c69607ba" - integrity sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg== - dependencies: - d3-dispatch "1 - 3" - d3-selection "3" - -"d3-dsv@1 - 3", d3-dsv@3: - version "3.0.1" - resolved "https://registry.yarnpkg.com/d3-dsv/-/d3-dsv-3.0.1.tgz#c63af978f4d6a0d084a52a673922be2160789b73" - integrity sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q== - dependencies: - commander "7" - iconv-lite "0.6" - rw "1" - -"d3-ease@1 - 3", d3-ease@3: - version "3.0.1" - resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-3.0.1.tgz#9658ac38a2140d59d346160f1f6c30fda0bd12f4" - integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w== - -d3-fetch@3: - version "3.0.1" - resolved "https://registry.yarnpkg.com/d3-fetch/-/d3-fetch-3.0.1.tgz#83141bff9856a0edb5e38de89cdcfe63d0a60a22" - integrity sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw== - dependencies: - d3-dsv "1 - 3" - -d3-force@3: - version "3.0.0" - resolved "https://registry.yarnpkg.com/d3-force/-/d3-force-3.0.0.tgz#3e2ba1a61e70888fe3d9194e30d6d14eece155c4" - integrity sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg== - dependencies: - d3-dispatch "1 - 3" - d3-quadtree "1 - 3" - d3-timer "1 - 3" - -"d3-format@1 - 3", d3-format@3: - version "3.1.0" - resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-3.1.0.tgz#9260e23a28ea5cb109e93b21a06e24e2ebd55641" - integrity sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA== - -d3-geo@3: - version "3.1.1" - resolved "https://registry.yarnpkg.com/d3-geo/-/d3-geo-3.1.1.tgz#6027cf51246f9b2ebd64f99e01dc7c3364033a4d" - integrity sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q== - dependencies: - d3-array "2.5.0 - 3" - -d3-hierarchy@3: - version "3.1.2" - resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz#b01cd42c1eed3d46db77a5966cf726f8c09160c6" - integrity sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA== - -"d3-interpolate@1 - 3", "d3-interpolate@1.2.0 - 3", d3-interpolate@3: - version "3.0.1" - resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d" - integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g== - dependencies: - d3-color "1 - 3" - -d3-path@1: - version "1.0.9" - resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-1.0.9.tgz#48c050bb1fe8c262493a8caf5524e3e9591701cf" - integrity sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg== - -"d3-path@1 - 3", d3-path@3, d3-path@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-3.1.0.tgz#22df939032fb5a71ae8b1800d61ddb7851c42526" - integrity sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ== - -d3-polygon@3: - version "3.0.1" - resolved "https://registry.yarnpkg.com/d3-polygon/-/d3-polygon-3.0.1.tgz#0b45d3dd1c48a29c8e057e6135693ec80bf16398" - integrity sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg== - -"d3-quadtree@1 - 3", d3-quadtree@3: - version "3.0.1" - resolved "https://registry.yarnpkg.com/d3-quadtree/-/d3-quadtree-3.0.1.tgz#6dca3e8be2b393c9a9d514dabbd80a92deef1a4f" - integrity sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw== - -d3-random@3: - version "3.0.1" - resolved "https://registry.yarnpkg.com/d3-random/-/d3-random-3.0.1.tgz#d4926378d333d9c0bfd1e6fa0194d30aebaa20f4" - integrity sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ== - -d3-sankey@^0.12.3: - version "0.12.3" - resolved "https://registry.yarnpkg.com/d3-sankey/-/d3-sankey-0.12.3.tgz#b3c268627bd72e5d80336e8de6acbfec9d15d01d" - integrity sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ== - dependencies: - d3-array "1 - 2" - d3-shape "^1.2.0" - -d3-scale-chromatic@3: - version "3.1.0" - resolved "https://registry.yarnpkg.com/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz#34c39da298b23c20e02f1a4b239bd0f22e7f1314" - integrity sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ== - dependencies: - d3-color "1 - 3" - d3-interpolate "1 - 3" - -d3-scale@4: - version "4.0.2" - resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-4.0.2.tgz#82b38e8e8ff7080764f8dcec77bd4be393689396" - integrity sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ== - dependencies: - d3-array "2.10.0 - 3" - d3-format "1 - 3" - d3-interpolate "1.2.0 - 3" - d3-time "2.1.1 - 3" - d3-time-format "2 - 4" - -"d3-selection@2 - 3", d3-selection@3: - version "3.0.0" - resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-3.0.0.tgz#c25338207efa72cc5b9bd1458a1a41901f1e1b31" - integrity sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ== - -d3-shape@3: - version "3.2.0" - resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-3.2.0.tgz#a1a839cbd9ba45f28674c69d7f855bcf91dfc6a5" - integrity sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA== - dependencies: - d3-path "^3.1.0" - -d3-shape@^1.2.0: - version "1.3.7" - resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-1.3.7.tgz#df63801be07bc986bc54f63789b4fe502992b5d7" - integrity sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw== - dependencies: - d3-path "1" - -"d3-time-format@2 - 4", d3-time-format@4: - version "4.1.0" - resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-4.1.0.tgz#7ab5257a5041d11ecb4fe70a5c7d16a195bb408a" - integrity sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg== - dependencies: - d3-time "1 - 3" - -"d3-time@1 - 3", "d3-time@2.1.1 - 3", d3-time@3: - version "3.1.0" - resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-3.1.0.tgz#9310db56e992e3c0175e1ef385e545e48a9bb5c7" - integrity sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q== - dependencies: - d3-array "2 - 3" - -"d3-timer@1 - 3", d3-timer@3: - version "3.0.1" - resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0" - integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA== - -"d3-transition@2 - 3", d3-transition@3: - version "3.0.1" - resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-3.0.1.tgz#6869fdde1448868077fdd5989200cb61b2a1645f" - integrity sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w== - dependencies: - d3-color "1 - 3" - d3-dispatch "1 - 3" - d3-ease "1 - 3" - d3-interpolate "1 - 3" - d3-timer "1 - 3" - -d3-zoom@3: - version "3.0.0" - resolved "https://registry.yarnpkg.com/d3-zoom/-/d3-zoom-3.0.0.tgz#d13f4165c73217ffeaa54295cd6969b3e7aee8f3" - integrity sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw== - dependencies: - d3-dispatch "1 - 3" - d3-drag "2 - 3" - d3-interpolate "1 - 3" - d3-selection "2 - 3" - d3-transition "2 - 3" - -d3@^7.4.0, d3@^7.8.2: - version "7.9.0" - resolved "https://registry.yarnpkg.com/d3/-/d3-7.9.0.tgz#579e7acb3d749caf8860bd1741ae8d371070cd5d" - integrity sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA== - dependencies: - d3-array "3" - d3-axis "3" - d3-brush "3" - d3-chord "3" - d3-color "3" - d3-contour "4" - d3-delaunay "6" - d3-dispatch "3" - d3-drag "3" - d3-dsv "3" - d3-ease "3" - d3-fetch "3" - d3-force "3" - d3-format "3" - d3-geo "3" - d3-hierarchy "3" - d3-interpolate "3" - d3-path "3" - d3-polygon "3" - d3-quadtree "3" - d3-random "3" - d3-scale "4" - d3-scale-chromatic "3" - d3-selection "3" - d3-shape "3" - d3-time "3" - d3-time-format "4" - d3-timer "3" - d3-transition "3" - d3-zoom "3" - -dagre-d3-es@7.0.10: - version "7.0.10" - resolved "https://registry.yarnpkg.com/dagre-d3-es/-/dagre-d3-es-7.0.10.tgz#19800d4be674379a3cd8c86a8216a2ac6827cadc" - integrity sha512-qTCQmEhcynucuaZgY5/+ti3X/rnszKZhEQH/ZdWdtP1tA/y3VoHJzcVrO9pjjJCNpigfscAtoUB5ONcd2wNn0A== - dependencies: - d3 "^7.8.2" - lodash-es "^4.17.21" - -dayjs@^1.11.7: - version "1.11.11" - resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.11.tgz#dfe0e9d54c5f8b68ccf8ca5f72ac603e7e5ed59e" - integrity sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg== - -debounce@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.1.tgz#38881d8f4166a5c5848020c11827b834bcb3e0a5" - integrity sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug== - -debug@2.6.9, debug@^2.6.0: - version "2.6.9" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" - -debug@4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1: - version "4.3.5" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.5.tgz#e83444eceb9fedd4a1da56d671ae2446a01a6e1e" - integrity sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg== - dependencies: - ms "2.1.2" - -debug@4.3.4: - version "4.3.4" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" - integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== - dependencies: - ms "2.1.2" - -decode-named-character-reference@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz#daabac9690874c394c81e4162a0304b35d824f0e" - integrity sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg== - dependencies: - character-entities "^2.0.0" - -decompress-response@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc" - integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ== - dependencies: - mimic-response "^3.1.0" - -deep-extend@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" - integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== - -deepmerge@^4.0.0, deepmerge@^4.2.2, deepmerge@^4.3.1: - version "4.3.1" - resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" - integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== - -default-gateway@^6.0.3: - version "6.0.3" - resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-6.0.3.tgz#819494c888053bdb743edbf343d6cdf7f2943a71" - integrity sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg== - dependencies: - execa "^5.0.0" - -defer-to-connect@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-2.0.1.tgz#8016bdb4143e4632b77a3449c6236277de520587" - integrity sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg== - -define-data-property@^1.0.1, define-data-property@^1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" - integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== - dependencies: - es-define-property "^1.0.0" - es-errors "^1.3.0" - gopd "^1.0.1" - -define-lazy-prop@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" - integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== - -define-properties@^1.1.3, define-properties@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" - integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== - dependencies: - define-data-property "^1.0.1" - has-property-descriptors "^1.0.0" - object-keys "^1.1.1" - -del@^6.1.1: - version "6.1.1" - resolved "https://registry.yarnpkg.com/del/-/del-6.1.1.tgz#3b70314f1ec0aa325c6b14eb36b95786671edb7a" - integrity sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg== - 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" - -delaunator@5: - version "5.0.1" - resolved "https://registry.yarnpkg.com/delaunator/-/delaunator-5.0.1.tgz#39032b08053923e924d6094fe2cde1a99cc51278" - integrity sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw== - dependencies: - robust-predicates "^3.0.2" - -depd@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" - integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== - -depd@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" - integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== - -dequal@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" - integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== - -des.js@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.1.0.tgz#1d37f5766f3bbff4ee9638e871a8768c173b81da" - integrity sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg== - dependencies: - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - -destroy@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" - integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== - -detect-node@^2.0.4: - version "2.1.0" - resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" - integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== - -detect-port-alt@^1.1.6: - version "1.1.6" - resolved "https://registry.yarnpkg.com/detect-port-alt/-/detect-port-alt-1.1.6.tgz#24707deabe932d4a3cf621302027c2b266568275" - integrity sha512-5tQykt+LqfJFBEYaDITx7S7cR7mJ/zQmLXZ2qt5w04ainYZw6tBf9dBunMjVeVOdYVRUzUOE4HkY5J7+uttb5Q== - dependencies: - address "^1.0.1" - debug "^2.6.0" - -detect-port@^1.5.1: - version "1.6.1" - resolved "https://registry.yarnpkg.com/detect-port/-/detect-port-1.6.1.tgz#45e4073997c5f292b957cb678fb0bb8ed4250a67" - integrity sha512-CmnVc+Hek2egPx1PeTFVta2W78xy2K/9Rkf6cC4T59S50tVnzKj+tnx5mmx5lwvCkujZ4uRrpRSuV+IVs3f90Q== - dependencies: - address "^1.0.1" - debug "4" - -devlop@^1.0.0, devlop@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/devlop/-/devlop-1.1.0.tgz#4db7c2ca4dc6e0e834c30be70c94bbc976dc7018" - integrity sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA== - dependencies: - dequal "^2.0.0" - -devtools-protocol@0.0.1045489: - version "0.0.1045489" - resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.1045489.tgz#f959ad560b05acd72d55644bc3fb8168a83abf28" - integrity sha512-D+PTmWulkuQW4D1NTiCRCFxF7pQPn0hgp4YyX4wAQ6xYXKOadSWPR3ENGDQ47MW/Ewc9v2rpC/UEEGahgBYpSQ== - -didyoumean@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037" - integrity sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw== - -diff@^5.0.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/diff/-/diff-5.2.0.tgz#26ded047cd1179b78b9537d5ef725503ce1ae531" - integrity sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A== - -diffie-hellman@^5.0.0: - version "5.0.3" - resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" - integrity sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg== - dependencies: - bn.js "^4.1.0" - miller-rabin "^4.0.0" - randombytes "^2.0.0" - -dir-glob@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" - integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== - dependencies: - path-type "^4.0.0" - -dlv@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/dlv/-/dlv-1.1.3.tgz#5c198a8a11453596e751494d49874bc7732f2e79" - integrity sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA== - -dns-packet@^5.2.2: - version "5.6.1" - resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-5.6.1.tgz#ae888ad425a9d1478a0674256ab866de1012cf2f" - integrity sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw== - dependencies: - "@leichtgewicht/ip-codec" "^2.0.1" - -docusaurus-plugin-image-zoom@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/docusaurus-plugin-image-zoom/-/docusaurus-plugin-image-zoom-1.0.1.tgz#17afec39f2e630cac50a4ed3a8bbdad8d0aa8b9d" - integrity sha512-96IpSKUx2RWy3db9aZ0s673OQo5DWgV9UVWouS+CPOSIVEdCWh6HKmWf6tB9rsoaiIF3oNn9keiyv6neEyKb1Q== - dependencies: - medium-zoom "^1.0.6" - validate-peer-dependencies "^2.2.0" - -docusaurus-plugin-openapi-docs@3.0.1, docusaurus-plugin-openapi-docs@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/docusaurus-plugin-openapi-docs/-/docusaurus-plugin-openapi-docs-3.0.1.tgz#954fdc4103d7e47133aede210a98353b3e0f0f99" - integrity sha512-6SRqwey/TXMNu2G02mbWgxrifhpjGOjDr30N+58AR0Ytgc+HXMqlPAUIvTe+e7sOBfAtBbiNlmOWv5KSYIjf3w== - dependencies: - "@apidevtools/json-schema-ref-parser" "^11.5.4" - "@docusaurus/plugin-content-docs" "^3.0.1" - "@docusaurus/utils" "^3.0.1" - "@docusaurus/utils-validation" "^3.0.1" - "@redocly/openapi-core" "^1.10.5" - chalk "^4.1.2" - clsx "^1.1.1" - fs-extra "^9.0.1" - json-pointer "^0.6.2" - json-schema-merge-allof "^0.8.1" - json5 "^2.2.3" - lodash "^4.17.20" - mustache "^4.2.0" - openapi-to-postmanv2 "^4.21.0" - postman-collection "^4.4.0" - slugify "^1.6.5" - swagger2openapi "^7.0.8" - xml-formatter "^2.6.1" - -docusaurus-plugin-sass@^0.2.3: - version "0.2.5" - resolved "https://registry.yarnpkg.com/docusaurus-plugin-sass/-/docusaurus-plugin-sass-0.2.5.tgz#6bfb8a227ac6265be685dcbc24ba1989e27b8005" - integrity sha512-Z+D0fLFUKcFpM+bqSUmqKIU+vO+YF1xoEQh5hoFreg2eMf722+siwXDD+sqtwU8E4MvVpuvsQfaHwODNlxJAEg== - dependencies: - sass-loader "^10.1.1" - -docusaurus-theme-github-codeblock@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/docusaurus-theme-github-codeblock/-/docusaurus-theme-github-codeblock-2.0.2.tgz#88b7044b81f9091330e8e4a07a1bdc9114a9fb93" - integrity sha512-H2WoQPWOLjGZO6KS58Gsd+eUVjTFJemkReiSSu9chqokyLc/3Ih3+zPRYfuEZ/HsDvSMIarf7CNcp+Vt+/G+ig== - dependencies: - "@docusaurus/types" "^3.0.0" - -docusaurus-theme-openapi-docs@3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/docusaurus-theme-openapi-docs/-/docusaurus-theme-openapi-docs-3.0.1.tgz#49789c63377f294e624a9632eddb8265a421020f" - integrity sha512-tqypV91tC3wuWj9O+4n0M/e5AgHOeMT2nvPj1tjlPkC7/dLinZvpwQStT4YDUPYSoHRseqxd7lhivFQHcmlryg== - dependencies: - "@docusaurus/theme-common" "^3.0.1" - "@hookform/error-message" "^2.0.1" - "@reduxjs/toolkit" "^1.7.1" - clsx "^1.1.1" - copy-text-to-clipboard "^3.1.0" - crypto-js "^4.1.1" - docusaurus-plugin-openapi-docs "^3.0.1" - docusaurus-plugin-sass "^0.2.3" - file-saver "^2.0.5" - lodash "^4.17.20" - node-polyfill-webpack-plugin "^2.0.1" - postman-code-generators "^1.10.1" - postman-collection "^4.4.0" - prism-react-renderer "^2.3.0" - react-hook-form "^7.43.8" - react-live "^4.0.0" - react-magic-dropzone "^1.0.1" - react-markdown "^8.0.1" - react-modal "^3.15.1" - react-redux "^7.2.0" - rehype-raw "^6.1.1" - sass "^1.58.1" - sass-loader "^13.3.2" - webpack "^5.61.0" - xml-formatter "^2.6.1" - -dom-converter@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768" - integrity sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA== - dependencies: - utila "~0.4" - -dom-serializer@^1.0.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.4.1.tgz#de5d41b1aea290215dc45a6dae8adcf1d32e2d30" - integrity sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag== - dependencies: - domelementtype "^2.0.1" - domhandler "^4.2.0" - entities "^2.0.0" - -dom-serializer@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53" - integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg== - dependencies: - domelementtype "^2.3.0" - domhandler "^5.0.2" - entities "^4.2.0" - -domain-browser@^4.22.0: - version "4.23.0" - resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-4.23.0.tgz#427ebb91efcb070f05cffdfb8a4e9a6c25f8c94b" - integrity sha512-ArzcM/II1wCCujdCNyQjXrAFwS4mrLh4C7DZWlaI8mdh7h3BfKdNd3bKXITfl2PT9FtfQqaGvhi1vPRQPimjGA== - -domelementtype@^2.0.1, domelementtype@^2.2.0, domelementtype@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" - integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== - -domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.3.1: - version "4.3.1" - resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.1.tgz#8d792033416f59d68bc03a5aa7b018c1ca89279c" - integrity sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ== - dependencies: - domelementtype "^2.2.0" - -domhandler@^5.0.2, domhandler@^5.0.3: - version "5.0.3" - resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31" - integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w== - dependencies: - domelementtype "^2.3.0" - -dompurify@^3.0.5: - version "3.1.5" - resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.1.5.tgz#2c6a113fc728682a0f55684b1388c58ddb79dc38" - integrity sha512-lwG+n5h8QNpxtyrJW/gJWckL+1/DQiYMX8f7t8Z2AZTPw1esVrqjI63i7Zc2Gz0aKzLVMYC1V1PL/ky+aY/NgA== - -domutils@^2.5.2, domutils@^2.8.0: - version "2.8.0" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135" - integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A== - dependencies: - dom-serializer "^1.0.1" - domelementtype "^2.2.0" - domhandler "^4.2.0" - -domutils@^3.0.1: - version "3.1.0" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.1.0.tgz#c47f551278d3dc4b0b1ab8cbb42d751a6f0d824e" - integrity sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA== - dependencies: - dom-serializer "^2.0.0" - domelementtype "^2.3.0" - domhandler "^5.0.3" - -dot-case@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751" - integrity sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w== - dependencies: - no-case "^3.0.4" - tslib "^2.0.3" - -dot-prop@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-6.0.1.tgz#fc26b3cf142b9e59b74dbd39ed66ce620c681083" - integrity sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA== - dependencies: - is-obj "^2.0.0" - -duplexer@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" - integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== - -eastasianwidth@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" - integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== - -ee-first@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" - integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== - -electron-to-chromium@^1.4.796: - version "1.4.803" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.803.tgz#cf55808a5ee12e2a2778bbe8cdc941ef87c2093b" - integrity sha512-61H9mLzGOCLLVsnLiRzCbc63uldP0AniRYPV3hbGVtONA1pI7qSGILdbofR7A8TMbOypDocEAjH/e+9k1QIe3g== - -elkjs@^0.9.0: - version "0.9.3" - resolved "https://registry.yarnpkg.com/elkjs/-/elkjs-0.9.3.tgz#16711f8ceb09f1b12b99e971b138a8384a529161" - integrity sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ== - -elliptic@^6.5.3, elliptic@^6.5.5: - version "6.5.7" - resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.7.tgz#8ec4da2cb2939926a1b9a73619d768207e647c8b" - integrity sha512-ESVCtTwiA+XhY3wyh24QqRGBoP3rEdDUl3EDUUo9tft074fi19IrdpH7hLCMMP3CIj7jb3W96rn8lt/BqIlt5Q== - dependencies: - bn.js "^4.11.9" - brorand "^1.1.0" - hash.js "^1.0.0" - hmac-drbg "^1.0.1" - inherits "^2.0.4" - minimalistic-assert "^1.0.1" - minimalistic-crypto-utils "^1.0.1" - -emoji-regex@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" - integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== - -emoji-regex@^9.2.2: - version "9.2.2" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" - integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== - -emojilib@^2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/emojilib/-/emojilib-2.4.0.tgz#ac518a8bb0d5f76dda57289ccb2fdf9d39ae721e" - integrity sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw== - -emojis-list@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" - integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== - -emoticon@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/emoticon/-/emoticon-4.0.1.tgz#2d2bbbf231ce3a5909e185bbb64a9da703a1e749" - integrity sha512-dqx7eA9YaqyvYtUhJwT4rC1HIp82j5ybS1/vQ42ur+jBe17dJMwZE4+gvL1XadSFfxaPFFGt3Xsw+Y8akThDlw== - -encodeurl@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" - integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== - -end-of-stream@^1.1.0, end-of-stream@^1.4.1: - version "1.4.4" - resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" - integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== - dependencies: - once "^1.4.0" - -enhanced-resolve@^5.17.0: - version "5.17.0" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.17.0.tgz#d037603789dd9555b89aaec7eb78845c49089bc5" - integrity sha512-dwDPwZL0dmye8Txp2gzFmA6sxALaSvdRDjPH0viLcKrtlOL3tw62nWWweVD1SdILDTJrbrL6tdWVN58Wo6U3eA== - dependencies: - graceful-fs "^4.2.4" - tapable "^2.2.0" - -entities@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" - integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== - -entities@^4.2.0, entities@^4.4.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" - integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== - -error-ex@^1.3.1: - version "1.3.2" - resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" - integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== - dependencies: - is-arrayish "^0.2.1" - -es-define-property@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" - integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== - dependencies: - get-intrinsic "^1.2.4" - -es-errors@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" - integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== - -es-module-lexer@^1.2.1: - version "1.5.3" - resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.5.3.tgz#25969419de9c0b1fbe54279789023e8a9a788412" - integrity sha512-i1gCgmR9dCl6Vil6UKPI/trA69s08g/syhiDK9TG0Nf1RJjjFI+AzoWW7sPufzkgYAn861skuCwJa0pIIHYxvg== - -es6-promise@^3.2.1: - version "3.3.1" - resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.3.1.tgz#a08cdde84ccdbf34d027a1451bc91d4bcd28a613" - integrity sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg== - -escalade@^3.1.1, escalade@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27" - integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA== - -escape-goat@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/escape-goat/-/escape-goat-4.0.0.tgz#9424820331b510b0666b98f7873fe11ac4aa8081" - integrity sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg== - -escape-html@^1.0.3, escape-html@~1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" - integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== - -escape-string-regexp@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== - -escape-string-regexp@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" - integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== - -escape-string-regexp@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz#4683126b500b61762f2dbebace1806e8be31b1c8" - integrity sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw== - -eslint-scope@5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" - integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== - dependencies: - esrecurse "^4.3.0" - estraverse "^4.1.1" - -esprima@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" - integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== - -esrecurse@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" - integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== - dependencies: - estraverse "^5.2.0" - -estraverse@^4.1.1: - version "4.3.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" - integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== - -estraverse@^5.2.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" - integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== - -estree-util-attach-comments@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/estree-util-attach-comments/-/estree-util-attach-comments-2.1.1.tgz#ee44f4ff6890ee7dfb3237ac7810154c94c63f84" - integrity sha512-+5Ba/xGGS6mnwFbXIuQiDPTbuTxuMCooq3arVv7gPZtYpjp+VXH/NkHAP35OOefPhNG/UGqU3vt/LTABwcHX0w== - dependencies: - "@types/estree" "^1.0.0" - -estree-util-attach-comments@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/estree-util-attach-comments/-/estree-util-attach-comments-3.0.0.tgz#344bde6a64c8a31d15231e5ee9e297566a691c2d" - integrity sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw== - dependencies: - "@types/estree" "^1.0.0" - -estree-util-build-jsx@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/estree-util-build-jsx/-/estree-util-build-jsx-3.0.1.tgz#b6d0bced1dcc4f06f25cf0ceda2b2dcaf98168f1" - integrity sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ== - dependencies: - "@types/estree-jsx" "^1.0.0" - devlop "^1.0.0" - estree-util-is-identifier-name "^3.0.0" - estree-walker "^3.0.0" - -estree-util-is-identifier-name@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/estree-util-is-identifier-name/-/estree-util-is-identifier-name-2.1.0.tgz#fb70a432dcb19045e77b05c8e732f1364b4b49b2" - integrity sha512-bEN9VHRyXAUOjkKVQVvArFym08BTWB0aJPppZZr0UNyAqWsLaVfAqP7hbaTJjzHifmB5ebnR8Wm7r7yGN/HonQ== - -estree-util-is-identifier-name@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz#0b5ef4c4ff13508b34dcd01ecfa945f61fce5dbd" - integrity sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg== - -estree-util-to-js@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/estree-util-to-js/-/estree-util-to-js-1.2.0.tgz#0f80d42443e3b13bd32f7012fffa6f93603f4a36" - integrity sha512-IzU74r1PK5IMMGZXUVZbmiu4A1uhiPgW5hm1GjcOfr4ZzHaMPpLNJjR7HjXiIOzi25nZDrgFTobHTkV5Q6ITjA== - dependencies: - "@types/estree-jsx" "^1.0.0" - astring "^1.8.0" - source-map "^0.7.0" - -estree-util-to-js@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/estree-util-to-js/-/estree-util-to-js-2.0.0.tgz#10a6fb924814e6abb62becf0d2bc4dea51d04f17" - integrity sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg== - dependencies: - "@types/estree-jsx" "^1.0.0" - astring "^1.8.0" - source-map "^0.7.0" - -estree-util-value-to-estree@^3.0.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/estree-util-value-to-estree/-/estree-util-value-to-estree-3.1.1.tgz#a007388eca677510f319603a2f279fed6d104a15" - integrity sha512-5mvUrF2suuv5f5cGDnDphIy4/gW86z82kl5qG6mM9z04SEQI4FB5Apmaw/TGEf3l55nLtMs5s51dmhUzvAHQCA== - dependencies: - "@types/estree" "^1.0.0" - is-plain-obj "^4.0.0" - -estree-util-visit@^1.0.0, estree-util-visit@^1.2.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/estree-util-visit/-/estree-util-visit-1.2.1.tgz#8bc2bc09f25b00827294703835aabee1cc9ec69d" - integrity sha512-xbgqcrkIVbIG+lI/gzbvd9SGTJL4zqJKBFttUl5pP27KhAjtMKbX/mQXJ7qgyXpMgVy/zvpm0xoQQaGL8OloOw== - dependencies: - "@types/estree-jsx" "^1.0.0" - "@types/unist" "^2.0.0" - -estree-util-visit@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/estree-util-visit/-/estree-util-visit-2.0.0.tgz#13a9a9f40ff50ed0c022f831ddf4b58d05446feb" - integrity sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww== - dependencies: - "@types/estree-jsx" "^1.0.0" - "@types/unist" "^3.0.0" - -estree-walker@^3.0.0: - version "3.0.3" - resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-3.0.3.tgz#67c3e549ec402a487b4fc193d1953a524752340d" - integrity sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g== - dependencies: - "@types/estree" "^1.0.0" - -esutils@^2.0.2: - version "2.0.3" - resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" - integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== - -eta@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/eta/-/eta-2.2.0.tgz#eb8b5f8c4e8b6306561a455e62cd7492fe3a9b8a" - integrity sha512-UVQ72Rqjy/ZKQalzV5dCCJP80GrmPrMxh6NlNf+erV6ObL0ZFkhCstWRawS85z3smdr3d2wXPsZEY7rDPfGd2g== - -etag@~1.8.1: - version "1.8.1" - resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" - integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== - -eval@^0.1.8: - version "0.1.8" - resolved "https://registry.yarnpkg.com/eval/-/eval-0.1.8.tgz#2b903473b8cc1d1989b83a1e7923f883eb357f85" - integrity sha512-EzV94NYKoO09GLXGjXj9JIlXijVck4ONSr5wiCWDvhsvj5jxSrzTmRU/9C1DyB6uToszLs8aifA6NQ7lEQdvFw== - dependencies: - "@types/node" "*" - require-like ">= 0.1.1" - -event-target-shim@^5.0.0: - version "5.0.1" - resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" - integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== - -eventemitter3@^4.0.0: - version "4.0.7" - resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" - integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== - -events@^3.2.0, events@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" - integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== - -evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02" - integrity sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA== - dependencies: - md5.js "^1.3.4" - safe-buffer "^5.1.1" - -execa@^5.0.0: - version "5.1.1" - resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" - integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== - 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" - -exenv@^1.2.0: - version "1.2.2" - resolved "https://registry.yarnpkg.com/exenv/-/exenv-1.2.2.tgz#2ae78e85d9894158670b03d47bec1f03bd91bb9d" - integrity sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw== - -express@^4.17.3: - version "4.19.2" - resolved "https://registry.yarnpkg.com/express/-/express-4.19.2.tgz#e25437827a3aa7f2a827bc8171bbbb664a356465" - integrity sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q== - dependencies: - accepts "~1.3.8" - array-flatten "1.1.1" - body-parser "1.20.2" - content-disposition "0.5.4" - content-type "~1.0.4" - cookie "0.6.0" - cookie-signature "1.0.6" - debug "2.6.9" - depd "2.0.0" - encodeurl "~1.0.2" - escape-html "~1.0.3" - etag "~1.8.1" - finalhandler "1.2.0" - fresh "0.5.2" - http-errors "2.0.0" - merge-descriptors "1.0.1" - methods "~1.1.2" - on-finished "2.4.1" - parseurl "~1.3.3" - path-to-regexp "0.1.7" - proxy-addr "~2.0.7" - qs "6.11.0" - range-parser "~1.2.1" - safe-buffer "5.2.1" - send "0.18.0" - serve-static "1.15.0" - setprototypeof "1.2.0" - statuses "2.0.1" - type-is "~1.6.18" - utils-merge "1.0.1" - vary "~1.1.2" - -extend-shallow@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" - integrity sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug== - dependencies: - is-extendable "^0.1.0" - -extend@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" - integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== - -extract-zip@2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a" - integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg== - dependencies: - debug "^4.1.1" - get-stream "^5.1.0" - yauzl "^2.10.0" - optionalDependencies: - "@types/yauzl" "^2.9.1" - -faker@5.5.3: - version "5.5.3" - resolved "https://registry.yarnpkg.com/faker/-/faker-5.5.3.tgz#c57974ee484431b25205c2c8dc09fda861e51e0e" - integrity sha512-wLTv2a28wjUyWkbnX7u/ABZBkUkIF2fCd73V6P2oFqEGEktDfzWx4UxrSqtPRw0xPRAcjeAOIiJWqZm3pP4u3g== - -fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" - integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== - -fast-glob@^3.2.11, fast-glob@^3.2.9, fast-glob@^3.3.0: - version "3.3.2" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" - integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== - dependencies: - "@nodelib/fs.stat" "^2.0.2" - "@nodelib/fs.walk" "^1.2.3" - glob-parent "^5.1.2" - merge2 "^1.3.0" - micromatch "^4.0.4" - -fast-json-stable-stringify@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" - integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== - -fast-safe-stringify@^2.0.7: - version "2.1.1" - resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" - integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== - -fast-url-parser@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/fast-url-parser/-/fast-url-parser-1.1.3.tgz#f4af3ea9f34d8a271cf58ad2b3759f431f0b318d" - integrity sha512-5jOCVXADYNuRkKFzNJ0dCCewsZiYo0dz8QNYljkOpFC6r2U4OBmKtvm/Tsuh4w1YYdDqDb31a8TVhBJ2OJKdqQ== - dependencies: - punycode "^1.3.2" - -fastq@^1.6.0: - version "1.17.1" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47" - integrity sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w== - dependencies: - reusify "^1.0.4" - -fault@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/fault/-/fault-2.0.1.tgz#d47ca9f37ca26e4bd38374a7c500b5a384755b6c" - integrity sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ== - dependencies: - format "^0.2.0" - -faye-websocket@^0.11.3: - version "0.11.4" - resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.4.tgz#7f0d9275cfdd86a1c963dc8b65fcc451edcbb1da" - integrity sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g== - dependencies: - websocket-driver ">=0.5.1" - -fd-slicer@~1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" - integrity sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g== - dependencies: - pend "~1.2.0" - -feed@^4.2.2: - version "4.2.2" - resolved "https://registry.yarnpkg.com/feed/-/feed-4.2.2.tgz#865783ef6ed12579e2c44bbef3c9113bc4956a7e" - integrity sha512-u5/sxGfiMfZNtJ3OvQpXcvotFpYkL0n9u9mM2vkui2nGo8b4wvDkJ8gAkYqbA8QpGyFCv3RK0Z+Iv+9veCS9bQ== - dependencies: - xml-js "^1.6.11" - -file-loader@^6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-6.2.0.tgz#baef7cf8e1840df325e4390b4484879480eebe4d" - integrity sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw== - dependencies: - loader-utils "^2.0.0" - schema-utils "^3.0.0" - -file-saver@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/file-saver/-/file-saver-2.0.5.tgz#d61cfe2ce059f414d899e9dd6d4107ee25670c38" - integrity sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA== - -file-type@3.9.0: - version "3.9.0" - resolved "https://registry.yarnpkg.com/file-type/-/file-type-3.9.0.tgz#257a078384d1db8087bc449d107d52a52672b9e9" - integrity sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA== - -filesize@^8.0.6: - version "8.0.7" - resolved "https://registry.yarnpkg.com/filesize/-/filesize-8.0.7.tgz#695e70d80f4e47012c132d57a059e80c6b580bd8" - integrity sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ== - -fill-range@^7.1.1: - version "7.1.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" - integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== - dependencies: - to-regex-range "^5.0.1" - -filter-obj@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/filter-obj/-/filter-obj-2.0.2.tgz#fff662368e505d69826abb113f0f6a98f56e9d5f" - integrity sha512-lO3ttPjHZRfjMcxWKb1j1eDhTFsu4meeR3lnMcnBFhk6RuLhvEiuALu2TlfL310ph4lCYYwgF/ElIjdP739tdg== - -finalhandler@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" - integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== - dependencies: - debug "2.6.9" - encodeurl "~1.0.2" - escape-html "~1.0.3" - on-finished "2.4.1" - parseurl "~1.3.3" - statuses "2.0.1" - unpipe "~1.0.0" - -find-cache-dir@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-4.0.0.tgz#a30ee0448f81a3990708f6453633c733e2f6eec2" - integrity sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg== - dependencies: - common-path-prefix "^3.0.0" - pkg-dir "^7.0.0" - -find-up@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" - integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== - dependencies: - locate-path "^3.0.0" - -find-up@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" - integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== - dependencies: - locate-path "^6.0.0" - path-exists "^4.0.0" - -find-up@^6.3.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-6.3.0.tgz#2abab3d3280b2dc7ac10199ef324c4e002c8c790" - integrity sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw== - dependencies: - locate-path "^7.1.0" - path-exists "^5.0.0" - -flat@^5.0.2: - version "5.0.2" - resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" - integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== - -follow-redirects@^1.0.0, follow-redirects@^1.14.7: - version "1.15.6" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" - integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== - -for-each@^0.3.3: - version "0.3.3" - resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" - integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== - dependencies: - is-callable "^1.1.3" - -foreach@^2.0.4: - version "2.0.6" - resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.6.tgz#87bcc8a1a0e74000ff2bf9802110708cfb02eb6e" - integrity sha512-k6GAGDyqLe9JaebCsFCoudPPWfihKu8pylYXRlqP1J7ms39iPoTtk2fviNglIeQEwdh0bQeKJ01ZPyuyQvKzwg== - -foreground-child@^3.1.0: - version "3.2.1" - resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.2.1.tgz#767004ccf3a5b30df39bed90718bab43fe0a59f7" - integrity sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA== - dependencies: - cross-spawn "^7.0.0" - signal-exit "^4.0.1" - -fork-ts-checker-webpack-plugin@^6.5.0: - version "6.5.3" - resolved "https://registry.yarnpkg.com/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.3.tgz#eda2eff6e22476a2688d10661688c47f611b37f3" - integrity sha512-SbH/l9ikmMWycd5puHJKTkZJKddF4iRLyW3DeZ08HTI7NGyLS38MXd/KGgeWumQO7YNQbW2u/NtPT2YowbPaGQ== - dependencies: - "@babel/code-frame" "^7.8.3" - "@types/json-schema" "^7.0.5" - chalk "^4.1.0" - chokidar "^3.4.2" - cosmiconfig "^6.0.0" - deepmerge "^4.2.2" - fs-extra "^9.0.0" - glob "^7.1.6" - memfs "^3.1.2" - minimatch "^3.0.4" - schema-utils "2.7.0" - semver "^7.3.2" - tapable "^1.0.0" - -form-data-encoder@^2.1.2: - version "2.1.4" - resolved "https://registry.yarnpkg.com/form-data-encoder/-/form-data-encoder-2.1.4.tgz#261ea35d2a70d48d30ec7a9603130fa5515e9cd5" - integrity sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw== - -format@^0.2.0: - version "0.2.2" - resolved "https://registry.yarnpkg.com/format/-/format-0.2.2.tgz#d6170107e9efdc4ed30c9dc39016df942b5cb58b" - integrity sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww== - -forwarded@0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" - integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== - -fraction.js@^4.3.7: - version "4.3.7" - resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7" - integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew== - -fresh@0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" - integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== - -fs-constants@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" - integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== - -fs-extra@^11.1.1, fs-extra@^11.2.0: - version "11.2.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.2.0.tgz#e70e17dfad64232287d01929399e0ea7c86b0e5b" - integrity sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw== - dependencies: - graceful-fs "^4.2.0" - jsonfile "^6.0.1" - universalify "^2.0.0" - -fs-extra@^9.0.0, fs-extra@^9.0.1: - version "9.1.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" - integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== - dependencies: - at-least-node "^1.0.0" - graceful-fs "^4.2.0" - jsonfile "^6.0.1" - universalify "^2.0.0" - -fs-monkey@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/fs-monkey/-/fs-monkey-1.0.6.tgz#8ead082953e88d992cf3ff844faa907b26756da2" - integrity sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg== - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== - -fsevents@~2.3.2: - version "2.3.3" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" - integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== - -function-bind@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" - integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== - -gensync@^1.0.0-beta.2: - version "1.0.0-beta.2" - resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" - integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== - -get-caller-file@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" - integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== - -get-intrinsic@^1.1.3, get-intrinsic@^1.2.4: - version "1.2.4" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" - integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== - dependencies: - es-errors "^1.3.0" - function-bind "^1.1.2" - has-proto "^1.0.1" - has-symbols "^1.0.3" - hasown "^2.0.0" - -get-own-enumerable-property-symbols@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664" - integrity sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g== - -get-stream@^5.1.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" - integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== - dependencies: - pump "^3.0.0" - -get-stream@^6.0.0, get-stream@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" - integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== - -github-slugger@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/github-slugger/-/github-slugger-1.5.0.tgz#17891bbc73232051474d68bd867a34625c955f7d" - integrity sha512-wIh+gKBI9Nshz2o46B0B3f5k/W+WI9ZAv6y5Dn5WJ5SK1t0TnDimB4WE5rmTD05ZAIn8HALCZVmCsvj0w0v0lw== - -glob-parent@^5.1.2, glob-parent@~5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" - integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== - dependencies: - is-glob "^4.0.1" - -glob-parent@^6.0.1, glob-parent@^6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" - integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== - dependencies: - is-glob "^4.0.3" - -glob-to-regexp@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" - integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== - -glob@^10.3.10: - version "10.4.1" - resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.1.tgz#0cfb01ab6a6b438177bfe6a58e2576f6efe909c2" - integrity sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw== - dependencies: - foreground-child "^3.1.0" - jackspeak "^3.1.2" - minimatch "^9.0.4" - minipass "^7.1.2" - path-scurry "^1.11.1" - -glob@^7.0.0, glob@^7.1.3, glob@^7.1.6: - version "7.2.3" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" - integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== - 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" - -global-dirs@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-3.0.1.tgz#0c488971f066baceda21447aecb1a8b911d22485" - integrity sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA== - dependencies: - ini "2.0.0" - -global-modules@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780" - integrity sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A== - dependencies: - global-prefix "^3.0.0" - -global-prefix@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-3.0.0.tgz#fc85f73064df69f50421f47f883fe5b913ba9b97" - integrity sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg== - dependencies: - ini "^1.3.5" - kind-of "^6.0.2" - which "^1.3.1" - -globals@^11.1.0: - version "11.12.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" - integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== - -globby@^11.0.1, globby@^11.0.4, globby@^11.1.0: - version "11.1.0" - resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" - integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== - 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" - -globby@^13.1.1: - version "13.2.2" - resolved "https://registry.yarnpkg.com/globby/-/globby-13.2.2.tgz#63b90b1bf68619c2135475cbd4e71e66aa090592" - integrity sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w== - dependencies: - dir-glob "^3.0.1" - fast-glob "^3.3.0" - ignore "^5.2.4" - merge2 "^1.4.1" - slash "^4.0.0" - -gopd@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" - integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== - dependencies: - get-intrinsic "^1.1.3" - -got@^12.1.0: - version "12.6.1" - resolved "https://registry.yarnpkg.com/got/-/got-12.6.1.tgz#8869560d1383353204b5a9435f782df9c091f549" - integrity sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ== - dependencies: - "@sindresorhus/is" "^5.2.0" - "@szmarczak/http-timer" "^5.0.1" - cacheable-lookup "^7.0.0" - cacheable-request "^10.2.8" - decompress-response "^6.0.0" - form-data-encoder "^2.1.2" - get-stream "^6.0.1" - http2-wrapper "^2.1.10" - lowercase-keys "^3.0.0" - p-cancelable "^3.0.0" - responselike "^3.0.0" - -graceful-fs@4.2.10: - version "4.2.10" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" - integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== - -graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: - version "4.2.11" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" - integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== - -graphlib@2.1.8: - version "2.1.8" - resolved "https://registry.yarnpkg.com/graphlib/-/graphlib-2.1.8.tgz#5761d414737870084c92ec7b5dbcb0592c9d35da" - integrity sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A== - dependencies: - lodash "^4.17.15" - -gray-matter@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/gray-matter/-/gray-matter-4.0.3.tgz#e893c064825de73ea1f5f7d88c7a9f7274288798" - 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" - -gzip-size@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-6.0.0.tgz#065367fd50c239c0671cbcbad5be3e2eeb10e462" - integrity sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q== - dependencies: - duplexer "^0.1.2" - -handle-thing@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e" - integrity sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg== - -has-flag@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" - integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== - -has-flag@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" - integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== - -has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" - integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== - dependencies: - es-define-property "^1.0.0" - -has-proto@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" - integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== - -has-symbols@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" - integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== - -has-tostringtag@^1.0.0, has-tostringtag@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" - integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== - dependencies: - has-symbols "^1.0.3" - -has-yarn@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/has-yarn/-/has-yarn-3.0.0.tgz#c3c21e559730d1d3b57e28af1f30d06fac38147d" - integrity sha512-IrsVwUHhEULx3R8f/aA8AHuEzAorplsab/v8HBzEiIukwq5i/EC+xmOW+HfP1OaDP+2JkgT1yILHN2O3UFIbcA== - -hash-base@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.1.0.tgz#55c381d9e06e1d2997a883b4a3fddfe7f0d3af33" - integrity sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA== - dependencies: - inherits "^2.0.4" - readable-stream "^3.6.0" - safe-buffer "^5.2.0" - -hash-base@~3.0: - version "3.0.4" - resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.0.4.tgz#5fc8686847ecd73499403319a6b0a3f3f6ae4918" - integrity sha512-EeeoJKjTyt868liAlVmcv2ZsUfGHlE3Q+BICOXcZiwN3osr5Q/zFGYmTJpoIzuaSTAwndFy+GqhEwlU4L3j4Ow== - dependencies: - inherits "^2.0.1" - safe-buffer "^5.0.1" - -hash.js@^1.0.0, hash.js@^1.0.3: - version "1.1.7" - resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" - integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA== - dependencies: - inherits "^2.0.3" - minimalistic-assert "^1.0.1" - -hasown@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" - integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== - dependencies: - function-bind "^1.1.2" - -hast-util-from-html@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/hast-util-from-html/-/hast-util-from-html-1.0.2.tgz#2482fd701b2d8270b912b3909d6fb645d4a346cf" - integrity sha512-LhrTA2gfCbLOGJq2u/asp4kwuG0y6NhWTXiPKP+n0qNukKy7hc10whqqCFfyvIA1Q5U5d0sp9HhNim9gglEH4A== - dependencies: - "@types/hast" "^2.0.0" - hast-util-from-parse5 "^7.0.0" - parse5 "^7.0.0" - vfile "^5.0.0" - vfile-message "^3.0.0" - -hast-util-from-parse5@^7.0.0: - version "7.1.2" - resolved "https://registry.yarnpkg.com/hast-util-from-parse5/-/hast-util-from-parse5-7.1.2.tgz#aecfef73e3ceafdfa4550716443e4eb7b02e22b0" - integrity sha512-Nz7FfPBuljzsN3tCQ4kCBKqdNhQE2l0Tn+X1ubgKBPRoiDIu1mL08Cfw4k7q71+Duyaw7DXDN+VTAp4Vh3oCOw== - dependencies: - "@types/hast" "^2.0.0" - "@types/unist" "^2.0.0" - hastscript "^7.0.0" - property-information "^6.0.0" - vfile "^5.0.0" - vfile-location "^4.0.0" - web-namespaces "^2.0.0" - -hast-util-from-parse5@^8.0.0: - version "8.0.1" - resolved "https://registry.yarnpkg.com/hast-util-from-parse5/-/hast-util-from-parse5-8.0.1.tgz#654a5676a41211e14ee80d1b1758c399a0327651" - integrity sha512-Er/Iixbc7IEa7r/XLtuG52zoqn/b3Xng/w6aZQ0xGVxzhw5xUFxcRqdPzP6yFi/4HBYRaifaI5fQ1RH8n0ZeOQ== - dependencies: - "@types/hast" "^3.0.0" - "@types/unist" "^3.0.0" - devlop "^1.0.0" - hastscript "^8.0.0" - property-information "^6.0.0" - vfile "^6.0.0" - vfile-location "^5.0.0" - web-namespaces "^2.0.0" - -hast-util-parse-selector@^3.0.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/hast-util-parse-selector/-/hast-util-parse-selector-3.1.1.tgz#25ab00ae9e75cbc62cf7a901f68a247eade659e2" - integrity sha512-jdlwBjEexy1oGz0aJ2f4GKMaVKkA9jwjr4MjAAI22E5fM/TXVZHuS5OpONtdeIkRKqAaryQ2E9xNQxijoThSZA== - dependencies: - "@types/hast" "^2.0.0" - -hast-util-parse-selector@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz#352879fa86e25616036037dd8931fb5f34cb4a27" - integrity sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A== - dependencies: - "@types/hast" "^3.0.0" - -hast-util-raw@^7.2.0: - version "7.2.3" - resolved "https://registry.yarnpkg.com/hast-util-raw/-/hast-util-raw-7.2.3.tgz#dcb5b22a22073436dbdc4aa09660a644f4991d99" - integrity sha512-RujVQfVsOrxzPOPSzZFiwofMArbQke6DJjnFfceiEbFh7S05CbPt0cYN+A5YeD3pso0JQk6O1aHBnx9+Pm2uqg== - dependencies: - "@types/hast" "^2.0.0" - "@types/parse5" "^6.0.0" - hast-util-from-parse5 "^7.0.0" - hast-util-to-parse5 "^7.0.0" - html-void-elements "^2.0.0" - parse5 "^6.0.0" - unist-util-position "^4.0.0" - unist-util-visit "^4.0.0" - vfile "^5.0.0" - web-namespaces "^2.0.0" - zwitch "^2.0.0" - -hast-util-raw@^9.0.0: - version "9.0.3" - resolved "https://registry.yarnpkg.com/hast-util-raw/-/hast-util-raw-9.0.3.tgz#87ad66bdd7b1ceb166452bdab7dfb3e9ba640419" - integrity sha512-ICWvVOF2fq4+7CMmtCPD5CM4QKjPbHpPotE6+8tDooV0ZuyJVUzHsrNX+O5NaRbieTf0F7FfeBOMAwi6Td0+yQ== - 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" - -hast-util-to-estree@^2.1.0: - version "2.3.3" - resolved "https://registry.yarnpkg.com/hast-util-to-estree/-/hast-util-to-estree-2.3.3.tgz#da60142ffe19a6296923ec222aba73339c8bf470" - integrity sha512-ihhPIUPxN0v0w6M5+IiAZZrn0LH2uZomeWwhn7uP7avZC6TE7lIiEh2yBMPr5+zi1aUCXq6VoYRgs2Bw9xmycQ== - dependencies: - "@types/estree" "^1.0.0" - "@types/estree-jsx" "^1.0.0" - "@types/hast" "^2.0.0" - "@types/unist" "^2.0.0" - comma-separated-tokens "^2.0.0" - estree-util-attach-comments "^2.0.0" - estree-util-is-identifier-name "^2.0.0" - hast-util-whitespace "^2.0.0" - mdast-util-mdx-expression "^1.0.0" - mdast-util-mdxjs-esm "^1.0.0" - property-information "^6.0.0" - space-separated-tokens "^2.0.0" - style-to-object "^0.4.1" - unist-util-position "^4.0.0" - zwitch "^2.0.0" - -hast-util-to-estree@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/hast-util-to-estree/-/hast-util-to-estree-3.1.0.tgz#f2afe5e869ddf0cf690c75f9fc699f3180b51b19" - integrity sha512-lfX5g6hqVh9kjS/B9E2gSkvHH4SZNiQFiqWS0x9fENzEl+8W12RqdRxX6d/Cwxi30tPQs3bIO+aolQJNp1bIyw== - dependencies: - "@types/estree" "^1.0.0" - "@types/estree-jsx" "^1.0.0" - "@types/hast" "^3.0.0" - comma-separated-tokens "^2.0.0" - devlop "^1.0.0" - estree-util-attach-comments "^3.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 "^6.0.0" - space-separated-tokens "^2.0.0" - style-to-object "^0.4.0" - unist-util-position "^5.0.0" - zwitch "^2.0.0" - -hast-util-to-jsx-runtime@^2.0.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.0.tgz#3ed27caf8dc175080117706bf7269404a0aa4f7c" - integrity sha512-H/y0+IWPdsLLS738P8tDnrQ8Z+dj12zQQ6WC11TIM21C8WFVoIxcqWXf2H3hiTVZjF1AWqoimGwrTWecWrnmRQ== - 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 "^6.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" - -hast-util-to-parse5@^7.0.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/hast-util-to-parse5/-/hast-util-to-parse5-7.1.0.tgz#c49391bf8f151973e0c9adcd116b561e8daf29f3" - integrity sha512-YNRgAJkH2Jky5ySkIqFXTQiaqcAtJyVE+D5lkN6CdtOqrnkLfGYYrEcKuHOJZlp+MwjSwuD3fZuawI+sic/RBw== - dependencies: - "@types/hast" "^2.0.0" - comma-separated-tokens "^2.0.0" - property-information "^6.0.0" - space-separated-tokens "^2.0.0" - web-namespaces "^2.0.0" - zwitch "^2.0.0" - -hast-util-to-parse5@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/hast-util-to-parse5/-/hast-util-to-parse5-8.0.0.tgz#477cd42d278d4f036bc2ea58586130f6f39ee6ed" - 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" - -hast-util-whitespace@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/hast-util-whitespace/-/hast-util-whitespace-2.0.1.tgz#0ec64e257e6fc216c7d14c8a1b74d27d650b4557" - integrity sha512-nAxA0v8+vXSBDt3AnRUNjyRIQ0rD+ntpbAp4LnPkumc5M9yUbSMa4XDU9Q6etY4f1Wp4bNgvc1yjiZtsTTrSng== - -hast-util-whitespace@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz#7778ed9d3c92dd9e8c5c8f648a49c21fc51cb621" - integrity sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw== - dependencies: - "@types/hast" "^3.0.0" - -hastscript@^7.0.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/hastscript/-/hastscript-7.2.0.tgz#0eafb7afb153d047077fa2a833dc9b7ec604d10b" - integrity sha512-TtYPq24IldU8iKoJQqvZOuhi5CyCQRAbvDOX0x1eW6rsHSxa/1i2CCiptNTotGHJ3VoHRGmqiv6/D3q113ikkw== - dependencies: - "@types/hast" "^2.0.0" - comma-separated-tokens "^2.0.0" - hast-util-parse-selector "^3.0.0" - property-information "^6.0.0" - space-separated-tokens "^2.0.0" - -hastscript@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/hastscript/-/hastscript-8.0.0.tgz#4ef795ec8dee867101b9f23cc830d4baf4fd781a" - integrity sha512-dMOtzCEd3ABUeSIISmrETiKuyydk1w0pa+gE/uormcTpSYuaNJPbX1NU3JLyscSLjwAQM8bWMhhIlnCqnRvDTw== - dependencies: - "@types/hast" "^3.0.0" - comma-separated-tokens "^2.0.0" - hast-util-parse-selector "^4.0.0" - property-information "^6.0.0" - space-separated-tokens "^2.0.0" - -he@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" - integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== - -history@^4.9.0: - version "4.10.1" - resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3" - integrity sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew== - dependencies: - "@babel/runtime" "^7.1.2" - loose-envify "^1.2.0" - resolve-pathname "^3.0.0" - tiny-invariant "^1.0.2" - tiny-warning "^1.0.0" - value-equal "^1.0.1" - -hmac-drbg@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" - integrity sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg== - dependencies: - hash.js "^1.0.3" - minimalistic-assert "^1.0.0" - minimalistic-crypto-utils "^1.0.1" - -hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: - version "3.3.2" - resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" - integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== - dependencies: - react-is "^16.7.0" - -hpack.js@^2.1.6: - version "2.1.6" - resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2" - integrity sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ== - dependencies: - inherits "^2.0.1" - obuf "^1.0.0" - readable-stream "^2.0.1" - wbuf "^1.1.0" - -html-entities@^2.3.2: - version "2.5.2" - resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.5.2.tgz#201a3cf95d3a15be7099521620d19dfb4f65359f" - integrity sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA== - -html-escaper@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" - integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== - -html-minifier-terser@^6.0.2: - version "6.1.0" - resolved "https://registry.yarnpkg.com/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz#bfc818934cc07918f6b3669f5774ecdfd48f32ab" - integrity sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw== - 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" - -html-minifier-terser@^7.2.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/html-minifier-terser/-/html-minifier-terser-7.2.0.tgz#18752e23a2f0ed4b0f550f217bb41693e975b942" - integrity sha512-tXgn3QfqPIpGl9o+K5tpcj3/MN4SfLtsx2GWwBC3SSd0tXQGyF3gsSqad8loJgKZGM3ZxbYDd5yhiBIdWpmvLA== - dependencies: - camel-case "^4.1.2" - clean-css "~5.3.2" - commander "^10.0.0" - entities "^4.4.0" - param-case "^3.0.4" - relateurl "^0.2.7" - terser "^5.15.1" - -html-tags@^3.3.1: - version "3.3.1" - resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.3.1.tgz#a04026a18c882e4bba8a01a3d39cfe465d40b5ce" - integrity sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ== - -html-void-elements@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/html-void-elements/-/html-void-elements-2.0.1.tgz#29459b8b05c200b6c5ee98743c41b979d577549f" - integrity sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A== - -html-void-elements@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/html-void-elements/-/html-void-elements-3.0.0.tgz#fc9dbd84af9e747249034d4d62602def6517f1d7" - integrity sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg== - -html-webpack-plugin@^5.5.3: - version "5.6.0" - resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-5.6.0.tgz#50a8fa6709245608cb00e811eacecb8e0d7b7ea0" - integrity sha512-iwaY4wzbe48AfKLZ/Cc8k0L+FKG6oSNRaZ8x5A/T/IVDGyXcbHncM9TdDa93wn0FsSm82FhTKW7f3vS61thXAw== - 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" - -htmlparser2@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7" - integrity sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A== - dependencies: - domelementtype "^2.0.1" - domhandler "^4.0.0" - domutils "^2.5.2" - entities "^2.0.0" - -htmlparser2@^8.0.1: - version "8.0.2" - resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.2.tgz#f002151705b383e62433b5cf466f5b716edaec21" - integrity sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA== - dependencies: - domelementtype "^2.3.0" - domhandler "^5.0.3" - domutils "^3.0.1" - entities "^4.4.0" - -http-cache-semantics@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a" - integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ== - -http-deceiver@^1.2.7: - version "1.2.7" - resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" - integrity sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw== - -http-errors@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" - integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== - dependencies: - depd "2.0.0" - inherits "2.0.4" - setprototypeof "1.2.0" - statuses "2.0.1" - toidentifier "1.0.1" - -http-errors@~1.6.2: - version "1.6.3" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" - integrity sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A== - dependencies: - depd "~1.1.2" - inherits "2.0.3" - setprototypeof "1.1.0" - statuses ">= 1.4.0 < 2" - -http-parser-js@>=0.5.1: - version "0.5.8" - resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.8.tgz#af23090d9ac4e24573de6f6aecc9d84a48bf20e3" - integrity sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q== - -http-proxy-middleware@^2.0.3: - version "2.0.7" - resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz#915f236d92ae98ef48278a95dedf17e991936ec6" - integrity sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA== - dependencies: - "@types/http-proxy" "^1.17.8" - http-proxy "^1.18.1" - is-glob "^4.0.1" - is-plain-obj "^3.0.0" - micromatch "^4.0.2" - -http-proxy@^1.18.1: - version "1.18.1" - resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" - integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== - dependencies: - eventemitter3 "^4.0.0" - follow-redirects "^1.0.0" - requires-port "^1.0.0" - -http-reasons@0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/http-reasons/-/http-reasons-0.1.0.tgz#a953ca670078669dde142ce899401b9d6e85d3b4" - integrity sha512-P6kYh0lKZ+y29T2Gqz+RlC9WBLhKe8kDmcJ+A+611jFfxdPsbMRQ5aNmFRM3lENqFkK+HTTL+tlQviAiv0AbLQ== - -http2-client@^1.2.5: - version "1.3.5" - resolved "https://registry.yarnpkg.com/http2-client/-/http2-client-1.3.5.tgz#20c9dc909e3cc98284dd20af2432c524086df181" - integrity sha512-EC2utToWl4RKfs5zd36Mxq7nzHHBuomZboI0yYL6Y0RmBgT7Sgkq4rQ0ezFTYoIsSs7Tm9SJe+o2FcAg6GBhGA== - -http2-wrapper@^2.1.10: - version "2.2.1" - resolved "https://registry.yarnpkg.com/http2-wrapper/-/http2-wrapper-2.2.1.tgz#310968153dcdedb160d8b72114363ef5fce1f64a" - integrity sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ== - dependencies: - quick-lru "^5.1.1" - resolve-alpn "^1.2.0" - -https-browserify@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" - integrity sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg== - -https-proxy-agent@5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" - integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== - dependencies: - agent-base "6" - debug "4" - -human-signals@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" - integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== - -iconv-lite@0.4.24: - version "0.4.24" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" - integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== - dependencies: - safer-buffer ">= 2.1.2 < 3" - -iconv-lite@0.6, iconv-lite@0.6.3: - version "0.6.3" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" - integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== - dependencies: - safer-buffer ">= 2.1.2 < 3.0.0" - -icss-utils@^5.0.0, icss-utils@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae" - integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== - -ieee754@^1.1.13, ieee754@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" - integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== - -ignore@^5.2.0, ignore@^5.2.4: - version "5.3.1" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef" - integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw== - -image-size@^1.0.2: - version "1.2.1" - resolved "https://registry.yarnpkg.com/image-size/-/image-size-1.2.1.tgz#ee118aedfe666db1a6ee12bed5821cde3740276d" - integrity sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw== - dependencies: - queue "6.0.2" - -immer@^9.0.21, immer@^9.0.7: - version "9.0.21" - resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.21.tgz#1e025ea31a40f24fb064f1fef23e931496330176" - integrity sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA== - -immutable@^4.0.0: - version "4.3.6" - resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.6.tgz#6a05f7858213238e587fb83586ffa3b4b27f0447" - integrity sha512-Ju0+lEMyzMVZarkTn/gqRpdqd5dOPaz1mCZ0SH3JV6iFw81PldE/PEB1hWVEA288HPt4WXW8O7AWxB10M+03QQ== - -import-fresh@^3.1.0, import-fresh@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" - integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== - dependencies: - parent-module "^1.0.0" - resolve-from "^4.0.0" - -import-lazy@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-4.0.0.tgz#e8eb627483a0a43da3c03f3e35548be5cb0cc153" - integrity sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw== - -imurmurhash@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" - integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== - -indent-string@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" - integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== - -infima@0.2.0-alpha.43: - version "0.2.0-alpha.43" - resolved "https://registry.yarnpkg.com/infima/-/infima-0.2.0-alpha.43.tgz#f7aa1d7b30b6c08afef441c726bac6150228cbe0" - integrity sha512-2uw57LvUqW0rK/SWYnd/2rRfxNA5DDNOh33jxF7fy46VWoNhGxiUQyVZHbBMjQ33mQem0cjdDVwgWVAmlRfgyQ== - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3, inherits@~2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -inherits@2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" - integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw== - -ini@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ini/-/ini-2.0.0.tgz#e5fd556ecdd5726be978fa1001862eacb0a94bc5" - integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA== - -ini@^1.3.4, ini@^1.3.5, ini@~1.3.0: - version "1.3.8" - resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" - integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== - -inline-style-parser@0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/inline-style-parser/-/inline-style-parser-0.1.1.tgz#ec8a3b429274e9c0a1f1c4ffa9453a7fef72cea1" - integrity sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q== - -inline-style-parser@0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/inline-style-parser/-/inline-style-parser-0.2.3.tgz#e35c5fb45f3a83ed7849fe487336eb7efa25971c" - integrity sha512-qlD8YNDqyTKTyuITrDOffsl6Tdhv+UC4hcdAVuQsK4IMQ99nSgd1MIA/Q+jQYoh9r3hVUXhYh7urSRmXPkW04g== - -"internmap@1 - 2": - version "2.0.3" - resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009" - integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg== - -internmap@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/internmap/-/internmap-1.0.1.tgz#0017cc8a3b99605f0302f2b198d272e015e5df95" - integrity sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw== - -interpret@^1.0.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" - integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== - -invariant@^2.2.4: - version "2.2.4" - resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" - integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== - dependencies: - loose-envify "^1.0.0" - -ipaddr.js@1.9.1: - version "1.9.1" - resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" - integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== - -ipaddr.js@^2.0.1: - version "2.2.0" - resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.2.0.tgz#d33fa7bac284f4de7af949638c9d68157c6b92e8" - integrity sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA== - -is-alphabetical@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-2.0.1.tgz#01072053ea7c1036df3c7d19a6daaec7f19e789b" - integrity sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ== - -is-alphanumerical@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz#7c03fbe96e3e931113e57f964b0a368cc2dfd875" - integrity sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw== - dependencies: - is-alphabetical "^2.0.0" - is-decimal "^2.0.0" - -is-arguments@^1.0.4: - version "1.1.1" - resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" - integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== - dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" - -is-arrayish@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" - integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== - -is-binary-path@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" - integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== - dependencies: - binary-extensions "^2.0.0" - -is-buffer@^2.0.0: - version "2.0.5" - resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191" - integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== - -is-callable@^1.1.3: - version "1.2.7" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" - integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== - -is-ci@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-3.0.1.tgz#db6ecbed1bd659c43dac0f45661e7674103d1867" - integrity sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ== - dependencies: - ci-info "^3.2.0" - -is-core-module@^2.13.0: - version "2.13.1" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384" - integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw== - dependencies: - hasown "^2.0.0" - -is-decimal@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-2.0.1.tgz#9469d2dc190d0214fd87d78b78caecc0cc14eef7" - integrity sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A== - -is-docker@^2.0.0, is-docker@^2.1.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" - integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== - -is-extendable@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" - integrity sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw== - -is-extglob@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" - integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== - -is-fullwidth-code-point@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" - integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== - -is-generator-function@^1.0.7: - version "1.0.10" - resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72" - integrity sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A== - dependencies: - has-tostringtag "^1.0.0" - -is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: - version "4.0.3" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" - integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== - dependencies: - is-extglob "^2.1.1" - -is-hexadecimal@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz#86b5bf668fca307498d319dfc03289d781a90027" - integrity sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg== - -is-installed-globally@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.4.0.tgz#9a0fd407949c30f86eb6959ef1b7994ed0b7b520" - integrity sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ== - dependencies: - global-dirs "^3.0.0" - is-path-inside "^3.0.2" - -is-nan@^1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.3.2.tgz#043a54adea31748b55b6cd4e09aadafa69bd9e1d" - integrity sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w== - dependencies: - call-bind "^1.0.0" - define-properties "^1.1.3" - -is-npm@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-6.0.0.tgz#b59e75e8915543ca5d881ecff864077cba095261" - integrity sha512-JEjxbSmtPSt1c8XTkVrlujcXdKV1/tvuQ7GwKcAlyiVLeYFQ2VHat8xfrDJsIkhCdF/tZ7CiIR3sy141c6+gPQ== - -is-number@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" - integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== - -is-obj@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" - integrity sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg== - -is-obj@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" - integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== - -is-path-cwd@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-2.2.0.tgz#67d43b82664a7b5191fd9119127eb300048a9fdb" - integrity sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ== - -is-path-inside@^3.0.2: - version "3.0.3" - resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" - integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== - -is-plain-obj@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-3.0.0.tgz#af6f2ea14ac5a646183a5bbdb5baabbc156ad9d7" - integrity sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA== - -is-plain-obj@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz#d65025edec3657ce032fd7db63c97883eaed71f0" - integrity sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg== - -is-plain-object@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" - integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== - dependencies: - isobject "^3.0.1" - -is-reference@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/is-reference/-/is-reference-3.0.2.tgz#154747a01f45cd962404ee89d43837af2cba247c" - integrity sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg== - dependencies: - "@types/estree" "*" - -is-regexp@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-1.0.0.tgz#fd2d883545c46bac5a633e7b9a09e87fa2cb5069" - integrity sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA== - -is-root@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-root/-/is-root-2.1.0.tgz#809e18129cf1129644302a4f8544035d51984a9c" - integrity sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg== - -is-stream@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" - integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== - -is-typed-array@^1.1.3: - version "1.1.13" - resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.13.tgz#d6c5ca56df62334959322d7d7dd1cca50debe229" - integrity sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw== - dependencies: - which-typed-array "^1.1.14" - -is-typedarray@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" - integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== - -is-wsl@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" - integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== - dependencies: - is-docker "^2.0.0" - -is-yarn-global@^0.4.0: - version "0.4.1" - resolved "https://registry.yarnpkg.com/is-yarn-global/-/is-yarn-global-0.4.1.tgz#b312d902b313f81e4eaf98b6361ba2b45cd694bb" - integrity sha512-/kppl+R+LO5VmhYSEWARUFjodS25D68gvj8W7z0I7OWhUla5xWu8KL6CtB2V0R6yqhnRgbcaREMr4EEM6htLPQ== - -isarray@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" - integrity sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ== - -isarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== - -isexe@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== - -isobject@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" - integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== - -jackspeak@^3.1.2: - version "3.4.0" - resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.0.tgz#a75763ff36ad778ede6a156d8ee8b124de445b4a" - integrity sha512-JVYhQnN59LVPFCEcVa2C3CrEKYacvjRfqIQl+h8oi91aLYQVWRYbxjPcv1bUiUy/kLmQaANrYfNMCO3kuEDHfw== - dependencies: - "@isaacs/cliui" "^8.0.2" - optionalDependencies: - "@pkgjs/parseargs" "^0.11.0" - -jest-util@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.7.0.tgz#23c2b62bfb22be82b44de98055802ff3710fc0bc" - integrity sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA== - 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" - -jest-worker@^27.4.5: - version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0" - integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg== - dependencies: - "@types/node" "*" - merge-stream "^2.0.0" - supports-color "^8.0.0" - -jest-worker@^29.4.3: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-29.7.0.tgz#acad073acbbaeb7262bd5389e1bcf43e10058d4a" - integrity sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw== - dependencies: - "@types/node" "*" - jest-util "^29.7.0" - merge-stream "^2.0.0" - supports-color "^8.0.0" - -jiti@^1.20.0, jiti@^1.21.0: - version "1.21.6" - resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.6.tgz#6c7f7398dd4b3142767f9a168af2f317a428d268" - integrity sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w== - -joi@^17.6.0, joi@^17.9.2: - version "17.13.1" - resolved "https://registry.yarnpkg.com/joi/-/joi-17.13.1.tgz#9c7b53dc3b44dd9ae200255cc3b398874918a6ca" - integrity sha512-vaBlIKCyo4FCUtCm7Eu4QZd/q02bWcxfUO6YSXAZOWF6gzcLBeba8kwotUdYJjDLW8Cz8RywsSOqiNJZW0mNvg== - dependencies: - "@hapi/hoek" "^9.3.0" - "@hapi/topo" "^5.1.0" - "@sideway/address" "^4.1.5" - "@sideway/formula" "^3.0.1" - "@sideway/pinpoint" "^2.0.0" - -js-levenshtein@^1.1.6: - version "1.1.6" - resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.6.tgz#c6cee58eb3550372df8deb85fad5ce66ce01d59d" - integrity sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g== - -"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" - integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== - -js-yaml@4.1.0, js-yaml@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" - integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== - dependencies: - argparse "^2.0.1" - -js-yaml@^3.13.1: - version "3.14.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" - integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== - dependencies: - argparse "^1.0.7" - esprima "^4.0.0" - -jsesc@^2.5.1: - version "2.5.2" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" - integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== - -jsesc@~0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" - integrity sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA== - -json-buffer@3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" - integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== - -json-parse-even-better-errors@^2.3.0, json-parse-even-better-errors@^2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" - integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== - -json-pointer@^0.6.2: - version "0.6.2" - resolved "https://registry.yarnpkg.com/json-pointer/-/json-pointer-0.6.2.tgz#f97bd7550be5e9ea901f8c9264c9d436a22a93cd" - integrity sha512-vLWcKbOaXlO+jvRy4qNd+TI1QUPZzfJj1tpJ3vAXDych5XJf93ftpUKe5pKCrzyIIwgBJcOcCVRUfqQP25afBw== - dependencies: - foreach "^2.0.4" - -json-schema-compare@^0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/json-schema-compare/-/json-schema-compare-0.2.2.tgz#dd601508335a90c7f4cfadb6b2e397225c908e56" - integrity sha512-c4WYmDKyJXhs7WWvAWm3uIYnfyWFoIp+JEoX34rctVvEkMYCPGhXtvmFFXiffBbxfZsvQ0RNnV5H7GvDF5HCqQ== - dependencies: - lodash "^4.17.4" - -json-schema-merge-allof@0.8.1, json-schema-merge-allof@^0.8.1: - version "0.8.1" - resolved "https://registry.yarnpkg.com/json-schema-merge-allof/-/json-schema-merge-allof-0.8.1.tgz#ed2828cdd958616ff74f932830a26291789eaaf2" - integrity sha512-CTUKmIlPJbsWfzRRnOXz+0MjIqvnleIXwFTzz+t9T86HnYX/Rozria6ZVGLktAU9e+NygNljveP+yxqtQp/Q4w== - dependencies: - compute-lcm "^1.1.2" - json-schema-compare "^0.2.2" - lodash "^4.17.20" - -json-schema-traverse@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" - integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== - -json-schema-traverse@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" - integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== - -json5@^2.1.2, json5@^2.2.3: - version "2.2.3" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" - integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== - -jsonfile@^6.0.1: - version "6.1.0" - resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" - integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== - dependencies: - universalify "^2.0.0" - optionalDependencies: - graceful-fs "^4.1.6" - -katex@^0.16.9: - version "0.16.10" - resolved "https://registry.yarnpkg.com/katex/-/katex-0.16.10.tgz#6f81b71ac37ff4ec7556861160f53bc5f058b185" - integrity sha512-ZiqaC04tp2O5utMsl2TEZTXxa6WSC4yo0fv5ML++D3QZv/vx2Mct0mTlRx3O+uUkjfuAgOkzsCmq5MiUEsDDdA== - dependencies: - commander "^8.3.0" - -keyv@^4.5.3: - version "4.5.4" - resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" - integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== - dependencies: - json-buffer "3.0.1" - -khroma@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/khroma/-/khroma-2.1.0.tgz#45f2ce94ce231a437cf5b63c2e886e6eb42bbbb1" - integrity sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw== - -kind-of@^6.0.0, kind-of@^6.0.2: - version "6.0.3" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" - integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== - -kleur@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" - integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== - -kleur@^4.0.3: - version "4.1.5" - resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.5.tgz#95106101795f7050c6c650f350c683febddb1780" - integrity sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ== - -klona@^2.0.4: - version "2.0.6" - resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.6.tgz#85bffbf819c03b2f53270412420a4555ef882e22" - integrity sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA== - -latest-version@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-7.0.0.tgz#843201591ea81a4d404932eeb61240fe04e9e5da" - integrity sha512-KvNT4XqAMzdcL6ka6Tl3i2lYeFDgXNCuIX+xNx6ZMVR1dFq+idXd9FLKNMOIx0t9mJ9/HudyX4oZWXZQ0UJHeg== - dependencies: - package-json "^8.1.0" - -launch-editor@^2.6.0: - version "2.7.0" - resolved "https://registry.yarnpkg.com/launch-editor/-/launch-editor-2.7.0.tgz#53ba12b3eb131edefee99acaef7850c40272273f" - integrity sha512-KAc66u6LxWL8MifQ94oG3YGKYWDwz/Gi0T15lN//GaQoZe08vQGFJxrXkPAeu50UXgvJPPaRKVGuP1TRUm/aHQ== - dependencies: - picocolors "^1.0.0" - shell-quote "^1.8.1" - -layout-base@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/layout-base/-/layout-base-1.0.2.tgz#1291e296883c322a9dd4c5dd82063721b53e26e2" - integrity sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg== - -leven@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" - integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== - -lilconfig@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52" - integrity sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ== - -lilconfig@^3.0.0, lilconfig@^3.1.1: - version "3.1.2" - resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.1.2.tgz#e4a7c3cb549e3a606c8dcc32e5ae1005e62c05cb" - integrity sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow== - -lines-and-columns@^1.1.6: - version "1.2.4" - resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" - integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== - -liquid-json@0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/liquid-json/-/liquid-json-0.3.1.tgz#9155a18136d8a6b2615e5f16f9a2448ab6b50eea" - integrity sha512-wUayTU8MS827Dam6MxgD72Ui+KOSF+u/eIqpatOtjnvgJ0+mnDq33uC2M7J0tPK+upe/DpUAuK4JUU89iBoNKQ== - -load-script@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/load-script/-/load-script-1.0.0.tgz#0491939e0bee5643ee494a7e3da3d2bac70c6ca4" - integrity sha512-kPEjMFtZvwL9TaZo0uZ2ml+Ye9HUMmPwbYRJ324qF9tqMejwykJ5ggTyvzmrbBeapCAbk98BSbTeovHEEP1uCA== - -loader-runner@^4.2.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.0.tgz#c1b4a163b99f614830353b16755e7149ac2314e1" - integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg== - -loader-utils@^2.0.0: - version "2.0.4" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.4.tgz#8b5cb38b5c34a9a018ee1fc0e6a066d1dfcc528c" - integrity sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw== - dependencies: - big.js "^5.2.2" - emojis-list "^3.0.0" - json5 "^2.1.2" - -loader-utils@^3.2.0: - version "3.3.1" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-3.3.1.tgz#735b9a19fd63648ca7adbd31c2327dfe281304e5" - integrity sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg== - -locate-path@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" - integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== - dependencies: - p-locate "^3.0.0" - path-exists "^3.0.0" - -locate-path@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" - integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== - dependencies: - p-locate "^5.0.0" - -locate-path@^7.1.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-7.2.0.tgz#69cb1779bd90b35ab1e771e1f2f89a202c2a8a8a" - integrity sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA== - dependencies: - p-locate "^6.0.0" - -lodash-es@^4.17.21: - version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" - integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== - -lodash.debounce@^4.0.8: - version "4.0.8" - resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" - integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== - -lodash.isequal@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" - integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ== - -lodash.memoize@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" - integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag== - -lodash.uniq@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" - integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ== - -lodash@4.17.21, lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4: - version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" - integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== - -longest-streak@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-3.1.0.tgz#62fa67cd958742a1574af9f39866364102d90cd4" - integrity sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g== - -loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" - integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== - dependencies: - js-tokens "^3.0.0 || ^4.0.0" - -lower-case@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28" - integrity sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg== - dependencies: - tslib "^2.0.3" - -lowercase-keys@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-3.0.0.tgz#c5e7d442e37ead247ae9db117a9d0a467c89d4f2" - integrity sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ== - -lru-cache@^10.2.0: - version "10.2.2" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.2.tgz#48206bc114c1252940c41b25b41af5b545aca878" - integrity sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ== - -lru-cache@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" - integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== - dependencies: - yallist "^3.0.2" - -lru-cache@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" - integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== - dependencies: - yallist "^4.0.0" - -markdown-extensions@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/markdown-extensions/-/markdown-extensions-2.0.0.tgz#34bebc83e9938cae16e0e017e4a9814a8330d3c4" - integrity sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q== - -markdown-table@^3.0.0: - version "3.0.3" - resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-3.0.3.tgz#e6331d30e493127e031dd385488b5bd326e4a6bd" - integrity sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw== - -md5.js@^1.3.4: - version "1.3.5" - resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" - integrity sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg== - dependencies: - hash-base "^3.0.0" - inherits "^2.0.1" - safe-buffer "^5.1.2" - -mdast-util-definitions@^5.0.0: - version "5.1.2" - resolved "https://registry.yarnpkg.com/mdast-util-definitions/-/mdast-util-definitions-5.1.2.tgz#9910abb60ac5d7115d6819b57ae0bcef07a3f7a7" - integrity sha512-8SVPMuHqlPME/z3gqVwWY4zVXn8lqKv/pAhC57FuJ40ImXyBpmO5ukh98zB2v7Blql2FiHjHv9LVztSIqjY+MA== - dependencies: - "@types/mdast" "^3.0.0" - "@types/unist" "^2.0.0" - unist-util-visit "^4.0.0" - -mdast-util-directive@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/mdast-util-directive/-/mdast-util-directive-3.0.0.tgz#3fb1764e705bbdf0afb0d3f889e4404c3e82561f" - integrity sha512-JUpYOqKI4mM3sZcNxmF/ox04XYFFkNwr0CFlrQIkCwbvH0xzMCqkMqAde9wRd80VAhaUrwFwKm2nxretdT1h7Q== - dependencies: - "@types/mdast" "^4.0.0" - "@types/unist" "^3.0.0" - devlop "^1.0.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-visit-parents "^6.0.0" - -mdast-util-find-and-replace@^3.0.0, mdast-util-find-and-replace@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.1.tgz#a6fc7b62f0994e973490e45262e4bc07607b04e0" - integrity sha512-SG21kZHGC3XRTSUhtofZkBzZTJNM5ecCi0SK2IMKmSXR8vO3peL+kb1O0z7Zl83jKtutG4k5Wv/W7V3/YHvzPA== - dependencies: - "@types/mdast" "^4.0.0" - escape-string-regexp "^5.0.0" - unist-util-is "^6.0.0" - unist-util-visit-parents "^6.0.0" - -mdast-util-from-markdown@^1.0.0, mdast-util-from-markdown@^1.1.0, mdast-util-from-markdown@^1.2.0, mdast-util-from-markdown@^1.3.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz#9421a5a247f10d31d2faed2a30df5ec89ceafcf0" - integrity sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww== - dependencies: - "@types/mdast" "^3.0.0" - "@types/unist" "^2.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" - -mdast-util-from-markdown@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.1.tgz#32a6e8f512b416e1f51eb817fc64bd867ebcd9cc" - integrity sha512-aJEUyzZ6TzlsX2s5B4Of7lN7EQtAxvtradMMglCQDyaTFgse6CmtmdJ15ElnVRlCg1vpNyVtbem0PWzlNieZsA== - dependencies: - "@types/mdast" "^4.0.0" - "@types/unist" "^3.0.0" - decode-named-character-reference "^1.0.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" - -mdast-util-frontmatter@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/mdast-util-frontmatter/-/mdast-util-frontmatter-2.0.1.tgz#f5f929eb1eb36c8a7737475c7eb438261f964ee8" - integrity sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA== - dependencies: - "@types/mdast" "^4.0.0" - devlop "^1.0.0" - escape-string-regexp "^5.0.0" - mdast-util-from-markdown "^2.0.0" - mdast-util-to-markdown "^2.0.0" - micromark-extension-frontmatter "^2.0.0" - -mdast-util-gfm-autolink-literal@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.0.tgz#5baf35407421310a08e68c15e5d8821e8898ba2a" - integrity sha512-FyzMsduZZHSc3i0Px3PQcBT4WJY/X/RCtEJKuybiC6sjPqLv7h1yqAkmILZtuxMSsUyaLUWNp71+vQH2zqp5cg== - dependencies: - "@types/mdast" "^4.0.0" - ccount "^2.0.0" - devlop "^1.0.0" - mdast-util-find-and-replace "^3.0.0" - micromark-util-character "^2.0.0" - -mdast-util-gfm-footnote@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.0.0.tgz#25a1753c7d16db8bfd53cd84fe50562bd1e6d6a9" - integrity sha512-5jOT2boTSVkMnQ7LTrd6n/18kqwjmuYqo7JUPe+tRCY6O7dAuTFMtTPauYYrMPpox9hlN0uOx/FL8XvEfG9/mQ== - dependencies: - "@types/mdast" "^4.0.0" - devlop "^1.1.0" - mdast-util-from-markdown "^2.0.0" - mdast-util-to-markdown "^2.0.0" - micromark-util-normalize-identifier "^2.0.0" - -mdast-util-gfm-strikethrough@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz#d44ef9e8ed283ac8c1165ab0d0dfd058c2764c16" - integrity sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg== - dependencies: - "@types/mdast" "^4.0.0" - mdast-util-from-markdown "^2.0.0" - mdast-util-to-markdown "^2.0.0" - -mdast-util-gfm-table@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz#7a435fb6223a72b0862b33afbd712b6dae878d38" - integrity sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg== - dependencies: - "@types/mdast" "^4.0.0" - devlop "^1.0.0" - markdown-table "^3.0.0" - mdast-util-from-markdown "^2.0.0" - mdast-util-to-markdown "^2.0.0" - -mdast-util-gfm-task-list-item@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz#e68095d2f8a4303ef24094ab642e1047b991a936" - integrity sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ== - dependencies: - "@types/mdast" "^4.0.0" - devlop "^1.0.0" - mdast-util-from-markdown "^2.0.0" - mdast-util-to-markdown "^2.0.0" - -mdast-util-gfm@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/mdast-util-gfm/-/mdast-util-gfm-3.0.0.tgz#3f2aecc879785c3cb6a81ff3a243dc11eca61095" - integrity sha512-dgQEX5Amaq+DuUqf26jJqSK9qgixgd6rYDHAv4aTBuA92cTknZlKpPfa86Z/s8Dj8xsAQpFfBmPUHWJBWqS4Bw== - dependencies: - mdast-util-from-markdown "^2.0.0" - mdast-util-gfm-autolink-literal "^2.0.0" - mdast-util-gfm-footnote "^2.0.0" - mdast-util-gfm-strikethrough "^2.0.0" - mdast-util-gfm-table "^2.0.0" - mdast-util-gfm-task-list-item "^2.0.0" - mdast-util-to-markdown "^2.0.0" - -mdast-util-mdx-expression@^1.0.0: - version "1.3.2" - resolved "https://registry.yarnpkg.com/mdast-util-mdx-expression/-/mdast-util-mdx-expression-1.3.2.tgz#d027789e67524d541d6de543f36d51ae2586f220" - integrity sha512-xIPmR5ReJDu/DHH1OoIT1HkuybIfRGYRywC+gJtI7qHjCJp/M9jrmBEJW22O8lskDWm562BX2W8TiAwRTb0rKA== - dependencies: - "@types/estree-jsx" "^1.0.0" - "@types/hast" "^2.0.0" - "@types/mdast" "^3.0.0" - mdast-util-from-markdown "^1.0.0" - mdast-util-to-markdown "^1.0.0" - -mdast-util-mdx-expression@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.0.tgz#4968b73724d320a379110d853e943a501bfd9d87" - integrity sha512-fGCu8eWdKUKNu5mohVGkhBXCXGnOTLuFqOvGMvdikr+J1w7lDJgxThOKpwRWzzbyXAU2hhSwsmssOY4yTokluw== - 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" - -mdast-util-mdx-jsx@^2.0.0: - version "2.1.4" - resolved "https://registry.yarnpkg.com/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-2.1.4.tgz#7c1f07f10751a78963cfabee38017cbc8b7786d1" - integrity sha512-DtMn9CmVhVzZx3f+optVDF8yFgQVt7FghCRNdlIaS3X5Bnym3hZwPbg/XW86vdpKjlc1PVj26SpnLGeJBXD3JA== - dependencies: - "@types/estree-jsx" "^1.0.0" - "@types/hast" "^2.0.0" - "@types/mdast" "^3.0.0" - "@types/unist" "^2.0.0" - ccount "^2.0.0" - mdast-util-from-markdown "^1.1.0" - mdast-util-to-markdown "^1.3.0" - parse-entities "^4.0.0" - stringify-entities "^4.0.0" - unist-util-remove-position "^4.0.0" - unist-util-stringify-position "^3.0.0" - vfile-message "^3.0.0" - -mdast-util-mdx-jsx@^3.0.0: - version "3.1.2" - resolved "https://registry.yarnpkg.com/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.1.2.tgz#daae777c72f9c4a106592e3025aa50fb26068e1b" - integrity sha512-eKMQDeywY2wlHc97k5eD8VC+9ASMjN8ItEZQNGwJ6E0XWKiW/Z0V5/H8pvoXUf+y+Mj0VIgeRRbujBmFn4FTyA== - 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-remove-position "^5.0.0" - unist-util-stringify-position "^4.0.0" - vfile-message "^4.0.0" - -mdast-util-mdx@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/mdast-util-mdx/-/mdast-util-mdx-2.0.1.tgz#49b6e70819b99bb615d7223c088d295e53bb810f" - integrity sha512-38w5y+r8nyKlGvNjSEqWrhG0w5PmnRA+wnBvm+ulYCct7nsGYhFVb0lljS9bQav4psDAS1eGkP2LMVcZBi/aqw== - dependencies: - mdast-util-from-markdown "^1.0.0" - mdast-util-mdx-expression "^1.0.0" - mdast-util-mdx-jsx "^2.0.0" - mdast-util-mdxjs-esm "^1.0.0" - mdast-util-to-markdown "^1.0.0" - -mdast-util-mdx@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/mdast-util-mdx/-/mdast-util-mdx-3.0.0.tgz#792f9cf0361b46bee1fdf1ef36beac424a099c41" - integrity sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w== - dependencies: - mdast-util-from-markdown "^2.0.0" - mdast-util-mdx-expression "^2.0.0" - mdast-util-mdx-jsx "^3.0.0" - mdast-util-mdxjs-esm "^2.0.0" - mdast-util-to-markdown "^2.0.0" - -mdast-util-mdxjs-esm@^1.0.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-1.3.1.tgz#645d02cd607a227b49721d146fd81796b2e2d15b" - integrity sha512-SXqglS0HrEvSdUEfoXFtcg7DRl7S2cwOXc7jkuusG472Mmjag34DUDeOJUZtl+BVnyeO1frIgVpHlNRWc2gk/w== - dependencies: - "@types/estree-jsx" "^1.0.0" - "@types/hast" "^2.0.0" - "@types/mdast" "^3.0.0" - mdast-util-from-markdown "^1.0.0" - mdast-util-to-markdown "^1.0.0" - -mdast-util-mdxjs-esm@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz#019cfbe757ad62dd557db35a695e7314bcc9fa97" - 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" - -mdast-util-phrasing@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/mdast-util-phrasing/-/mdast-util-phrasing-3.0.1.tgz#c7c21d0d435d7fb90956038f02e8702781f95463" - integrity sha512-WmI1gTXUBJo4/ZmSk79Wcb2HcjPJBzM1nlI/OUWA8yk2X9ik3ffNbBGsU+09BFmXaL1IBb9fiuvq6/KMiNycSg== - dependencies: - "@types/mdast" "^3.0.0" - unist-util-is "^5.0.0" - -mdast-util-phrasing@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz#7cc0a8dec30eaf04b7b1a9661a92adb3382aa6e3" - integrity sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w== - dependencies: - "@types/mdast" "^4.0.0" - unist-util-is "^6.0.0" - -mdast-util-to-hast@^12.1.0: - version "12.3.0" - resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-12.3.0.tgz#045d2825fb04374e59970f5b3f279b5700f6fb49" - integrity sha512-pits93r8PhnIoU4Vy9bjW39M2jJ6/tdHyja9rrot9uujkN7UTU9SDnE6WNJz/IGyQk3XHX6yNNtrBH6cQzm8Hw== - dependencies: - "@types/hast" "^2.0.0" - "@types/mdast" "^3.0.0" - mdast-util-definitions "^5.0.0" - micromark-util-sanitize-uri "^1.1.0" - trim-lines "^3.0.0" - unist-util-generated "^2.0.0" - unist-util-position "^4.0.0" - unist-util-visit "^4.0.0" - -mdast-util-to-hast@^13.0.0: - version "13.2.0" - resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz#5ca58e5b921cc0a3ded1bc02eed79a4fe4fe41f4" - integrity sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA== - dependencies: - "@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-position "^5.0.0" - unist-util-visit "^5.0.0" - vfile "^6.0.0" - -mdast-util-to-markdown@^1.0.0, mdast-util-to-markdown@^1.3.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/mdast-util-to-markdown/-/mdast-util-to-markdown-1.5.0.tgz#c13343cb3fc98621911d33b5cd42e7d0731171c6" - integrity sha512-bbv7TPv/WC49thZPg3jXuqzuvI45IL2EVAr/KxF0BSdHsU0ceFHOmwQn6evxAh1GaoK/6GQ1wp4R4oW2+LFL/A== - dependencies: - "@types/mdast" "^3.0.0" - "@types/unist" "^2.0.0" - longest-streak "^3.0.0" - mdast-util-phrasing "^3.0.0" - mdast-util-to-string "^3.0.0" - micromark-util-decode-string "^1.0.0" - unist-util-visit "^4.0.0" - zwitch "^2.0.0" - -mdast-util-to-markdown@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.0.tgz#9813f1d6e0cdaac7c244ec8c6dabfdb2102ea2b4" - integrity sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ== - dependencies: - "@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-decode-string "^2.0.0" - unist-util-visit "^5.0.0" - zwitch "^2.0.0" - -mdast-util-to-string@^3.0.0, mdast-util-to-string@^3.1.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz#66f7bb6324756741c5f47a53557f0cbf16b6f789" - integrity sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg== - dependencies: - "@types/mdast" "^3.0.0" - -mdast-util-to-string@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz#7a5121475556a04e7eddeb67b264aae79d312814" - integrity sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg== - dependencies: - "@types/mdast" "^4.0.0" - -mdn-data@2.0.28: - version "2.0.28" - resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.28.tgz#5ec48e7bef120654539069e1ae4ddc81ca490eba" - integrity sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g== - -mdn-data@2.0.30: - version "2.0.30" - resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.30.tgz#ce4df6f80af6cfbe218ecd5c552ba13c4dfa08cc" - integrity sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA== - -mdx-mermaid@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/mdx-mermaid/-/mdx-mermaid-2.0.0.tgz#0bc73b31615810f5f52d0031b641a859fa30de7d" - integrity sha512-vmkh4yg/EgkhAWxdFsyol5Tgk9aTnM16njgGIYk3R3SdbejPt8YV+HRYycAOstR1TJefMNAmjAyqkRjukLP7qg== - optionalDependencies: - estree-util-to-js "^1.1.0" - estree-util-visit "^1.2.0" - hast-util-from-html "^1.0.1" - hast-util-to-estree "^2.1.0" - mdast-util-from-markdown "^1.2.0" - mdast-util-mdx "^2.0.0" - micromark-extension-mdxjs "^1.0.0" - puppeteer "^18.0.0" - -media-typer@0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" - integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== - -medium-zoom@^1.0.6: - version "1.1.0" - resolved "https://registry.yarnpkg.com/medium-zoom/-/medium-zoom-1.1.0.tgz#6efb6bbda861a02064ee71a2617a8dc4381ecc71" - integrity sha512-ewyDsp7k4InCUp3jRmwHBRFGyjBimKps/AJLjRSox+2q/2H4p/PNpQf+pwONWlJiOudkBXtbdmVbFjqyybfTmQ== - -memfs@^3.1.2, memfs@^3.4.3: - version "3.6.0" - resolved "https://registry.yarnpkg.com/memfs/-/memfs-3.6.0.tgz#d7a2110f86f79dd950a8b6df6d57bc984aa185f6" - integrity sha512-EGowvkkgbMcIChjMTMkESFDbZeSh8xZ7kNSF0hAiAN4Jh6jgHCRS0Ga/+C8y6Au+oqpezRHCfPsmJ2+DwAgiwQ== - dependencies: - fs-monkey "^1.0.4" - -memoize-one@^5.1.1: - version "5.2.1" - resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" - integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q== - -merge-descriptors@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" - integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== - -merge-stream@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" - integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== - -merge2@^1.3.0, merge2@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" - integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== - -mermaid@^10.4.0, mermaid@^10.9.1: - version "10.9.1" - resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-10.9.1.tgz#5f582c23f3186c46c6aa673e59eeb46d741b2ea6" - integrity sha512-Mx45Obds5W1UkW1nv/7dHRsbfMM1aOKA2+Pxs/IGHNonygDHwmng8xTHyS9z4KWVi0rbko8gjiBmuwwXQ7tiNA== - dependencies: - "@braintree/sanitize-url" "^6.0.1" - "@types/d3-scale" "^4.0.3" - "@types/d3-scale-chromatic" "^3.0.0" - cytoscape "^3.28.1" - cytoscape-cose-bilkent "^4.1.0" - d3 "^7.4.0" - d3-sankey "^0.12.3" - dagre-d3-es "7.0.10" - dayjs "^1.11.7" - dompurify "^3.0.5" - elkjs "^0.9.0" - katex "^0.16.9" - khroma "^2.0.0" - lodash-es "^4.17.21" - mdast-util-from-markdown "^1.3.0" - non-layered-tidy-tree-layout "^2.0.2" - stylis "^4.1.3" - ts-dedent "^2.2.0" - uuid "^9.0.0" - web-worker "^1.2.0" - -methods@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" - integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== - -micromark-core-commonmark@^1.0.0, micromark-core-commonmark@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz#1386628df59946b2d39fb2edfd10f3e8e0a75bb8" - integrity sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw== - 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" - -micromark-core-commonmark@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/micromark-core-commonmark/-/micromark-core-commonmark-2.0.1.tgz#9a45510557d068605c6e9a80f282b2bb8581e43d" - integrity sha512-CUQyKr1e///ZODyD1U3xit6zXwy1a8q2a1S1HKtIlmgvurrEpaw/Y9y6KSIbF8P59cn/NjzHyO+Q2fAyYLQrAA== - dependencies: - decode-named-character-reference "^1.0.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" - -micromark-extension-directive@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/micromark-extension-directive/-/micromark-extension-directive-3.0.0.tgz#527869de497a6de9024138479091bc885dae076b" - integrity sha512-61OI07qpQrERc+0wEysLHMvoiO3s2R56x5u7glHq2Yqq6EHbH4dW25G9GfDdGCDYqA21KE6DWgNSzxSwHc2hSg== - dependencies: - devlop "^1.0.0" - micromark-factory-space "^2.0.0" - micromark-factory-whitespace "^2.0.0" - micromark-util-character "^2.0.0" - micromark-util-symbol "^2.0.0" - micromark-util-types "^2.0.0" - parse-entities "^4.0.0" - -micromark-extension-frontmatter@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-extension-frontmatter/-/micromark-extension-frontmatter-2.0.0.tgz#651c52ffa5d7a8eeed687c513cd869885882d67a" - integrity sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg== - dependencies: - fault "^2.0.0" - micromark-util-character "^2.0.0" - micromark-util-symbol "^2.0.0" - micromark-util-types "^2.0.0" - -micromark-extension-gfm-autolink-literal@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.0.0.tgz#f1e50b42e67d441528f39a67133eddde2bbabfd9" - integrity sha512-rTHfnpt/Q7dEAK1Y5ii0W8bhfJlVJFnJMHIPisfPK3gpVNuOP0VnRl96+YJ3RYWV/P4gFeQoGKNlT3RhuvpqAg== - dependencies: - micromark-util-character "^2.0.0" - micromark-util-sanitize-uri "^2.0.0" - micromark-util-symbol "^2.0.0" - micromark-util-types "^2.0.0" - -micromark-extension-gfm-footnote@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.0.0.tgz#91afad310065a94b636ab1e9dab2c60d1aab953c" - integrity sha512-6Rzu0CYRKDv3BfLAUnZsSlzx3ak6HAoI85KTiijuKIz5UxZxbUI+pD6oHgw+6UtQuiRwnGRhzMmPRv4smcz0fg== - dependencies: - devlop "^1.0.0" - micromark-core-commonmark "^2.0.0" - micromark-factory-space "^2.0.0" - micromark-util-character "^2.0.0" - micromark-util-normalize-identifier "^2.0.0" - micromark-util-sanitize-uri "^2.0.0" - micromark-util-symbol "^2.0.0" - micromark-util-types "^2.0.0" - -micromark-extension-gfm-strikethrough@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.0.0.tgz#6917db8e320da70e39ffbf97abdbff83e6783e61" - integrity sha512-c3BR1ClMp5fxxmwP6AoOY2fXO9U8uFMKs4ADD66ahLTNcwzSCyRVU4k7LPV5Nxo/VJiR4TdzxRQY2v3qIUceCw== - dependencies: - devlop "^1.0.0" - micromark-util-chunked "^2.0.0" - micromark-util-classify-character "^2.0.0" - micromark-util-resolve-all "^2.0.0" - micromark-util-symbol "^2.0.0" - micromark-util-types "^2.0.0" - -micromark-extension-gfm-table@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.0.0.tgz#2cf3fe352d9e089b7ef5fff003bdfe0da29649b7" - integrity sha512-PoHlhypg1ItIucOaHmKE8fbin3vTLpDOUg8KAr8gRCF1MOZI9Nquq2i/44wFvviM4WuxJzc3demT8Y3dkfvYrw== - dependencies: - devlop "^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" - -micromark-extension-gfm-tagfilter@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz#f26d8a7807b5985fba13cf61465b58ca5ff7dc57" - integrity sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg== - dependencies: - micromark-util-types "^2.0.0" - -micromark-extension-gfm-task-list-item@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.0.1.tgz#ee8b208f1ced1eb9fb11c19a23666e59d86d4838" - integrity sha512-cY5PzGcnULaN5O7T+cOzfMoHjBW7j+T9D2sucA5d/KbsBTPcYdebm9zUd9zzdgJGCwahV+/W78Z3nbulBYVbTw== - dependencies: - devlop "^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" - -micromark-extension-gfm@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz#3e13376ab95dd7a5cfd0e29560dfe999657b3c5b" - integrity sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w== - dependencies: - micromark-extension-gfm-autolink-literal "^2.0.0" - micromark-extension-gfm-footnote "^2.0.0" - micromark-extension-gfm-strikethrough "^2.0.0" - micromark-extension-gfm-table "^2.0.0" - micromark-extension-gfm-tagfilter "^2.0.0" - micromark-extension-gfm-task-list-item "^2.0.0" - micromark-util-combine-extensions "^2.0.0" - micromark-util-types "^2.0.0" - -micromark-extension-mdx-expression@^1.0.0: - version "1.0.8" - resolved "https://registry.yarnpkg.com/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-1.0.8.tgz#5bc1f5fd90388e8293b3ef4f7c6f06c24aff6314" - integrity sha512-zZpeQtc5wfWKdzDsHRBY003H2Smg+PUi2REhqgIhdzAa5xonhP03FcXxqFSerFiNUr5AWmHpaNPQTBVOS4lrXw== - dependencies: - "@types/estree" "^1.0.0" - micromark-factory-mdx-expression "^1.0.0" - micromark-factory-space "^1.0.0" - micromark-util-character "^1.0.0" - micromark-util-events-to-acorn "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - uvu "^0.5.0" - -micromark-extension-mdx-expression@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.0.tgz#1407b9ce69916cf5e03a196ad9586889df25302a" - integrity sha512-sI0nwhUDz97xyzqJAbHQhp5TfaxEvZZZ2JDqUo+7NvyIYG6BZ5CPPqj2ogUoPJlmXHBnyZUzISg9+oUmU6tUjQ== - dependencies: - "@types/estree" "^1.0.0" - devlop "^1.0.0" - micromark-factory-mdx-expression "^2.0.0" - micromark-factory-space "^2.0.0" - micromark-util-character "^2.0.0" - micromark-util-events-to-acorn "^2.0.0" - micromark-util-symbol "^2.0.0" - micromark-util-types "^2.0.0" - -micromark-extension-mdx-jsx@^1.0.0: - version "1.0.5" - resolved "https://registry.yarnpkg.com/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-1.0.5.tgz#e72d24b7754a30d20fb797ece11e2c4e2cae9e82" - integrity sha512-gPH+9ZdmDflbu19Xkb8+gheqEDqkSpdCEubQyxuz/Hn8DOXiXvrXeikOoBA71+e8Pfi0/UYmU3wW3H58kr7akA== - dependencies: - "@types/acorn" "^4.0.0" - "@types/estree" "^1.0.0" - estree-util-is-identifier-name "^2.0.0" - micromark-factory-mdx-expression "^1.0.0" - micromark-factory-space "^1.0.0" - micromark-util-character "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - uvu "^0.5.0" - vfile-message "^3.0.0" - -micromark-extension-mdx-jsx@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-3.0.0.tgz#4aba0797c25efb2366a3fd2d367c6b1c1159f4f5" - integrity sha512-uvhhss8OGuzR4/N17L1JwvmJIpPhAd8oByMawEKx6NVdBCbesjH4t+vjEp3ZXft9DwvlKSD07fCeI44/N0Vf2w== - dependencies: - "@types/acorn" "^4.0.0" - "@types/estree" "^1.0.0" - devlop "^1.0.0" - estree-util-is-identifier-name "^3.0.0" - micromark-factory-mdx-expression "^2.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" - vfile-message "^4.0.0" - -micromark-extension-mdx-md@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/micromark-extension-mdx-md/-/micromark-extension-mdx-md-1.0.1.tgz#595d4b2f692b134080dca92c12272ab5b74c6d1a" - integrity sha512-7MSuj2S7xjOQXAjjkbjBsHkMtb+mDGVW6uI2dBL9snOBCbZmoNgDAeZ0nSn9j3T42UE/g2xVNMn18PJxZvkBEA== - dependencies: - micromark-util-types "^1.0.0" - -micromark-extension-mdx-md@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-extension-mdx-md/-/micromark-extension-mdx-md-2.0.0.tgz#1d252881ea35d74698423ab44917e1f5b197b92d" - integrity sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ== - dependencies: - micromark-util-types "^2.0.0" - -micromark-extension-mdxjs-esm@^1.0.0: - version "1.0.5" - resolved "https://registry.yarnpkg.com/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-1.0.5.tgz#e4f8be9c14c324a80833d8d3a227419e2b25dec1" - integrity sha512-xNRBw4aoURcyz/S69B19WnZAkWJMxHMT5hE36GtDAyhoyn/8TuAeqjFJQlwk+MKQsUD7b3l7kFX+vlfVWgcX1w== - dependencies: - "@types/estree" "^1.0.0" - micromark-core-commonmark "^1.0.0" - micromark-util-character "^1.0.0" - micromark-util-events-to-acorn "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - unist-util-position-from-estree "^1.1.0" - uvu "^0.5.0" - vfile-message "^3.0.0" - -micromark-extension-mdxjs-esm@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-3.0.0.tgz#de21b2b045fd2059bd00d36746081de38390d54a" - integrity sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A== - dependencies: - "@types/estree" "^1.0.0" - devlop "^1.0.0" - micromark-core-commonmark "^2.0.0" - micromark-util-character "^2.0.0" - micromark-util-events-to-acorn "^2.0.0" - micromark-util-symbol "^2.0.0" - micromark-util-types "^2.0.0" - unist-util-position-from-estree "^2.0.0" - vfile-message "^4.0.0" - -micromark-extension-mdxjs@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/micromark-extension-mdxjs/-/micromark-extension-mdxjs-1.0.1.tgz#f78d4671678d16395efeda85170c520ee795ded8" - integrity sha512-7YA7hF6i5eKOfFUzZ+0z6avRG52GpWR8DL+kN47y3f2KhxbBZMhmxe7auOeaTBrW2DenbbZTf1ea9tA2hDpC2Q== - dependencies: - acorn "^8.0.0" - acorn-jsx "^5.0.0" - micromark-extension-mdx-expression "^1.0.0" - micromark-extension-mdx-jsx "^1.0.0" - micromark-extension-mdx-md "^1.0.0" - micromark-extension-mdxjs-esm "^1.0.0" - micromark-util-combine-extensions "^1.0.0" - micromark-util-types "^1.0.0" - -micromark-extension-mdxjs@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/micromark-extension-mdxjs/-/micromark-extension-mdxjs-3.0.0.tgz#b5a2e0ed449288f3f6f6c544358159557549de18" - integrity sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ== - dependencies: - acorn "^8.0.0" - acorn-jsx "^5.0.0" - micromark-extension-mdx-expression "^3.0.0" - micromark-extension-mdx-jsx "^3.0.0" - micromark-extension-mdx-md "^2.0.0" - micromark-extension-mdxjs-esm "^3.0.0" - micromark-util-combine-extensions "^2.0.0" - micromark-util-types "^2.0.0" - -micromark-factory-destination@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-factory-destination/-/micromark-factory-destination-1.1.0.tgz#eb815957d83e6d44479b3df640f010edad667b9f" - integrity sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg== - dependencies: - micromark-util-character "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - -micromark-factory-destination@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-factory-destination/-/micromark-factory-destination-2.0.0.tgz#857c94debd2c873cba34e0445ab26b74f6a6ec07" - integrity sha512-j9DGrQLm/Uhl2tCzcbLhy5kXsgkHUrjJHg4fFAeoMRwJmJerT9aw4FEhIbZStWN8A3qMwOp1uzHr4UL8AInxtA== - dependencies: - micromark-util-character "^2.0.0" - micromark-util-symbol "^2.0.0" - micromark-util-types "^2.0.0" - -micromark-factory-label@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-factory-label/-/micromark-factory-label-1.1.0.tgz#cc95d5478269085cfa2a7282b3de26eb2e2dec68" - integrity sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w== - dependencies: - micromark-util-character "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - uvu "^0.5.0" - -micromark-factory-label@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-factory-label/-/micromark-factory-label-2.0.0.tgz#17c5c2e66ce39ad6f4fc4cbf40d972f9096f726a" - integrity sha512-RR3i96ohZGde//4WSe/dJsxOX6vxIg9TimLAS3i4EhBAFx8Sm5SmqVfR8E87DPSR31nEAjZfbt91OMZWcNgdZw== - dependencies: - devlop "^1.0.0" - micromark-util-character "^2.0.0" - micromark-util-symbol "^2.0.0" - micromark-util-types "^2.0.0" - -micromark-factory-mdx-expression@^1.0.0: - version "1.0.9" - resolved "https://registry.yarnpkg.com/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-1.0.9.tgz#57ba4571b69a867a1530f34741011c71c73a4976" - integrity sha512-jGIWzSmNfdnkJq05c7b0+Wv0Kfz3NJ3N4cBjnbO4zjXIlxJr+f8lk+5ZmwFvqdAbUy2q6B5rCY//g0QAAaXDWA== - dependencies: - "@types/estree" "^1.0.0" - micromark-util-character "^1.0.0" - micromark-util-events-to-acorn "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - unist-util-position-from-estree "^1.0.0" - uvu "^0.5.0" - vfile-message "^3.0.0" - -micromark-factory-mdx-expression@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-2.0.1.tgz#f2a9724ce174f1751173beb2c1f88062d3373b1b" - integrity sha512-F0ccWIUHRLRrYp5TC9ZYXmZo+p2AM13ggbsW4T0b5CRKP8KHVRB8t4pwtBgTxtjRmwrK0Irwm7vs2JOZabHZfg== - dependencies: - "@types/estree" "^1.0.0" - devlop "^1.0.0" - micromark-util-character "^2.0.0" - micromark-util-events-to-acorn "^2.0.0" - micromark-util-symbol "^2.0.0" - micromark-util-types "^2.0.0" - unist-util-position-from-estree "^2.0.0" - vfile-message "^4.0.0" - -micromark-factory-space@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz#c8f40b0640a0150751d3345ed885a080b0d15faf" - integrity sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ== - dependencies: - micromark-util-character "^1.0.0" - micromark-util-types "^1.0.0" - -micromark-factory-space@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-factory-space/-/micromark-factory-space-2.0.0.tgz#5e7afd5929c23b96566d0e1ae018ae4fcf81d030" - integrity sha512-TKr+LIDX2pkBJXFLzpyPyljzYK3MtmllMUMODTQJIUfDGncESaqB90db9IAUcz4AZAJFdd8U9zOp9ty1458rxg== - dependencies: - micromark-util-character "^2.0.0" - micromark-util-types "^2.0.0" - -micromark-factory-title@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-factory-title/-/micromark-factory-title-1.1.0.tgz#dd0fe951d7a0ac71bdc5ee13e5d1465ad7f50ea1" - integrity sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ== - 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-title@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-factory-title/-/micromark-factory-title-2.0.0.tgz#726140fc77892af524705d689e1cf06c8a83ea95" - integrity sha512-jY8CSxmpWLOxS+t8W+FG3Xigc0RDQA9bKMY/EwILvsesiRniiVMejYTE4wumNc2f4UbAa4WsHqe3J1QS1sli+A== - dependencies: - micromark-factory-space "^2.0.0" - micromark-util-character "^2.0.0" - micromark-util-symbol "^2.0.0" - micromark-util-types "^2.0.0" - -micromark-factory-whitespace@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-factory-whitespace/-/micromark-factory-whitespace-1.1.0.tgz#798fb7489f4c8abafa7ca77eed6b5745853c9705" - integrity sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ== - 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-whitespace@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.0.tgz#9e92eb0f5468083381f923d9653632b3cfb5f763" - integrity sha512-28kbwaBjc5yAI1XadbdPYHX/eDnqaUFVikLwrO7FDnKG7lpgxnvk/XGRhX/PN0mOZ+dBSZ+LgunHS+6tYQAzhA== - dependencies: - micromark-factory-space "^2.0.0" - micromark-util-character "^2.0.0" - micromark-util-symbol "^2.0.0" - micromark-util-types "^2.0.0" - -micromark-util-character@^1.0.0, micromark-util-character@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/micromark-util-character/-/micromark-util-character-1.2.0.tgz#4fedaa3646db249bc58caeb000eb3549a8ca5dcc" - integrity sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg== - dependencies: - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - -micromark-util-character@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-character/-/micromark-util-character-2.1.0.tgz#31320ace16b4644316f6bf057531689c71e2aee1" - integrity sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ== - dependencies: - micromark-util-symbol "^2.0.0" - micromark-util-types "^2.0.0" - -micromark-util-chunked@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz#37a24d33333c8c69a74ba12a14651fd9ea8a368b" - integrity sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ== - dependencies: - micromark-util-symbol "^1.0.0" - -micromark-util-chunked@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-util-chunked/-/micromark-util-chunked-2.0.0.tgz#e51f4db85fb203a79dbfef23fd41b2f03dc2ef89" - integrity sha512-anK8SWmNphkXdaKgz5hJvGa7l00qmcaUQoMYsBwDlSKFKjc6gjGXPDw3FNL3Nbwq5L8gE+RCbGqTw49FK5Qyvg== - dependencies: - micromark-util-symbol "^2.0.0" - -micromark-util-classify-character@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-classify-character/-/micromark-util-classify-character-1.1.0.tgz#6a7f8c8838e8a120c8e3c4f2ae97a2bff9190e9d" - integrity sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw== - dependencies: - micromark-util-character "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - -micromark-util-classify-character@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-util-classify-character/-/micromark-util-classify-character-2.0.0.tgz#8c7537c20d0750b12df31f86e976d1d951165f34" - integrity sha512-S0ze2R9GH+fu41FA7pbSqNWObo/kzwf8rN/+IGlW/4tC6oACOs8B++bh+i9bVyNnwCcuksbFwsBme5OCKXCwIw== - dependencies: - micromark-util-character "^2.0.0" - micromark-util-symbol "^2.0.0" - micromark-util-types "^2.0.0" - -micromark-util-combine-extensions@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.1.0.tgz#192e2b3d6567660a85f735e54d8ea6e3952dbe84" - integrity sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA== - dependencies: - micromark-util-chunked "^1.0.0" - micromark-util-types "^1.0.0" - -micromark-util-combine-extensions@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.0.tgz#75d6ab65c58b7403616db8d6b31315013bfb7ee5" - integrity sha512-vZZio48k7ON0fVS3CUgFatWHoKbbLTK/rT7pzpJ4Bjp5JjkZeasRfrS9wsBdDJK2cJLHMckXZdzPSSr1B8a4oQ== - dependencies: - micromark-util-chunked "^2.0.0" - micromark-util-types "^2.0.0" - -micromark-util-decode-numeric-character-reference@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.1.0.tgz#b1e6e17009b1f20bc652a521309c5f22c85eb1c6" - integrity sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw== - dependencies: - micromark-util-symbol "^1.0.0" - -micromark-util-decode-numeric-character-reference@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.1.tgz#2698bbb38f2a9ba6310e359f99fcb2b35a0d2bd5" - integrity sha512-bmkNc7z8Wn6kgjZmVHOX3SowGmVdhYS7yBpMnuMnPzDq/6xwVA604DuOXMZTO1lvq01g+Adfa0pE2UKGlxL1XQ== - dependencies: - micromark-util-symbol "^2.0.0" - -micromark-util-decode-string@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-decode-string/-/micromark-util-decode-string-1.1.0.tgz#dc12b078cba7a3ff690d0203f95b5d5537f2809c" - integrity sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ== - 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-decode-string@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-util-decode-string/-/micromark-util-decode-string-2.0.0.tgz#7dfa3a63c45aecaa17824e656bcdb01f9737154a" - integrity sha512-r4Sc6leeUTn3P6gk20aFMj2ntPwn6qpDZqWvYmAG6NgvFTIlj4WtrAudLi65qYoaGdXYViXYw2pkmn7QnIFasA== - dependencies: - decode-named-character-reference "^1.0.0" - micromark-util-character "^2.0.0" - micromark-util-decode-numeric-character-reference "^2.0.0" - micromark-util-symbol "^2.0.0" - -micromark-util-encode@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz#92e4f565fd4ccb19e0dcae1afab9a173bbeb19a5" - integrity sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw== - -micromark-util-encode@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-util-encode/-/micromark-util-encode-2.0.0.tgz#0921ac7953dc3f1fd281e3d1932decfdb9382ab1" - integrity sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA== - -micromark-util-events-to-acorn@^1.0.0: - version "1.2.3" - resolved "https://registry.yarnpkg.com/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-1.2.3.tgz#a4ab157f57a380e646670e49ddee97a72b58b557" - integrity sha512-ij4X7Wuc4fED6UoLWkmo0xJQhsktfNh1J0m8g4PbIMPlx+ek/4YdW5mvbye8z/aZvAPUoxgXHrwVlXAPKMRp1w== - dependencies: - "@types/acorn" "^4.0.0" - "@types/estree" "^1.0.0" - "@types/unist" "^2.0.0" - estree-util-visit "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - uvu "^0.5.0" - vfile-message "^3.0.0" - -micromark-util-events-to-acorn@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-2.0.2.tgz#4275834f5453c088bd29cd72dfbf80e3327cec07" - integrity sha512-Fk+xmBrOv9QZnEDguL9OI9/NQQp6Hz4FuQ4YmCb/5V7+9eAh1s6AYSvL20kHkD67YIg7EpE54TiSlcsf3vyZgA== - dependencies: - "@types/acorn" "^4.0.0" - "@types/estree" "^1.0.0" - "@types/unist" "^3.0.0" - devlop "^1.0.0" - estree-util-visit "^2.0.0" - micromark-util-symbol "^2.0.0" - micromark-util-types "^2.0.0" - vfile-message "^4.0.0" - -micromark-util-html-tag-name@^1.0.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.2.0.tgz#48fd7a25826f29d2f71479d3b4e83e94829b3588" - integrity sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q== - -micromark-util-html-tag-name@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.0.tgz#ae34b01cbe063363847670284c6255bb12138ec4" - integrity sha512-xNn4Pqkj2puRhKdKTm8t1YHC/BAjx6CEwRFXntTaRf/x16aqka6ouVoutm+QdkISTlT7e2zU7U4ZdlDLJd2Mcw== - -micromark-util-normalize-identifier@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz#7a73f824eb9f10d442b4d7f120fecb9b38ebf8b7" - integrity sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q== - dependencies: - micromark-util-symbol "^1.0.0" - -micromark-util-normalize-identifier@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.0.tgz#91f9a4e65fe66cc80c53b35b0254ad67aa431d8b" - integrity sha512-2xhYT0sfo85FMrUPtHcPo2rrp1lwbDEEzpx7jiH2xXJLqBuy4H0GgXk5ToU8IEwoROtXuL8ND0ttVa4rNqYK3w== - dependencies: - micromark-util-symbol "^2.0.0" - -micromark-util-resolve-all@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-resolve-all/-/micromark-util-resolve-all-1.1.0.tgz#4652a591ee8c8fa06714c9b54cd6c8e693671188" - integrity sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA== - dependencies: - micromark-util-types "^1.0.0" - -micromark-util-resolve-all@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.0.tgz#189656e7e1a53d0c86a38a652b284a252389f364" - integrity sha512-6KU6qO7DZ7GJkaCgwBNtplXCvGkJToU86ybBAUdavvgsCiG8lSSvYxr9MhwmQ+udpzywHsl4RpGJsYWG1pDOcA== - dependencies: - micromark-util-types "^2.0.0" - -micromark-util-sanitize-uri@^1.0.0, micromark-util-sanitize-uri@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz#613f738e4400c6eedbc53590c67b197e30d7f90d" - integrity sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A== - dependencies: - micromark-util-character "^1.0.0" - micromark-util-encode "^1.0.0" - micromark-util-symbol "^1.0.0" - -micromark-util-sanitize-uri@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.0.tgz#ec8fbf0258e9e6d8f13d9e4770f9be64342673de" - integrity sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw== - dependencies: - micromark-util-character "^2.0.0" - micromark-util-encode "^2.0.0" - micromark-util-symbol "^2.0.0" - -micromark-util-subtokenize@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz#941c74f93a93eaf687b9054aeb94642b0e92edb1" - integrity sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A== - dependencies: - micromark-util-chunked "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - uvu "^0.5.0" - -micromark-util-subtokenize@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/micromark-util-subtokenize/-/micromark-util-subtokenize-2.0.1.tgz#76129c49ac65da6e479c09d0ec4b5f29ec6eace5" - integrity sha512-jZNtiFl/1aY73yS3UGQkutD0UbhTt68qnRpw2Pifmz5wV9h8gOVsN70v+Lq/f1rKaU/W8pxRe8y8Q9FX1AOe1Q== - dependencies: - devlop "^1.0.0" - micromark-util-chunked "^2.0.0" - micromark-util-symbol "^2.0.0" - micromark-util-types "^2.0.0" - -micromark-util-symbol@^1.0.0, micromark-util-symbol@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz#813cd17837bdb912d069a12ebe3a44b6f7063142" - integrity sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag== - -micromark-util-symbol@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz#12225c8f95edf8b17254e47080ce0862d5db8044" - integrity sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw== - -micromark-util-types@^1.0.0, micromark-util-types@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-types/-/micromark-util-types-1.1.0.tgz#e6676a8cae0bb86a2171c498167971886cb7e283" - integrity sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg== - -micromark-util-types@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/micromark-util-types/-/micromark-util-types-2.0.0.tgz#63b4b7ffeb35d3ecf50d1ca20e68fc7caa36d95e" - integrity sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w== - -micromark@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/micromark/-/micromark-3.2.0.tgz#1af9fef3f995ea1ea4ac9c7e2f19c48fd5c006e9" - integrity sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA== - dependencies: - "@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" - -micromark@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/micromark/-/micromark-4.0.0.tgz#84746a249ebd904d9658cfabc1e8e5f32cbc6249" - integrity sha512-o/sd0nMof8kYff+TqcDx3VSrgBTcZpSvYcAHIfHhv5VAuNmisCxjhx6YmxS8PFEpb9z5WKWKPdzf0jM23ro3RQ== - dependencies: - "@types/debug" "^4.0.0" - debug "^4.0.0" - decode-named-character-reference "^1.0.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" - -micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5: - version "4.0.8" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" - integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== - dependencies: - braces "^3.0.3" - picomatch "^2.3.1" - -miller-rabin@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" - integrity sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA== - dependencies: - bn.js "^4.0.0" - brorand "^1.0.1" - -mime-db@1.48.0: - version "1.48.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.48.0.tgz#e35b31045dd7eada3aaad537ed88a33afbef2d1d" - integrity sha512-FM3QwxV+TnZYQ2aRqhlKBMHxk10lTbMt3bBkMAp54ddrNeVSfcQYOOKuGuy3Ddrm38I04If834fOUSq1yzslJQ== - -mime-db@1.52.0, "mime-db@>= 1.43.0 < 2": - version "1.52.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" - integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== - -mime-db@~1.33.0: - version "1.33.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db" - integrity sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ== - -mime-format@2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/mime-format/-/mime-format-2.0.1.tgz#1274876d58bc803332427a515f5f7036e07b9413" - integrity sha512-XxU3ngPbEnrYnNbIX+lYSaYg0M01v6p2ntd2YaFksTu0vayaw5OJvbdRyWs07EYRlLED5qadUZ+xo+XhOvFhwg== - dependencies: - charset "^1.0.0" - -mime-types@2.1.18: - version "2.1.18" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8" - integrity sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ== - dependencies: - mime-db "~1.33.0" - -mime-types@2.1.31: - version "2.1.31" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.31.tgz#a00d76b74317c61f9c2db2218b8e9f8e9c5c9e6b" - integrity sha512-XGZnNzm3QvgKxa8dpzyhFTHmpP3l5YNusmne07VUOXxou9CqUqYa/HBy124RqtVh/O2pECas/MOcsDgpilPOPg== - dependencies: - mime-db "1.48.0" - -mime-types@2.1.35, mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.24, mime-types@~2.1.34: - version "2.1.35" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" - integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== - dependencies: - mime-db "1.52.0" - -mime@1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" - integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== - -mimic-fn@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" - integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== - -mimic-response@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9" - integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== - -mimic-response@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-4.0.0.tgz#35468b19e7c75d10f5165ea25e75a5ceea7cf70f" - integrity sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg== - -mini-css-extract-plugin@^2.7.6: - version "2.9.0" - resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.0.tgz#c73a1327ccf466f69026ac22a8e8fd707b78a235" - integrity sha512-Zs1YsZVfemekSZG+44vBsYTLQORkPMwnlv+aehcxK/NLKC+EGhDB39/YePYYqx/sTk6NnYpuqikhSn7+JIevTA== - dependencies: - schema-utils "^4.0.0" - tapable "^2.2.1" - -minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" - integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== - -minimalistic-crypto-utils@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" - integrity sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg== - -minimatch@3.1.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1: - version "3.1.2" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" - integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== - dependencies: - brace-expansion "^1.1.7" - -minimatch@^5.0.1: - version "5.1.6" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" - integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== - dependencies: - brace-expansion "^2.0.1" - -minimatch@^9.0.4: - version "9.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.4.tgz#8e49c731d1749cbec05050ee5145147b32496a51" - integrity sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw== - dependencies: - brace-expansion "^2.0.1" - -minimist@^1.2.0, minimist@^1.2.5: - version "1.2.8" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" - integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== - -"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.1.2: - version "7.1.2" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" - integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== - -mkdirp-classic@^0.5.2: - version "0.5.3" - resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" - integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== - -mri@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b" - integrity sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA== - -mrmime@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/mrmime/-/mrmime-2.0.0.tgz#151082a6e06e59a9a39b46b3e14d5cfe92b3abb4" - integrity sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw== - -ms@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== - -ms@2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" - integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== - -ms@2.1.3: - version "2.1.3" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" - integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== - -multicast-dns@^7.2.5: - version "7.2.5" - resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-7.2.5.tgz#77eb46057f4d7adbd16d9290fa7299f6fa64cced" - integrity sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg== - dependencies: - dns-packet "^5.2.2" - thunky "^1.0.2" - -mustache@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/mustache/-/mustache-4.2.0.tgz#e5892324d60a12ec9c2a73359edca52972bf6f64" - integrity sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ== - -mz@^2.7.0: - version "2.7.0" - resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32" - integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q== - dependencies: - any-promise "^1.0.0" - object-assign "^4.0.1" - thenify-all "^1.0.0" - -nanoid@^3.3.7: - version "3.3.8" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf" - integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w== - -negotiator@0.6.3: - version "0.6.3" - resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" - integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== - -neo-async@^2.6.2: - version "2.6.2" - resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" - integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== - -no-case@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d" - integrity sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg== - dependencies: - lower-case "^2.0.2" - tslib "^2.0.3" - -node-emoji@^2.1.0: - version "2.1.3" - resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-2.1.3.tgz#93cfabb5cc7c3653aa52f29d6ffb7927d8047c06" - integrity sha512-E2WEOVsgs7O16zsURJ/eH8BqhF029wGpEOnv7Urwdo2wmQanOACwJQh0devF9D9RhoZru0+9JXIS0dBXIAz+lA== - dependencies: - "@sindresorhus/is" "^4.6.0" - char-regex "^1.0.2" - emojilib "^2.4.0" - skin-tone "^2.0.0" - -node-fetch-h2@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/node-fetch-h2/-/node-fetch-h2-2.3.0.tgz#c6188325f9bd3d834020bf0f2d6dc17ced2241ac" - integrity sha512-ofRW94Ab0T4AOh5Fk8t0h8OBWrmjb0SSB20xh1H8YnPV9EJ+f5AMoYSUQ2zgJ4Iq2HAK0I2l5/Nequ8YzFS3Hg== - dependencies: - http2-client "^1.2.5" - -node-fetch@2.6.7: - version "2.6.7" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" - integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== - dependencies: - whatwg-url "^5.0.0" - -node-fetch@^2.6.1: - version "2.7.0" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" - integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== - dependencies: - whatwg-url "^5.0.0" - -node-forge@^1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" - integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== - -node-polyfill-webpack-plugin@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/node-polyfill-webpack-plugin/-/node-polyfill-webpack-plugin-2.0.1.tgz#141d86f177103a8517c71d99b7c6a46edbb1bb58" - integrity sha512-ZUMiCnZkP1LF0Th2caY6J/eKKoA0TefpoVa68m/LQU1I/mE8rGt4fNYGgNuCcK+aG8P8P43nbeJ2RqJMOL/Y1A== - dependencies: - assert "^2.0.0" - browserify-zlib "^0.2.0" - buffer "^6.0.3" - console-browserify "^1.2.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" - https-browserify "^1.0.0" - os-browserify "^0.3.0" - path-browserify "^1.0.1" - process "^0.11.10" - punycode "^2.1.1" - querystring-es3 "^0.2.1" - readable-stream "^4.0.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" - util "^0.12.4" - vm-browserify "^1.1.2" - -node-readfiles@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/node-readfiles/-/node-readfiles-0.2.0.tgz#dbbd4af12134e2e635c245ef93ffcf6f60673a5d" - integrity sha512-SU00ZarexNlE4Rjdm83vglt5Y9yiQ+XI1XpflWlb7q7UTN1JUItm69xMeiQCTxtTfnzt+83T8Cx+vI2ED++VDA== - dependencies: - es6-promise "^3.2.1" - -node-releases@^2.0.14: - version "2.0.14" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b" - integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw== - -non-layered-tidy-tree-layout@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/non-layered-tidy-tree-layout/-/non-layered-tidy-tree-layout-2.0.2.tgz#57d35d13c356643fc296a55fb11ac15e74da7804" - integrity sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw== - -normalize-path@^3.0.0, normalize-path@~3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" - integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== - -normalize-range@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" - integrity sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA== - -normalize-url@^8.0.0: - version "8.0.1" - resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-8.0.1.tgz#9b7d96af9836577c58f5883e939365fa15623a4a" - integrity sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w== - -npm-run-path@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" - integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== - dependencies: - path-key "^3.0.0" - -nprogress@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/nprogress/-/nprogress-0.2.0.tgz#cb8f34c53213d895723fcbab907e9422adbcafb1" - integrity sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA== - -nth-check@^2.0.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d" - integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w== - dependencies: - boolbase "^1.0.0" - -oas-kit-common@^1.0.8: - version "1.0.8" - resolved "https://registry.yarnpkg.com/oas-kit-common/-/oas-kit-common-1.0.8.tgz#6d8cacf6e9097967a4c7ea8bcbcbd77018e1f535" - integrity sha512-pJTS2+T0oGIwgjGpw7sIRU8RQMcUoKCDWFLdBqKB2BNmGpbBMH2sdqAaOXUg8OzonZHU0L7vfJu1mJFEiYDWOQ== - dependencies: - fast-safe-stringify "^2.0.7" - -oas-linter@^3.2.2: - version "3.2.2" - resolved "https://registry.yarnpkg.com/oas-linter/-/oas-linter-3.2.2.tgz#ab6a33736313490659035ca6802dc4b35d48aa1e" - integrity sha512-KEGjPDVoU5K6swgo9hJVA/qYGlwfbFx+Kg2QB/kd7rzV5N8N5Mg6PlsoCMohVnQmo+pzJap/F610qTodKzecGQ== - dependencies: - "@exodus/schemasafe" "^1.0.0-rc.2" - should "^13.2.1" - yaml "^1.10.0" - -oas-resolver-browser@2.5.6: - version "2.5.6" - resolved "https://registry.yarnpkg.com/oas-resolver-browser/-/oas-resolver-browser-2.5.6.tgz#1974db66d594fa8c67d3aa866b46b9e2156a8b55" - integrity sha512-Jw5elT/kwUJrnGaVuRWe1D7hmnYWB8rfDDjBnpQ+RYY/dzAewGXeTexXzt4fGEo6PUE4eqKqPWF79MZxxvMppA== - dependencies: - node-fetch-h2 "^2.3.0" - oas-kit-common "^1.0.8" - path-browserify "^1.0.1" - reftools "^1.1.9" - yaml "^1.10.0" - yargs "^17.0.1" - -oas-resolver@^2.5.6: - version "2.5.6" - resolved "https://registry.yarnpkg.com/oas-resolver/-/oas-resolver-2.5.6.tgz#10430569cb7daca56115c915e611ebc5515c561b" - integrity sha512-Yx5PWQNZomfEhPPOphFbZKi9W93CocQj18NlD2Pa4GWZzdZpSJvYwoiuurRI7m3SpcChrnO08hkuQDL3FGsVFQ== - dependencies: - node-fetch-h2 "^2.3.0" - oas-kit-common "^1.0.8" - reftools "^1.1.9" - yaml "^1.10.0" - yargs "^17.0.1" - -oas-schema-walker@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/oas-schema-walker/-/oas-schema-walker-1.1.5.tgz#74c3cd47b70ff8e0b19adada14455b5d3ac38a22" - integrity sha512-2yucenq1a9YPmeNExoUa9Qwrt9RFkjqaMAA1X+U7sbb0AqBeTIdMHky9SQQ6iN94bO5NW0W4TRYXerG+BdAvAQ== - -oas-validator@^5.0.8: - version "5.0.8" - resolved "https://registry.yarnpkg.com/oas-validator/-/oas-validator-5.0.8.tgz#387e90df7cafa2d3ffc83b5fb976052b87e73c28" - integrity sha512-cu20/HE5N5HKqVygs3dt94eYJfBi0TsZvPVXDhbXQHiEityDN+RROTleefoKRKKJ9dFAF2JBkDHgvWj0sjKGmw== - dependencies: - call-me-maybe "^1.0.1" - oas-kit-common "^1.0.8" - oas-linter "^3.2.2" - oas-resolver "^2.5.6" - oas-schema-walker "^1.1.5" - reftools "^1.1.9" - should "^13.2.1" - yaml "^1.10.0" - -object-assign@^4.0.1, object-assign@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" - integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== - -object-hash@3.0.0, object-hash@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9" - integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw== - -object-inspect@^1.13.1: - version "1.13.1" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2" - integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ== - -object-is@^1.1.5: - version "1.1.6" - resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.6.tgz#1a6a53aed2dd8f7e6775ff870bea58545956ab07" - integrity sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q== - dependencies: - call-bind "^1.0.7" - define-properties "^1.2.1" - -object-keys@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" - integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== - -object.assign@^4.1.0, object.assign@^4.1.4: - version "4.1.5" - resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.5.tgz#3a833f9ab7fdb80fc9e8d2300c803d216d8fdbb0" - integrity sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ== - dependencies: - call-bind "^1.0.5" - define-properties "^1.2.1" - has-symbols "^1.0.3" - object-keys "^1.1.1" - -obuf@^1.0.0, obuf@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" - integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== - -on-finished@2.4.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" - integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== - dependencies: - ee-first "1.1.1" - -on-headers@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" - integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== - -once@^1.3.0, once@^1.3.1, once@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== - dependencies: - wrappy "1" - -onetime@^5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" - integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== - dependencies: - mimic-fn "^2.1.0" - -open@^8.0.9, open@^8.4.0: - version "8.4.2" - resolved "https://registry.yarnpkg.com/open/-/open-8.4.2.tgz#5b5ffe2a8f793dcd2aad73e550cb87b59cb084f9" - integrity sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ== - dependencies: - define-lazy-prop "^2.0.0" - is-docker "^2.1.1" - is-wsl "^2.2.0" - -openapi-to-postmanv2@^4.21.0: - version "4.21.0" - resolved "https://registry.yarnpkg.com/openapi-to-postmanv2/-/openapi-to-postmanv2-4.21.0.tgz#4bc5b19ccbd1514c2b3466268a7f5dd64b61f535" - integrity sha512-UyHf2xOjDfl4bIaYrUP6w4UiR894gsiOt1HR93cWOxv33Ucw4rGG0B6ewQczSGBpvFMgri+KSSykNEVjsmB55w== - dependencies: - ajv "8.11.0" - ajv-draft-04 "1.0.0" - ajv-formats "2.1.1" - async "3.2.4" - commander "2.20.3" - graphlib "2.1.8" - js-yaml "4.1.0" - json-schema-merge-allof "0.8.1" - lodash "4.17.21" - oas-resolver-browser "2.5.6" - object-hash "3.0.0" - path-browserify "1.0.1" - postman-collection "4.2.1" - swagger2openapi "7.0.8" - traverse "0.6.6" - yaml "1.10.2" - -opener@^1.5.2: - version "1.5.2" - resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598" - integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A== - -os-browserify@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" - integrity sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A== - -p-cancelable@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-3.0.0.tgz#63826694b54d61ca1c20ebcb6d3ecf5e14cd8050" - integrity sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw== - -p-limit@^2.0.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" - integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== - dependencies: - p-try "^2.0.0" - -p-limit@^3.0.2: - version "3.1.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" - integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== - dependencies: - yocto-queue "^0.1.0" - -p-limit@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-4.0.0.tgz#914af6544ed32bfa54670b061cafcbd04984b644" - integrity sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ== - dependencies: - yocto-queue "^1.0.0" - -p-locate@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" - integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== - dependencies: - p-limit "^2.0.0" - -p-locate@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" - integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== - dependencies: - p-limit "^3.0.2" - -p-locate@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-6.0.0.tgz#3da9a49d4934b901089dca3302fa65dc5a05c04f" - integrity sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw== - dependencies: - p-limit "^4.0.0" - -p-map@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" - integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== - dependencies: - aggregate-error "^3.0.0" - -p-retry@^4.5.0: - version "4.6.2" - resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-4.6.2.tgz#9baae7184057edd4e17231cee04264106e092a16" - integrity sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ== - dependencies: - "@types/retry" "0.12.0" - retry "^0.13.1" - -p-try@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" - integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== - -package-json@^8.1.0: - version "8.1.1" - resolved "https://registry.yarnpkg.com/package-json/-/package-json-8.1.1.tgz#3e9948e43df40d1e8e78a85485f1070bf8f03dc8" - integrity sha512-cbH9IAIJHNj9uXi196JVsRlt7cHKak6u/e6AkL/bkRelZ7rlL3X1YKxsZwa36xipOEKAsdtmaG6aAJoM1fx2zA== - dependencies: - got "^12.1.0" - registry-auth-token "^5.0.1" - registry-url "^6.0.0" - semver "^7.3.7" - -pako@~1.0.5: - version "1.0.11" - resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" - integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== - -param-case@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/param-case/-/param-case-3.0.4.tgz#7d17fe4aa12bde34d4a77d91acfb6219caad01c5" - integrity sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A== - dependencies: - dot-case "^3.0.4" - tslib "^2.0.3" - -parent-module@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" - integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== - dependencies: - callsites "^3.0.0" - -parse-asn1@^5.0.0, parse-asn1@^5.1.7: - version "5.1.7" - resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.7.tgz#73cdaaa822125f9647165625eb45f8a051d2df06" - integrity sha512-CTM5kuWR3sx9IFamcl5ErfPl6ea/N8IYwiJ+vpeB2g+1iknv7zBl5uPwbMbRVznRVbrNY6lGuDoE5b30grmbqg== - dependencies: - 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" - -parse-entities@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-4.0.1.tgz#4e2a01111fb1c986549b944af39eeda258fc9e4e" - integrity sha512-SWzvYcSJh4d/SGLIOQfZ/CoNv6BTlI6YEQ7Nj82oDVnRpwe/Z/F1EMx42x3JAOwGBlCjeCH0BRJQbQ/opHL17w== - dependencies: - "@types/unist" "^2.0.0" - character-entities "^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" - -parse-json@^5.0.0, parse-json@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" - integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== - dependencies: - "@babel/code-frame" "^7.0.0" - error-ex "^1.3.1" - json-parse-even-better-errors "^2.3.0" - lines-and-columns "^1.1.6" - -parse-numeric-range@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/parse-numeric-range/-/parse-numeric-range-1.3.0.tgz#7c63b61190d61e4d53a1197f0c83c47bb670ffa3" - integrity sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ== - -parse5-htmlparser2-tree-adapter@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz#23c2cc233bcf09bb7beba8b8a69d46b08c62c2f1" - integrity sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g== - dependencies: - domhandler "^5.0.2" - parse5 "^7.0.0" - -parse5@^6.0.0: - version "6.0.1" - resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" - integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== - -parse5@^7.0.0: - version "7.1.2" - resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32" - integrity sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw== - dependencies: - entities "^4.4.0" - -parseurl@~1.3.2, parseurl@~1.3.3: - version "1.3.3" - resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" - integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== - -pascal-case@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/pascal-case/-/pascal-case-3.1.2.tgz#b48e0ef2b98e205e7c1dae747d0b1508237660eb" - integrity sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g== - dependencies: - no-case "^3.0.4" - tslib "^2.0.3" - -path-browserify@1.0.1, path-browserify@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd" - integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g== - -path-exists@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" - integrity sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ== - -path-exists@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" - integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== - -path-exists@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-5.0.0.tgz#a6aad9489200b21fab31e49cf09277e5116fb9e7" - integrity sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ== - -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== - -path-is-inside@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" - integrity sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w== - -path-key@^3.0.0, path-key@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" - integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== - -path-parse@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" - integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== - -path-root-regex@^0.1.0: - version "0.1.2" - resolved "https://registry.yarnpkg.com/path-root-regex/-/path-root-regex-0.1.2.tgz#bfccdc8df5b12dc52c8b43ec38d18d72c04ba96d" - integrity sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ== - -path-root@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/path-root/-/path-root-0.1.1.tgz#9a4a6814cac1c0cd73360a95f32083c8ea4745b7" - integrity sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg== - dependencies: - path-root-regex "^0.1.0" - -path-scurry@^1.11.1: - version "1.11.1" - resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2" - integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== - dependencies: - lru-cache "^10.2.0" - minipass "^5.0.0 || ^6.0.2 || ^7.0.0" - -path-to-regexp@0.1.7: - version "0.1.7" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" - integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== - -path-to-regexp@2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-2.2.1.tgz#90b617025a16381a879bc82a38d4e8bdeb2bcf45" - integrity sha512-gu9bD6Ta5bwGrrU8muHzVOBFFREpp2iRkVfhBJahwJ6p6Xw20SjT0MxLnwkjOibQmGSYhiUnf2FLe7k+jcFmGQ== - -path-to-regexp@^1.7.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a" - integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA== - dependencies: - isarray "0.0.1" - -path-type@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" - integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== - -path@0.12.7: - version "0.12.7" - resolved "https://registry.yarnpkg.com/path/-/path-0.12.7.tgz#d4dc2a506c4ce2197eb481ebfcd5b36c0140b10f" - integrity sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q== - dependencies: - process "^0.11.1" - util "^0.10.3" - -pbkdf2@^3.0.3, pbkdf2@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.2.tgz#dd822aa0887580e52f1a039dc3eda108efae3075" - integrity sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA== - dependencies: - create-hash "^1.1.2" - create-hmac "^1.1.4" - ripemd160 "^2.0.1" - safe-buffer "^5.0.1" - sha.js "^2.4.8" - -pend@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" - integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg== - -periscopic@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/periscopic/-/periscopic-3.1.0.tgz#7e9037bf51c5855bd33b48928828db4afa79d97a" - integrity sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw== - dependencies: - "@types/estree" "^1.0.0" - estree-walker "^3.0.0" - is-reference "^3.0.0" - -picocolors@^1.0.0, picocolors@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1" - integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew== - -picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" - integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== - -pify@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" - integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog== - -pirates@^4.0.1: - version "4.0.6" - resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9" - integrity sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg== - -pkg-dir@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-7.0.0.tgz#8f0c08d6df4476756c5ff29b3282d0bab7517d11" - integrity sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA== - dependencies: - find-up "^6.3.0" - -pkg-up@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-3.1.0.tgz#100ec235cc150e4fd42519412596a28512a0def5" - integrity sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA== - dependencies: - find-up "^3.0.0" - -pluralize@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1" - integrity sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA== - -possible-typed-array-names@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f" - integrity sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q== - -postcss-calc@^9.0.1: - version "9.0.1" - resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-9.0.1.tgz#a744fd592438a93d6de0f1434c572670361eb6c6" - integrity sha512-TipgjGyzP5QzEhsOZUaIkeO5mKeMFpebWzRogWG/ysonUlnHcq5aJe0jOjpfzUU8PeSaBQnrE8ehR0QA5vs8PQ== - dependencies: - postcss-selector-parser "^6.0.11" - postcss-value-parser "^4.2.0" - -postcss-colormin@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-6.1.0.tgz#076e8d3fb291fbff7b10e6b063be9da42ff6488d" - integrity sha512-x9yX7DOxeMAR+BgGVnNSAxmAj98NX/YxEMNFP+SDCEeNLb2r3i6Hh1ksMsnW8Ub5SLCpbescQqn9YEbE9554Sw== - dependencies: - browserslist "^4.23.0" - caniuse-api "^3.0.0" - colord "^2.9.3" - postcss-value-parser "^4.2.0" - -postcss-convert-values@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-6.1.0.tgz#3498387f8efedb817cbc63901d45bd1ceaa40f48" - integrity sha512-zx8IwP/ts9WvUM6NkVSkiU902QZL1bwPhaVaLynPtCsOTqp+ZKbNi+s6XJg3rfqpKGA/oc7Oxk5t8pOQJcwl/w== - dependencies: - browserslist "^4.23.0" - postcss-value-parser "^4.2.0" - -postcss-discard-comments@^6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-6.0.2.tgz#e768dcfdc33e0216380623652b0a4f69f4678b6c" - integrity sha512-65w/uIqhSBBfQmYnG92FO1mWZjJ4GL5b8atm5Yw2UgrwD7HiNiSSNwJor1eCFGzUgYnN/iIknhNRVqjrrpuglw== - -postcss-discard-duplicates@^6.0.3: - version "6.0.3" - resolved "https://registry.yarnpkg.com/postcss-discard-duplicates/-/postcss-discard-duplicates-6.0.3.tgz#d121e893c38dc58a67277f75bb58ba43fce4c3eb" - integrity sha512-+JA0DCvc5XvFAxwx6f/e68gQu/7Z9ud584VLmcgto28eB8FqSFZwtrLwB5Kcp70eIoWP/HXqz4wpo8rD8gpsTw== - -postcss-discard-empty@^6.0.3: - version "6.0.3" - resolved "https://registry.yarnpkg.com/postcss-discard-empty/-/postcss-discard-empty-6.0.3.tgz#ee39c327219bb70473a066f772621f81435a79d9" - integrity sha512-znyno9cHKQsK6PtxL5D19Fj9uwSzC2mB74cpT66fhgOadEUPyXFkbgwm5tvc3bt3NAy8ltE5MrghxovZRVnOjQ== - -postcss-discard-overridden@^6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-6.0.2.tgz#4e9f9c62ecd2df46e8fdb44dc17e189776572e2d" - integrity sha512-j87xzI4LUggC5zND7KdjsI25APtyMuynXZSujByMaav2roV6OZX+8AaCUcZSWqckZpjAjRyFDdpqybgjFO0HJQ== - -postcss-discard-unused@^6.0.5: - version "6.0.5" - resolved "https://registry.yarnpkg.com/postcss-discard-unused/-/postcss-discard-unused-6.0.5.tgz#c1b0e8c032c6054c3fbd22aaddba5b248136f338" - integrity sha512-wHalBlRHkaNnNwfC8z+ppX57VhvS+HWgjW508esjdaEYr3Mx7Gnn2xA4R/CKf5+Z9S5qsqC+Uzh4ueENWwCVUA== - dependencies: - postcss-selector-parser "^6.0.16" - -postcss-import@^15.1.0: - version "15.1.0" - resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-15.1.0.tgz#41c64ed8cc0e23735a9698b3249ffdbf704adc70" - integrity sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew== - dependencies: - postcss-value-parser "^4.0.0" - read-cache "^1.0.0" - resolve "^1.1.7" - -postcss-js@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-js/-/postcss-js-4.0.1.tgz#61598186f3703bab052f1c4f7d805f3991bee9d2" - integrity sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw== - dependencies: - camelcase-css "^2.0.1" - -postcss-load-config@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-4.0.2.tgz#7159dcf626118d33e299f485d6afe4aff7c4a3e3" - integrity sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ== - dependencies: - lilconfig "^3.0.0" - yaml "^2.3.4" - -postcss-loader@^7.3.3: - version "7.3.4" - resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-7.3.4.tgz#aed9b79ce4ed7e9e89e56199d25ad1ec8f606209" - integrity sha512-iW5WTTBSC5BfsBJ9daFMPVrLT36MrNiC6fqOZTTaHjBNX6Pfd5p+hSBqe/fEeNd7pc13QiAyGt7VdGMw4eRC4A== - dependencies: - cosmiconfig "^8.3.5" - jiti "^1.20.0" - semver "^7.5.4" - -postcss-merge-idents@^6.0.3: - version "6.0.3" - resolved "https://registry.yarnpkg.com/postcss-merge-idents/-/postcss-merge-idents-6.0.3.tgz#7b9c31c7bc823c94bec50f297f04e3c2b838ea65" - integrity sha512-1oIoAsODUs6IHQZkLQGO15uGEbK3EAl5wi9SS8hs45VgsxQfMnxvt+L+zIr7ifZFIH14cfAeVe2uCTa+SPRa3g== - dependencies: - cssnano-utils "^4.0.2" - postcss-value-parser "^4.2.0" - -postcss-merge-longhand@^6.0.5: - version "6.0.5" - resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-6.0.5.tgz#ba8a8d473617c34a36abbea8dda2b215750a065a" - integrity sha512-5LOiordeTfi64QhICp07nzzuTDjNSO8g5Ksdibt44d+uvIIAE1oZdRn8y/W5ZtYgRH/lnLDlvi9F8btZcVzu3w== - dependencies: - postcss-value-parser "^4.2.0" - stylehacks "^6.1.1" - -postcss-merge-rules@^6.1.1: - version "6.1.1" - resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-6.1.1.tgz#7aa539dceddab56019469c0edd7d22b64c3dea9d" - integrity sha512-KOdWF0gju31AQPZiD+2Ar9Qjowz1LTChSjFFbS+e2sFgc4uHOp3ZvVX4sNeTlk0w2O31ecFGgrFzhO0RSWbWwQ== - dependencies: - browserslist "^4.23.0" - caniuse-api "^3.0.0" - cssnano-utils "^4.0.2" - postcss-selector-parser "^6.0.16" - -postcss-minify-font-values@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/postcss-minify-font-values/-/postcss-minify-font-values-6.1.0.tgz#a0e574c02ee3f299be2846369211f3b957ea4c59" - integrity sha512-gklfI/n+9rTh8nYaSJXlCo3nOKqMNkxuGpTn/Qm0gstL3ywTr9/WRKznE+oy6fvfolH6dF+QM4nCo8yPLdvGJg== - dependencies: - postcss-value-parser "^4.2.0" - -postcss-minify-gradients@^6.0.3: - version "6.0.3" - resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-6.0.3.tgz#ca3eb55a7bdb48a1e187a55c6377be918743dbd6" - integrity sha512-4KXAHrYlzF0Rr7uc4VrfwDJ2ajrtNEpNEuLxFgwkhFZ56/7gaE4Nr49nLsQDZyUe+ds+kEhf+YAUolJiYXF8+Q== - dependencies: - colord "^2.9.3" - cssnano-utils "^4.0.2" - postcss-value-parser "^4.2.0" - -postcss-minify-params@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-6.1.0.tgz#54551dec77b9a45a29c3cb5953bf7325a399ba08" - integrity sha512-bmSKnDtyyE8ujHQK0RQJDIKhQ20Jq1LYiez54WiaOoBtcSuflfK3Nm596LvbtlFcpipMjgClQGyGr7GAs+H1uA== - dependencies: - browserslist "^4.23.0" - cssnano-utils "^4.0.2" - postcss-value-parser "^4.2.0" - -postcss-minify-selectors@^6.0.4: - version "6.0.4" - resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-6.0.4.tgz#197f7d72e6dd19eed47916d575d69dc38b396aff" - integrity sha512-L8dZSwNLgK7pjTto9PzWRoMbnLq5vsZSTu8+j1P/2GB8qdtGQfn+K1uSvFgYvgh83cbyxT5m43ZZhUMTJDSClQ== - dependencies: - postcss-selector-parser "^6.0.16" - -postcss-modules-extract-imports@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz#b4497cb85a9c0c4b5aabeb759bb25e8d89f15002" - integrity sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q== - -postcss-modules-local-by-default@^4.0.5: - version "4.0.5" - resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.5.tgz#f1b9bd757a8edf4d8556e8d0f4f894260e3df78f" - integrity sha512-6MieY7sIfTK0hYfafw1OMEG+2bg8Q1ocHCpoWLqOKj3JXlKu4G7btkmM/B7lFubYkYWmRSPLZi5chid63ZaZYw== - dependencies: - icss-utils "^5.0.0" - postcss-selector-parser "^6.0.2" - postcss-value-parser "^4.1.0" - -postcss-modules-scope@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.2.0.tgz#a43d28289a169ce2c15c00c4e64c0858e43457d5" - integrity sha512-oq+g1ssrsZOsx9M96c5w8laRmvEu9C3adDSjI8oTcbfkrTE8hx/zfyobUoWIxaKPO8bt6S62kxpw5GqypEw1QQ== - dependencies: - postcss-selector-parser "^6.0.4" - -postcss-modules-values@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz#d7c5e7e68c3bb3c9b27cbf48ca0bb3ffb4602c9c" - integrity sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ== - dependencies: - icss-utils "^5.0.0" - -postcss-nested@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-6.0.1.tgz#f83dc9846ca16d2f4fa864f16e9d9f7d0961662c" - integrity sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ== - dependencies: - postcss-selector-parser "^6.0.11" - -postcss-normalize-charset@^6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-6.0.2.tgz#1ec25c435057a8001dac942942a95ffe66f721e1" - integrity sha512-a8N9czmdnrjPHa3DeFlwqst5eaL5W8jYu3EBbTTkI5FHkfMhFZh1EGbku6jhHhIzTA6tquI2P42NtZ59M/H/kQ== - -postcss-normalize-display-values@^6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/postcss-normalize-display-values/-/postcss-normalize-display-values-6.0.2.tgz#54f02764fed0b288d5363cbb140d6950dbbdd535" - integrity sha512-8H04Mxsb82ON/aAkPeq8kcBbAtI5Q2a64X/mnRRfPXBq7XeogoQvReqxEfc0B4WPq1KimjezNC8flUtC3Qz6jg== - dependencies: - postcss-value-parser "^4.2.0" - -postcss-normalize-positions@^6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/postcss-normalize-positions/-/postcss-normalize-positions-6.0.2.tgz#e982d284ec878b9b819796266f640852dbbb723a" - integrity sha512-/JFzI441OAB9O7VnLA+RtSNZvQ0NCFZDOtp6QPFo1iIyawyXg0YI3CYM9HBy1WvwCRHnPep/BvI1+dGPKoXx/Q== - dependencies: - postcss-value-parser "^4.2.0" - -postcss-normalize-repeat-style@^6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-6.0.2.tgz#f8006942fd0617c73f049dd8b6201c3a3040ecf3" - integrity sha512-YdCgsfHkJ2jEXwR4RR3Tm/iOxSfdRt7jplS6XRh9Js9PyCR/aka/FCb6TuHT2U8gQubbm/mPmF6L7FY9d79VwQ== - dependencies: - postcss-value-parser "^4.2.0" - -postcss-normalize-string@^6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/postcss-normalize-string/-/postcss-normalize-string-6.0.2.tgz#e3cc6ad5c95581acd1fc8774b309dd7c06e5e363" - integrity sha512-vQZIivlxlfqqMp4L9PZsFE4YUkWniziKjQWUtsxUiVsSSPelQydwS8Wwcuw0+83ZjPWNTl02oxlIvXsmmG+CiQ== - dependencies: - postcss-value-parser "^4.2.0" - -postcss-normalize-timing-functions@^6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-6.0.2.tgz#40cb8726cef999de984527cbd9d1db1f3e9062c0" - integrity sha512-a+YrtMox4TBtId/AEwbA03VcJgtyW4dGBizPl7e88cTFULYsprgHWTbfyjSLyHeBcK/Q9JhXkt2ZXiwaVHoMzA== - dependencies: - postcss-value-parser "^4.2.0" - -postcss-normalize-unicode@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/postcss-normalize-unicode/-/postcss-normalize-unicode-6.1.0.tgz#aaf8bbd34c306e230777e80f7f12a4b7d27ce06e" - integrity sha512-QVC5TQHsVj33otj8/JD869Ndr5Xcc/+fwRh4HAsFsAeygQQXm+0PySrKbr/8tkDKzW+EVT3QkqZMfFrGiossDg== - dependencies: - browserslist "^4.23.0" - postcss-value-parser "^4.2.0" - -postcss-normalize-url@^6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/postcss-normalize-url/-/postcss-normalize-url-6.0.2.tgz#292792386be51a8de9a454cb7b5c58ae22db0f79" - integrity sha512-kVNcWhCeKAzZ8B4pv/DnrU1wNh458zBNp8dh4y5hhxih5RZQ12QWMuQrDgPRw3LRl8mN9vOVfHl7uhvHYMoXsQ== - dependencies: - postcss-value-parser "^4.2.0" - -postcss-normalize-whitespace@^6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/postcss-normalize-whitespace/-/postcss-normalize-whitespace-6.0.2.tgz#fbb009e6ebd312f8b2efb225c2fcc7cf32b400cd" - integrity sha512-sXZ2Nj1icbJOKmdjXVT9pnyHQKiSAyuNQHSgRCUgThn2388Y9cGVDR+E9J9iAYbSbLHI+UUwLVl1Wzco/zgv0Q== - dependencies: - postcss-value-parser "^4.2.0" - -postcss-ordered-values@^6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-6.0.2.tgz#366bb663919707093451ab70c3f99c05672aaae5" - integrity sha512-VRZSOB+JU32RsEAQrO94QPkClGPKJEL/Z9PCBImXMhIeK5KAYo6slP/hBYlLgrCjFxyqvn5VC81tycFEDBLG1Q== - dependencies: - cssnano-utils "^4.0.2" - postcss-value-parser "^4.2.0" - -postcss-reduce-idents@^6.0.3: - version "6.0.3" - resolved "https://registry.yarnpkg.com/postcss-reduce-idents/-/postcss-reduce-idents-6.0.3.tgz#b0d9c84316d2a547714ebab523ec7d13704cd486" - integrity sha512-G3yCqZDpsNPoQgbDUy3T0E6hqOQ5xigUtBQyrmq3tn2GxlyiL0yyl7H+T8ulQR6kOcHJ9t7/9H4/R2tv8tJbMA== - dependencies: - postcss-value-parser "^4.2.0" - -postcss-reduce-initial@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-6.1.0.tgz#4401297d8e35cb6e92c8e9586963e267105586ba" - integrity sha512-RarLgBK/CrL1qZags04oKbVbrrVK2wcxhvta3GCxrZO4zveibqbRPmm2VI8sSgCXwoUHEliRSbOfpR0b/VIoiw== - dependencies: - browserslist "^4.23.0" - caniuse-api "^3.0.0" - -postcss-reduce-transforms@^6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-6.0.2.tgz#6fa2c586bdc091a7373caeee4be75a0f3e12965d" - integrity sha512-sB+Ya++3Xj1WaT9+5LOOdirAxP7dJZms3GRcYheSPi1PiTMigsxHAdkrbItHxwYHr4kt1zL7mmcHstgMYT+aiA== - dependencies: - postcss-value-parser "^4.2.0" - -postcss-selector-parser@^6.0.11, postcss-selector-parser@^6.0.16, postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4: - version "6.1.0" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.1.0.tgz#49694cb4e7c649299fea510a29fa6577104bcf53" - integrity sha512-UMz42UD0UY0EApS0ZL9o1XnLhSTtvvvLe5Dc2H2O56fvRZi+KulDyf5ctDhhtYJBGKStV2FL1fy6253cmLgqVQ== - dependencies: - cssesc "^3.0.0" - util-deprecate "^1.0.2" - -postcss-sort-media-queries@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/postcss-sort-media-queries/-/postcss-sort-media-queries-5.2.0.tgz#4556b3f982ef27d3bac526b99b6c0d3359a6cf97" - integrity sha512-AZ5fDMLD8SldlAYlvi8NIqo0+Z8xnXU2ia0jxmuhxAU+Lqt9K+AlmLNJ/zWEnE9x+Zx3qL3+1K20ATgNOr3fAA== - dependencies: - sort-css-media-queries "2.2.0" - -postcss-svgo@^6.0.3: - version "6.0.3" - resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-6.0.3.tgz#1d6e180d6df1fa8a3b30b729aaa9161e94f04eaa" - integrity sha512-dlrahRmxP22bX6iKEjOM+c8/1p+81asjKT+V5lrgOH944ryx/OHpclnIbGsKVd3uWOXFLYJwCVf0eEkJGvO96g== - dependencies: - postcss-value-parser "^4.2.0" - svgo "^3.2.0" - -postcss-unique-selectors@^6.0.4: - version "6.0.4" - resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-6.0.4.tgz#983ab308896b4bf3f2baaf2336e14e52c11a2088" - integrity sha512-K38OCaIrO8+PzpArzkLKB42dSARtC2tmG6PvD4b1o1Q2E9Os8jzfWFfSy/rixsHwohtsDdFtAWGjFVFUdwYaMg== - dependencies: - postcss-selector-parser "^6.0.16" - -postcss-value-parser@^4.0.0, postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" - integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== - -postcss-zindex@^6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/postcss-zindex/-/postcss-zindex-6.0.2.tgz#e498304b83a8b165755f53db40e2ea65a99b56e1" - integrity sha512-5BxW9l1evPB/4ZIc+2GobEBoKC+h8gPGCMi+jxsYvd2x0mjq7wazk6DrP71pStqxE9Foxh5TVnonbWpFZzXaYg== - -postcss@^8.4.21, postcss@^8.4.23, postcss@^8.4.24, postcss@^8.4.26, postcss@^8.4.31, postcss@^8.4.33, postcss@^8.4.38: - version "8.4.38" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e" - integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A== - dependencies: - nanoid "^3.3.7" - picocolors "^1.0.0" - source-map-js "^1.2.0" - -postman-code-generators@^1.10.1: - version "1.10.1" - resolved "https://registry.yarnpkg.com/postman-code-generators/-/postman-code-generators-1.10.1.tgz#5d8d8500616b2bb0cac7417e923c36b2e73cbffe" - integrity sha512-VGjqIFezG6tZRP3GvSbYjLDq3bQtXUeNDnBrN5CUIbgc9siu5Ubkva9hZnFpk+/qfi+PXs3CUR5mvV+WzpMhsg== - dependencies: - async "3.2.2" - lodash "4.17.21" - path "0.12.7" - postman-collection "4.0.0" - shelljs "0.8.5" - -postman-collection@4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postman-collection/-/postman-collection-4.0.0.tgz#4a72c60f0d9725656d0e02d44e294e1c22ef3ffa" - integrity sha512-vDrXG/dclSu6RMqPqBz4ZqoQBwcj/a80sJYsQZmzWJ6dWgXiudPhwu6Vm3C1Hy7zX5W8A6am1Z6vb/TB4eyURA== - dependencies: - faker "5.5.3" - file-type "3.9.0" - http-reasons "0.1.0" - iconv-lite "0.6.3" - liquid-json "0.3.1" - lodash "4.17.21" - mime-format "2.0.1" - mime-types "2.1.31" - postman-url-encoder "3.0.1" - semver "7.3.5" - uuid "8.3.2" - -postman-collection@4.2.1: - version "4.2.1" - resolved "https://registry.yarnpkg.com/postman-collection/-/postman-collection-4.2.1.tgz#86e3c8ee11530a1f00a30864503d487846ee516f" - integrity sha512-DFLt3/yu8+ldtOTIzmBUctoupKJBOVK4NZO0t68K2lIir9smQg7OdQTBjOXYy+PDh7u0pSDvD66tm93eBHEPHA== - dependencies: - "@faker-js/faker" "5.5.3" - file-type "3.9.0" - http-reasons "0.1.0" - iconv-lite "0.6.3" - liquid-json "0.3.1" - lodash "4.17.21" - mime-format "2.0.1" - mime-types "2.1.35" - postman-url-encoder "3.0.5" - semver "7.5.4" - uuid "8.3.2" - -postman-collection@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/postman-collection/-/postman-collection-4.4.0.tgz#6acb6e3796fcd9f6ac5a94e6894185e42387d7da" - integrity sha512-2BGDFcUwlK08CqZFUlIC8kwRJueVzPjZnnokWPtJCd9f2J06HBQpGL7t2P1Ud1NEsK9NHq9wdipUhWLOPj5s/Q== - dependencies: - "@faker-js/faker" "5.5.3" - file-type "3.9.0" - http-reasons "0.1.0" - iconv-lite "0.6.3" - liquid-json "0.3.1" - lodash "4.17.21" - mime-format "2.0.1" - mime-types "2.1.35" - postman-url-encoder "3.0.5" - semver "7.5.4" - uuid "8.3.2" - -postman-url-encoder@3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/postman-url-encoder/-/postman-url-encoder-3.0.1.tgz#a7434a169567c45f022dc435d46a86d71de6a8aa" - integrity sha512-dMPqXnkDlstM2Eya+Gw4MIGWEan8TzldDcUKZIhZUsJ/G5JjubfQPhFhVWKzuATDMvwvrWbSjF+8VmAvbu6giw== - dependencies: - punycode "^2.1.1" - -postman-url-encoder@3.0.5: - version "3.0.5" - resolved "https://registry.yarnpkg.com/postman-url-encoder/-/postman-url-encoder-3.0.5.tgz#af2efee3bb7644e2b059d8a78bc8070fae0467a5" - integrity sha512-jOrdVvzUXBC7C+9gkIkpDJ3HIxOHTIqjpQ4C1EMt1ZGeMvSEpbFCKq23DEfgsj46vMnDgyQf+1ZLp2Wm+bKSsA== - dependencies: - punycode "^2.1.1" - -pretty-error@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-4.0.0.tgz#90a703f46dd7234adb46d0f84823e9d1cb8f10d6" - integrity sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw== - dependencies: - lodash "^4.17.20" - renderkid "^3.0.0" - -pretty-time@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/pretty-time/-/pretty-time-1.1.0.tgz#ffb7429afabb8535c346a34e41873adf3d74dd0e" - integrity sha512-28iF6xPQrP8Oa6uxE6a1biz+lWeTOAPKggvjB8HAs6nVMKZwf5bG++632Dx614hIWgUPkgivRfG+a8uAXGTIbA== - -prism-react-renderer@^2.0.6, prism-react-renderer@^2.1.0, prism-react-renderer@^2.3.0: - version "2.3.1" - resolved "https://registry.yarnpkg.com/prism-react-renderer/-/prism-react-renderer-2.3.1.tgz#e59e5450052ede17488f6bc85de1553f584ff8d5" - integrity sha512-Rdf+HzBLR7KYjzpJ1rSoxT9ioO85nZngQEoFIhL07XhtJHlCU3SOz0GJ6+qvMyQe0Se+BV3qpe6Yd/NmQF5Juw== - dependencies: - "@types/prismjs" "^1.26.0" - clsx "^2.0.0" - -prismjs@^1.29.0: - version "1.29.0" - resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.29.0.tgz#f113555a8fa9b57c35e637bba27509dcf802dd12" - integrity sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q== - -process-nextick-args@~2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" - integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== - -process@^0.11.1, process@^0.11.10: - version "0.11.10" - resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" - integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== - -progress@2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" - integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== - -prompts@^2.4.2: - version "2.4.2" - resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" - integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q== - dependencies: - kleur "^3.0.3" - sisteransi "^1.0.5" - -prop-types@^15.0.0, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: - version "15.8.1" - resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" - integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== - dependencies: - loose-envify "^1.4.0" - object-assign "^4.1.1" - react-is "^16.13.1" - -property-information@^6.0.0: - version "6.5.0" - resolved "https://registry.yarnpkg.com/property-information/-/property-information-6.5.0.tgz#6212fbb52ba757e92ef4fb9d657563b933b7ffec" - integrity sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig== - -proto-list@~1.2.1: - version "1.2.4" - resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" - integrity sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA== - -proxy-addr@~2.0.7: - version "2.0.7" - resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" - integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== - dependencies: - forwarded "0.2.0" - ipaddr.js "1.9.1" - -proxy-from-env@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" - integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== - -public-encrypt@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0" - integrity sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q== - dependencies: - bn.js "^4.1.0" - browserify-rsa "^4.0.0" - create-hash "^1.1.0" - parse-asn1 "^5.0.0" - randombytes "^2.0.1" - safe-buffer "^5.1.2" - -pump@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" - integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== - dependencies: - end-of-stream "^1.1.0" - once "^1.3.1" - -punycode@^1.3.2, punycode@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" - integrity sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ== - -punycode@^2.1.0, punycode@^2.1.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" - integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== - -pupa@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/pupa/-/pupa-3.1.0.tgz#f15610274376bbcc70c9a3aa8b505ea23f41c579" - integrity sha512-FLpr4flz5xZTSJxSeaheeMKN/EDzMdK7b8PTOC6a5PYFKTucWbdqjgqaEyH0shFiSJrVB1+Qqi4Tk19ccU6Aug== - dependencies: - escape-goat "^4.0.0" - -puppeteer-core@18.2.1: - version "18.2.1" - resolved "https://registry.yarnpkg.com/puppeteer-core/-/puppeteer-core-18.2.1.tgz#9b7827bb2bf478bb615e2c21425e4659555dc1fe" - integrity sha512-MRtTAZfQTluz3U2oU/X2VqVWPcR1+94nbA2V6ZrSZRVEwLqZ8eclZ551qGFQD/vD2PYqHJwWOW/fpC721uznVw== - dependencies: - cross-fetch "3.1.5" - debug "4.3.4" - devtools-protocol "0.0.1045489" - extract-zip "2.0.1" - https-proxy-agent "5.0.1" - proxy-from-env "1.1.0" - rimraf "3.0.2" - tar-fs "2.1.1" - unbzip2-stream "1.4.3" - ws "8.9.0" - -puppeteer@^18.0.0: - version "18.2.1" - resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-18.2.1.tgz#08967cd423efe511ee4c6e3a5c882ffaf2e6bbf3" - integrity sha512-7+UhmYa7wxPh2oMRwA++k8UGVDxh3YdWFB52r9C3tM81T6BU7cuusUSxImz0GEYSOYUKk/YzIhkQ6+vc0gHbxQ== - dependencies: - https-proxy-agent "5.0.1" - progress "2.0.3" - proxy-from-env "1.1.0" - puppeteer-core "18.2.1" - -qs@6.11.0: - version "6.11.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" - integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== - dependencies: - side-channel "^1.0.4" - -qs@^6.11.2: - version "6.12.1" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.12.1.tgz#39422111ca7cbdb70425541cba20c7d7b216599a" - integrity sha512-zWmv4RSuB9r2mYQw3zxQuHWeU+42aKi1wWig/j4ele4ygELZ7PEO6MM7rim9oAQH2A5MWfsAVf/jPvTPgCbvUQ== - dependencies: - side-channel "^1.0.6" - -querystring-es3@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" - integrity sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA== - -queue-microtask@^1.2.2: - version "1.2.3" - resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" - integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== - -queue@6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/queue/-/queue-6.0.2.tgz#b91525283e2315c7553d2efa18d83e76432fed65" - integrity sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA== - dependencies: - inherits "~2.0.3" - -quick-lru@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" - integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== - -randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" - integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== - dependencies: - safe-buffer "^5.1.0" - -randomfill@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/randomfill/-/randomfill-1.0.4.tgz#c92196fc86ab42be983f1bf31778224931d61458" - integrity sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw== - dependencies: - randombytes "^2.0.5" - safe-buffer "^5.1.0" - -range-parser@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" - integrity sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A== - -range-parser@^1.2.1, range-parser@~1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" - integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== - -raw-body@2.5.2: - version "2.5.2" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" - integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== - dependencies: - bytes "3.1.2" - http-errors "2.0.0" - iconv-lite "0.4.24" - unpipe "1.0.0" - -raw-loader@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-4.0.2.tgz#1aac6b7d1ad1501e66efdac1522c73e59a584eb6" - integrity sha512-ZnScIV3ag9A4wPX/ZayxL/jZH+euYb6FcUinPcgiQW0+UBtEv0O6Q3lGd3cqJ+GHH+rksEv3Pj99oxJ3u3VIKA== - dependencies: - loader-utils "^2.0.0" - schema-utils "^3.0.0" - -rc@1.2.8: - version "1.2.8" - resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" - integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== - dependencies: - deep-extend "^0.6.0" - ini "~1.3.0" - minimist "^1.2.0" - strip-json-comments "~2.0.1" - -react-copy-to-clipboard@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/react-copy-to-clipboard/-/react-copy-to-clipboard-5.1.0.tgz#09aae5ec4c62750ccb2e6421a58725eabc41255c" - integrity sha512-k61RsNgAayIJNoy9yDsYzDe/yAZAzEbEgcz3DZMhF686LEyukcE1hzurxe85JandPUG+yTfGVFzuEw3xt8WP/A== - dependencies: - copy-to-clipboard "^3.3.1" - prop-types "^15.8.1" - -react-dev-utils@^12.0.1: - version "12.0.1" - resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-12.0.1.tgz#ba92edb4a1f379bd46ccd6bcd4e7bc398df33e73" - integrity sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ== - dependencies: - "@babel/code-frame" "^7.16.0" - address "^1.1.2" - browserslist "^4.18.1" - chalk "^4.1.2" - cross-spawn "^7.0.3" - detect-port-alt "^1.1.6" - escape-string-regexp "^4.0.0" - filesize "^8.0.6" - find-up "^5.0.0" - fork-ts-checker-webpack-plugin "^6.5.0" - global-modules "^2.0.0" - globby "^11.0.4" - gzip-size "^6.0.0" - immer "^9.0.7" - is-root "^2.1.0" - loader-utils "^3.2.0" - open "^8.4.0" - pkg-up "^3.1.0" - prompts "^2.4.2" - react-error-overlay "^6.0.11" - recursive-readdir "^2.2.2" - shell-quote "^1.7.3" - strip-ansi "^6.0.1" - text-table "^0.2.0" - -react-dom@^18.2.0: - version "18.3.1" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4" - integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw== - dependencies: - loose-envify "^1.1.0" - scheduler "^0.23.2" - -react-error-overlay@^6.0.11: - version "6.0.11" - resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.11.tgz#92835de5841c5cf08ba00ddd2d677b6d17ff9adb" - integrity sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg== - -react-fast-compare@^3.0.1, react-fast-compare@^3.2.0, react-fast-compare@^3.2.2: - version "3.2.2" - resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.2.tgz#929a97a532304ce9fee4bcae44234f1ce2c21d49" - integrity sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ== - -react-google-charts@^5.2.1: - version "5.2.1" - resolved "https://registry.yarnpkg.com/react-google-charts/-/react-google-charts-5.2.1.tgz#d9cbe8ed45d7c0fafefea5c7c3361bee76648454" - integrity sha512-mCbPiObP8yWM5A9ogej7Qp3/HX4EzOwuEzUYvcfHtL98Xt4V/brD14KgfDzSNNtyD48MNXCpq5oVaYKt0ykQUQ== - -react-helmet-async@*: - version "2.0.5" - resolved "https://registry.yarnpkg.com/react-helmet-async/-/react-helmet-async-2.0.5.tgz#cfc70cd7bb32df7883a8ed55502a1513747223ec" - integrity sha512-rYUYHeus+i27MvFE+Jaa4WsyBKGkL6qVgbJvSBoX8mbsWoABJXdEO0bZyi0F6i+4f0NuIb8AvqPMj3iXFHkMwg== - dependencies: - invariant "^2.2.4" - react-fast-compare "^3.2.2" - shallowequal "^1.1.0" - -react-helmet-async@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/react-helmet-async/-/react-helmet-async-1.3.0.tgz#7bd5bf8c5c69ea9f02f6083f14ce33ef545c222e" - integrity sha512-9jZ57/dAn9t3q6hneQS0wukqC2ENOBgMNVEhb/ZG9ZSxUetzVIw4iAmEU38IaVg3QGYauQPhSeUTuIUtFglWpg== - dependencies: - "@babel/runtime" "^7.12.5" - invariant "^2.2.4" - prop-types "^15.7.2" - react-fast-compare "^3.2.0" - shallowequal "^1.1.0" - -react-hook-form@^7.43.8: - version "7.52.0" - resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.52.0.tgz#e52b33043e283719586b9dd80f6d51b68dd3999c" - integrity sha512-mJX506Xc6mirzLsmXUJyqlAI3Kj9Ph2RhplYhUVffeOQSnubK2uVqBFOBJmvKikvbFV91pxVXmDiR+QMF19x6A== - -react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0: - version "16.13.1" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" - integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== - -react-is@^17.0.2: - version "17.0.2" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" - integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== - -react-is@^18.0.0: - version "18.3.1" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e" - integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== - -react-json-view-lite@^1.2.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/react-json-view-lite/-/react-json-view-lite-1.4.0.tgz#0ff493245f4550abe5e1f1836f170fa70bb95914" - integrity sha512-wh6F6uJyYAmQ4fK0e8dSQMEWuvTs2Wr3el3sLD9bambX1+pSWUVXIz1RFaoy3TI1mZ0FqdpKq9YgbgTTgyrmXA== - -react-lifecycles-compat@^3.0.0: - version "3.0.4" - resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" - integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== - -react-live@^4.0.0: - version "4.1.6" - resolved "https://registry.yarnpkg.com/react-live/-/react-live-4.1.6.tgz#6d9b7d381bd2b359ca859767501135112b6bab33" - integrity sha512-2oq3MADi3rupqZcdoHMrV9p+Eg/92BDds278ZuoOz8d68qw6ct0xZxX89MRxeChrnFHy1XPr8BVknDJNJNdvVw== - dependencies: - prism-react-renderer "^2.0.6" - sucrase "^3.31.0" - use-editable "^2.3.3" - -react-loadable-ssr-addon-v5-slorber@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/react-loadable-ssr-addon-v5-slorber/-/react-loadable-ssr-addon-v5-slorber-1.0.1.tgz#2cdc91e8a744ffdf9e3556caabeb6e4278689883" - integrity sha512-lq3Lyw1lGku8zUEJPDxsNm1AfYHBrO9Y1+olAYwpUJ2IGFBskM0DMKok97A6LWUpHm+o7IvQBOWu9MLenp9Z+A== - dependencies: - "@babel/runtime" "^7.10.3" - -"react-loadable@npm:@docusaurus/react-loadable@6.0.0": - version "6.0.0" - resolved "https://registry.yarnpkg.com/@docusaurus/react-loadable/-/react-loadable-6.0.0.tgz#de6c7f73c96542bd70786b8e522d535d69069dc4" - integrity sha512-YMMxTUQV/QFSnbgrP3tjDzLHRg7vsbMn8e9HAa8o/1iXoiomo48b7sk/kkmWEuWNDPJVlKSJRB6Y2fHqdJk+SQ== - dependencies: - "@types/react" "*" - -react-magic-dropzone@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/react-magic-dropzone/-/react-magic-dropzone-1.0.1.tgz#bfd25b77b57e7a04aaef0a28910563b707ee54df" - integrity sha512-0BIROPARmXHpk4AS3eWBOsewxoM5ndk2psYP/JmbCq8tz3uR2LIV1XiroZ9PKrmDRMctpW+TvsBCtWasuS8vFA== - -react-markdown@^8.0.1: - version "8.0.7" - resolved "https://registry.yarnpkg.com/react-markdown/-/react-markdown-8.0.7.tgz#c8dbd1b9ba5f1c5e7e5f2a44de465a3caafdf89b" - integrity sha512-bvWbzG4MtOU62XqBx3Xx+zB2raaFFsq4mYiAzfjXJMEz2sixgeAfraA3tvzULF02ZdOMUOKTBFFaZJDDrq+BJQ== - 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" - -react-modal@^3.15.1: - version "3.16.1" - resolved "https://registry.yarnpkg.com/react-modal/-/react-modal-3.16.1.tgz#34018528fc206561b1a5467fc3beeaddafb39b2b" - integrity sha512-VStHgI3BVcGo7OXczvnJN7yT2TWHJPDXZWyI/a0ssFNhGZWsPmB8cF0z33ewDXq4VfYMO1vXgiv/g8Nj9NDyWg== - dependencies: - exenv "^1.2.0" - prop-types "^15.7.2" - react-lifecycles-compat "^3.0.0" - warning "^4.0.3" - -react-player@^2.15.1: - version "2.16.0" - resolved "https://registry.yarnpkg.com/react-player/-/react-player-2.16.0.tgz#89070700b03f5a5ded9f0b3165d4717390796481" - integrity sha512-mAIPHfioD7yxO0GNYVFD1303QFtI3lyyQZLY229UEAp/a10cSW+hPcakg0Keq8uWJxT2OiT/4Gt+Lc9bD6bJmQ== - dependencies: - deepmerge "^4.0.0" - load-script "^1.0.0" - memoize-one "^5.1.1" - prop-types "^15.7.2" - react-fast-compare "^3.0.1" - -react-redux@^7.2.0: - version "7.2.9" - resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.9.tgz#09488fbb9416a4efe3735b7235055442b042481d" - 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" - -react-router-config@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/react-router-config/-/react-router-config-5.1.1.tgz#0f4263d1a80c6b2dc7b9c1902c9526478194a988" - integrity sha512-DuanZjaD8mQp1ppHjgnnUnyOlqYXZVjnov/JzFhjLEwd3Z4dYjMSnqrEzzGThH47vpCOqPPwJM2FtthLeJ8Pbg== - dependencies: - "@babel/runtime" "^7.1.2" - -react-router-dom@^5.3.4: - version "5.3.4" - resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.3.4.tgz#2ed62ffd88cae6db134445f4a0c0ae8b91d2e5e6" - integrity sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ== - dependencies: - "@babel/runtime" "^7.12.13" - history "^4.9.0" - loose-envify "^1.3.1" - prop-types "^15.6.2" - react-router "5.3.4" - tiny-invariant "^1.0.2" - tiny-warning "^1.0.0" - -react-router@5.3.4, react-router@^5.3.4: - version "5.3.4" - resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.3.4.tgz#8ca252d70fcc37841e31473c7a151cf777887bb5" - integrity sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA== - dependencies: - "@babel/runtime" "^7.12.13" - history "^4.9.0" - hoist-non-react-statics "^3.1.0" - loose-envify "^1.3.1" - path-to-regexp "^1.7.0" - prop-types "^15.6.2" - react-is "^16.6.0" - tiny-invariant "^1.0.2" - tiny-warning "^1.0.0" - -react@^18.2.0: - version "18.3.1" - resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891" - integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ== - dependencies: - loose-envify "^1.1.0" - -read-cache@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774" - integrity sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA== - dependencies: - pify "^2.3.0" - -readable-stream@^2.0.1, readable-stream@^2.3.8: - version "2.3.8" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" - 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" - -readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.5.0, readable-stream@^3.6.0: - version "3.6.2" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" - integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - -readable-stream@^4.0.0: - version "4.5.2" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.5.2.tgz#9e7fc4c45099baeed934bff6eb97ba6cf2729e09" - integrity sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g== - dependencies: - abort-controller "^3.0.0" - buffer "^6.0.3" - events "^3.3.0" - process "^0.11.10" - string_decoder "^1.3.0" - -readdirp@~3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" - integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== - dependencies: - picomatch "^2.2.1" - -reading-time@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/reading-time/-/reading-time-1.5.0.tgz#d2a7f1b6057cb2e169beaf87113cc3411b5bc5bb" - integrity sha512-onYyVhBNr4CmAxFsKS7bz+uTLRakypIe4R+5A824vBSkQy/hB3fZepoVEf8OVAxzLvK+H/jm9TzpI3ETSm64Kg== - -rechoir@^0.6.2: - version "0.6.2" - resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" - integrity sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw== - dependencies: - resolve "^1.1.6" - -recursive-readdir@^2.2.2: - version "2.2.3" - resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.3.tgz#e726f328c0d69153bcabd5c322d3195252379372" - integrity sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA== - dependencies: - minimatch "^3.0.5" - -redux-thunk@^2.4.2: - version "2.4.2" - resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.4.2.tgz#b9d05d11994b99f7a91ea223e8b04cf0afa5ef3b" - integrity sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q== - -redux@^4.0.0, redux@^4.2.1: - version "4.2.1" - resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.1.tgz#c08f4306826c49b5e9dc901dee0452ea8fce6197" - integrity sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w== - dependencies: - "@babel/runtime" "^7.9.2" - -reftools@^1.1.9: - version "1.1.9" - resolved "https://registry.yarnpkg.com/reftools/-/reftools-1.1.9.tgz#e16e19f662ccd4648605312c06d34e5da3a2b77e" - integrity sha512-OVede/NQE13xBQ+ob5CKd5KyeJYU2YInb1bmV4nRoOfquZPkAkxuOXicSe1PvqIuZZ4kD13sPKBbR7UFDmli6w== - -regenerate-unicode-properties@^10.1.0: - version "10.1.1" - resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz#6b0e05489d9076b04c436f318d9b067bba459480" - integrity sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q== - dependencies: - regenerate "^1.4.2" - -regenerate@^1.4.2: - version "1.4.2" - resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" - integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== - -regenerator-runtime@^0.14.0: - version "0.14.1" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" - integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== - -regenerator-transform@^0.15.2: - version "0.15.2" - resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.15.2.tgz#5bbae58b522098ebdf09bca2f83838929001c7a4" - integrity sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg== - dependencies: - "@babel/runtime" "^7.8.4" - -regexpu-core@^5.3.1: - version "5.3.2" - resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-5.3.2.tgz#11a2b06884f3527aec3e93dbbf4a3b958a95546b" - integrity sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ== - 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" - -registry-auth-token@^5.0.1: - version "5.0.2" - resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-5.0.2.tgz#8b026cc507c8552ebbe06724136267e63302f756" - integrity sha512-o/3ikDxtXaA59BmZuZrJZDJv8NMDGSj+6j6XaeBmHw8eY1i1qd9+6H+LjVvQXx3HN6aRCGa1cUdJ9RaJZUugnQ== - dependencies: - "@pnpm/npm-conf" "^2.1.0" - -registry-url@^6.0.0: - version "6.0.1" - resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-6.0.1.tgz#056d9343680f2f64400032b1e199faa692286c58" - integrity sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q== - dependencies: - rc "1.2.8" - -regjsparser@^0.9.1: - version "0.9.1" - resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.9.1.tgz#272d05aa10c7c1f67095b1ff0addae8442fc5709" - integrity sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ== - dependencies: - jsesc "~0.5.0" - -rehype-raw@^6.1.1: - version "6.1.1" - resolved "https://registry.yarnpkg.com/rehype-raw/-/rehype-raw-6.1.1.tgz#81bbef3793bd7abacc6bf8335879d1b6c868c9d4" - integrity sha512-d6AKtisSRtDRX4aSPsJGTfnzrX2ZkHQLE5kiUuGOeEoLpbEulFF4hj0mLPbsa+7vmguDKOVVEQdHKDSwoaIDsQ== - dependencies: - "@types/hast" "^2.0.0" - hast-util-raw "^7.2.0" - unified "^10.0.0" - -rehype-raw@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/rehype-raw/-/rehype-raw-7.0.0.tgz#59d7348fd5dbef3807bbaa1d443efd2dd85ecee4" - integrity sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww== - dependencies: - "@types/hast" "^3.0.0" - hast-util-raw "^9.0.0" - vfile "^6.0.0" - -relateurl@^0.2.7: - version "0.2.7" - resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9" - integrity sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog== - -remark-directive@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/remark-directive/-/remark-directive-3.0.0.tgz#34452d951b37e6207d2e2a4f830dc33442923268" - integrity sha512-l1UyWJ6Eg1VPU7Hm/9tt0zKtReJQNOA4+iDMAxTyZNWnJnFlbS/7zhiel/rogTLQ2vMYwDzSJa4BiVNqGlqIMA== - dependencies: - "@types/mdast" "^4.0.0" - mdast-util-directive "^3.0.0" - micromark-extension-directive "^3.0.0" - unified "^11.0.0" - -remark-emoji@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/remark-emoji/-/remark-emoji-4.0.1.tgz#671bfda668047689e26b2078c7356540da299f04" - integrity sha512-fHdvsTR1dHkWKev9eNyhTo4EFwbUvJ8ka9SgeWkMPYFX4WoI7ViVBms3PjlQYgw5TLvNQso3GUB/b/8t3yo+dg== - dependencies: - "@types/mdast" "^4.0.2" - emoticon "^4.0.1" - mdast-util-find-and-replace "^3.0.1" - node-emoji "^2.1.0" - unified "^11.0.4" - -remark-frontmatter@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/remark-frontmatter/-/remark-frontmatter-5.0.0.tgz#b68d61552a421ec412c76f4f66c344627dc187a2" - integrity sha512-XTFYvNASMe5iPN0719nPrdItC9aU0ssC4v14mH1BCi1u0n1gAocqcujWUrByftZTbLhRtiKRyjYTSIOcr69UVQ== - dependencies: - "@types/mdast" "^4.0.0" - mdast-util-frontmatter "^2.0.0" - micromark-extension-frontmatter "^2.0.0" - unified "^11.0.0" - -remark-gfm@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/remark-gfm/-/remark-gfm-4.0.0.tgz#aea777f0744701aa288b67d28c43565c7e8c35de" - integrity sha512-U92vJgBPkbw4Zfu/IiW2oTZLSL3Zpv+uI7My2eq8JxKgqraFdU8YUGicEJCEgSbeaG+QDFqIcwwfMTOEelPxuA== - dependencies: - "@types/mdast" "^4.0.0" - mdast-util-gfm "^3.0.0" - micromark-extension-gfm "^3.0.0" - remark-parse "^11.0.0" - remark-stringify "^11.0.0" - unified "^11.0.0" - -remark-mdx@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/remark-mdx/-/remark-mdx-3.0.1.tgz#8f73dd635c1874e44426e243f72c0977cf60e212" - integrity sha512-3Pz3yPQ5Rht2pM5R+0J2MrGoBSrzf+tJG94N+t/ilfdh8YLyyKYtidAYwTveB20BoHAcwIopOUqhcmh2F7hGYA== - dependencies: - mdast-util-mdx "^3.0.0" - micromark-extension-mdxjs "^3.0.0" - -remark-parse@^10.0.0: - version "10.0.2" - resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-10.0.2.tgz#ca241fde8751c2158933f031a4e3efbaeb8bc262" - integrity sha512-3ydxgHa/ZQzG8LvC7jTXccARYDcRld3VfcgIIFs7bI6vbRSxJJmzgLEIIoYKyrfhaY+ujuWaf/PJiMZXoiCXgw== - dependencies: - "@types/mdast" "^3.0.0" - mdast-util-from-markdown "^1.0.0" - unified "^10.0.0" - -remark-parse@^11.0.0: - version "11.0.0" - resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-11.0.0.tgz#aa60743fcb37ebf6b069204eb4da304e40db45a1" - integrity sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA== - dependencies: - "@types/mdast" "^4.0.0" - mdast-util-from-markdown "^2.0.0" - micromark-util-types "^2.0.0" - unified "^11.0.0" - -remark-rehype@^10.0.0: - version "10.1.0" - resolved "https://registry.yarnpkg.com/remark-rehype/-/remark-rehype-10.1.0.tgz#32dc99d2034c27ecaf2e0150d22a6dcccd9a6279" - integrity sha512-EFmR5zppdBp0WQeDVZ/b66CWJipB2q2VLNFMabzDSGR66Z2fQii83G5gTBbgGEnEEA0QRussvrFHxk1HWGJskw== - dependencies: - "@types/hast" "^2.0.0" - "@types/mdast" "^3.0.0" - mdast-util-to-hast "^12.1.0" - unified "^10.0.0" - -remark-rehype@^11.0.0: - version "11.1.0" - resolved "https://registry.yarnpkg.com/remark-rehype/-/remark-rehype-11.1.0.tgz#d5f264f42bcbd4d300f030975609d01a1697ccdc" - integrity sha512-z3tJrAs2kIs1AqIIy6pzHmAHlF1hWQ+OdY4/hv+Wxe35EhyLKcajL33iUEn3ScxtFox9nUvRufR/Zre8Q08H/g== - dependencies: - "@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" - -remark-stringify@^11.0.0: - version "11.0.0" - resolved "https://registry.yarnpkg.com/remark-stringify/-/remark-stringify-11.0.0.tgz#4c5b01dd711c269df1aaae11743eb7e2e7636fd3" - integrity sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw== - dependencies: - "@types/mdast" "^4.0.0" - mdast-util-to-markdown "^2.0.0" - unified "^11.0.0" - -renderkid@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/renderkid/-/renderkid-3.0.0.tgz#5fd823e4d6951d37358ecc9a58b1f06836b6268a" - integrity sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg== - dependencies: - css-select "^4.1.3" - dom-converter "^0.2.0" - htmlparser2 "^6.1.0" - lodash "^4.17.21" - strip-ansi "^6.0.1" - -require-directory@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" - integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== - -require-from-string@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" - integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== - -"require-like@>= 0.1.1": - version "0.1.2" - resolved "https://registry.yarnpkg.com/require-like/-/require-like-0.1.2.tgz#ad6f30c13becd797010c468afa775c0c0a6b47fa" - integrity sha512-oyrU88skkMtDdauHDuKVrgR+zuItqr6/c//FXzvmxRGMexSDc6hNvJInGW3LL46n+8b50RykrvwSUIIQH2LQ5A== - -requires-port@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" - integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== - -reselect@^4.1.8: - version "4.1.8" - resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.1.8.tgz#3f5dc671ea168dccdeb3e141236f69f02eaec524" - integrity sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ== - -resolve-alpn@^1.2.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/resolve-alpn/-/resolve-alpn-1.2.1.tgz#b7adbdac3546aaaec20b45e7d8265927072726f9" - integrity sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g== - -resolve-from@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" - integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== - -resolve-package-path@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/resolve-package-path/-/resolve-package-path-4.0.3.tgz#31dab6897236ea6613c72b83658d88898a9040aa" - integrity sha512-SRpNAPW4kewOaNUt8VPqhJ0UMxawMwzJD8V7m1cJfdSTK9ieZwS6K7Dabsm4bmLFM96Z5Y/UznrpG5kt1im8yA== - dependencies: - path-root "^0.1.1" - -resolve-pathname@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-3.0.0.tgz#99d02224d3cf263689becbb393bc560313025dcd" - integrity sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng== - -resolve@^1.1.6, resolve@^1.1.7, resolve@^1.14.2, resolve@^1.22.2: - version "1.22.8" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" - integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== - dependencies: - is-core-module "^2.13.0" - path-parse "^1.0.7" - supports-preserve-symlinks-flag "^1.0.0" - -responselike@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/responselike/-/responselike-3.0.0.tgz#20decb6c298aff0dbee1c355ca95461d42823626" - integrity sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg== - dependencies: - lowercase-keys "^3.0.0" - -retry@^0.13.1: - version "0.13.1" - resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" - integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== - -reusify@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" - integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== - -rimraf@3.0.2, rimraf@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" - integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== - dependencies: - glob "^7.1.3" - -ripemd160@^2.0.0, ripemd160@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" - integrity sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA== - dependencies: - hash-base "^3.0.0" - inherits "^2.0.1" - -robust-predicates@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/robust-predicates/-/robust-predicates-3.0.2.tgz#d5b28528c4824d20fc48df1928d41d9efa1ad771" - integrity sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg== - -rtl-detect@^1.0.4: - version "1.1.2" - resolved "https://registry.yarnpkg.com/rtl-detect/-/rtl-detect-1.1.2.tgz#ca7f0330af5c6bb626c15675c642ba85ad6273c6" - integrity sha512-PGMBq03+TTG/p/cRB7HCLKJ1MgDIi07+QU1faSjiYRfmY5UsAttV9Hs08jDAHVwcOwmVLcSJkpwyfXszVjWfIQ== - -rtlcss@^4.1.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/rtlcss/-/rtlcss-4.1.1.tgz#f20409fcc197e47d1925996372be196fee900c0c" - integrity sha512-/oVHgBtnPNcggP2aVXQjSy6N1mMAfHg4GSag0QtZBlD5bdDgAHwr4pydqJGd+SUCu9260+Pjqbjwtvu7EMH1KQ== - dependencies: - escalade "^3.1.1" - picocolors "^1.0.0" - postcss "^8.4.21" - strip-json-comments "^3.1.1" - -run-parallel@^1.1.9: - version "1.2.0" - resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" - integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== - dependencies: - queue-microtask "^1.2.2" - -rw@1: - version "1.3.3" - resolved "https://registry.yarnpkg.com/rw/-/rw-1.3.3.tgz#3f862dfa91ab766b14885ef4d01124bfda074fb4" - integrity sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ== - -rxjs@^7.5.4: - version "7.8.1" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543" - integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg== - dependencies: - tslib "^2.1.0" - -sade@^1.7.3: - version "1.8.1" - resolved "https://registry.yarnpkg.com/sade/-/sade-1.8.1.tgz#0a78e81d658d394887be57d2a409bf703a3b2701" - integrity sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A== - dependencies: - mri "^1.1.0" - -safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - -safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@^5.2.1, safe-buffer@~5.2.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== - -"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": - version "2.1.2" - resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" - integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== - -sass-loader@^10.1.1: - version "10.5.2" - resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-10.5.2.tgz#1ca30534fff296417b853c7597ca3b0bbe8c37d0" - integrity sha512-vMUoSNOUKJILHpcNCCyD23X34gve1TS7Rjd9uXHeKqhvBG39x6XbswFDtpbTElj6XdMFezoWhkh5vtKudf2cgQ== - dependencies: - klona "^2.0.4" - loader-utils "^2.0.0" - neo-async "^2.6.2" - schema-utils "^3.0.0" - semver "^7.3.2" - -sass-loader@^13.3.2: - version "13.3.3" - resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-13.3.3.tgz#60df5e858788cffb1a3215e5b92e9cba61e7e133" - integrity sha512-mt5YN2F1MOZr3d/wBRcZxeFgwgkH44wVc2zohO2YF6JiOMkiXe4BYRZpSu2sO1g71mo/j16txzUhsKZlqjVGzA== - dependencies: - neo-async "^2.6.2" - -sass@^1.58.1: - version "1.77.5" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.77.5.tgz#5f9009820297521356e962c0bed13ee36710edfe" - integrity sha512-oDfX1mukIlxacPdQqNb6mV2tVCrnE+P3nVYioy72V5tlk56CPNcO4TCuFcaCRKKfJ1M3lH95CleRS+dVKL2qMg== - dependencies: - chokidar ">=3.0.0 <4.0.0" - immutable "^4.0.0" - source-map-js ">=0.6.2 <2.0.0" - -sax@^1.2.4: - version "1.4.1" - resolved "https://registry.yarnpkg.com/sax/-/sax-1.4.1.tgz#44cc8988377f126304d3b3fc1010c733b929ef0f" - integrity sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg== - -scheduler@^0.23.2: - version "0.23.2" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3" - integrity sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ== - dependencies: - loose-envify "^1.1.0" - -schema-utils@2.7.0: - version "2.7.0" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.0.tgz#17151f76d8eae67fbbf77960c33c676ad9f4efc7" - integrity sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A== - dependencies: - "@types/json-schema" "^7.0.4" - ajv "^6.12.2" - ajv-keywords "^3.4.1" - -schema-utils@^3.0.0, schema-utils@^3.1.1, schema-utils@^3.2.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.3.0.tgz#f50a88877c3c01652a15b622ae9e9795df7a60fe" - integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg== - dependencies: - "@types/json-schema" "^7.0.8" - ajv "^6.12.5" - ajv-keywords "^3.5.2" - -schema-utils@^4.0.0, schema-utils@^4.0.1: - version "4.2.0" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.2.0.tgz#70d7c93e153a273a805801882ebd3bff20d89c8b" - integrity sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw== - dependencies: - "@types/json-schema" "^7.0.9" - ajv "^8.9.0" - ajv-formats "^2.1.1" - ajv-keywords "^5.1.0" - -section-matter@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/section-matter/-/section-matter-1.0.0.tgz#e9041953506780ec01d59f292a19c7b850b84167" - integrity sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA== - dependencies: - extend-shallow "^2.0.1" - kind-of "^6.0.0" - -select-hose@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" - integrity sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg== - -selfsigned@^2.1.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-2.4.1.tgz#560d90565442a3ed35b674034cec4e95dceb4ae0" - integrity sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q== - dependencies: - "@types/node-forge" "^1.3.0" - node-forge "^1" - -semver-diff@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-4.0.0.tgz#3afcf5ed6d62259f5c72d0d5d50dffbdc9680df5" - integrity sha512-0Ju4+6A8iOnpL/Thra7dZsSlOHYAHIeMxfhWQRI1/VLcT3WDBZKKtQt/QkBOsiIN9ZpuvHE6cGZ0x4glCMmfiA== - dependencies: - semver "^7.3.5" - -semver@7.3.5: - version "7.3.5" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" - integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== - dependencies: - lru-cache "^6.0.0" - -semver@7.5.4: - version "7.5.4" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" - integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== - dependencies: - lru-cache "^6.0.0" - -semver@^6.3.1: - version "6.3.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" - integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== - -semver@^7.3.2, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.4: - version "7.6.2" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.2.tgz#1e3b34759f896e8f14d6134732ce798aeb0c6e13" - integrity sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w== - -send@0.18.0: - version "0.18.0" - resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" - integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== - 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" - -serialize-javascript@^6.0.0, serialize-javascript@^6.0.1: - version "6.0.2" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" - integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== - dependencies: - randombytes "^2.1.0" - -serve-handler@^6.1.5: - version "6.1.5" - resolved "https://registry.yarnpkg.com/serve-handler/-/serve-handler-6.1.5.tgz#a4a0964f5c55c7e37a02a633232b6f0d6f068375" - integrity sha512-ijPFle6Hwe8zfmBxJdE+5fta53fdIY0lHISJvuikXB3VYFafRjMRpOffSPvCYsbKyBA7pvy9oYr/BT1O3EArlg== - dependencies: - bytes "3.0.0" - content-disposition "0.5.2" - fast-url-parser "1.1.3" - mime-types "2.1.18" - minimatch "3.1.2" - path-is-inside "1.0.2" - path-to-regexp "2.2.1" - range-parser "1.2.0" - -serve-index@^1.9.1: - version "1.9.1" - resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239" - integrity sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw== - dependencies: - accepts "~1.3.4" - batch "0.6.1" - debug "2.6.9" - escape-html "~1.0.3" - http-errors "~1.6.2" - mime-types "~2.1.17" - parseurl "~1.3.2" - -serve-static@1.15.0: - version "1.15.0" - resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" - integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== - dependencies: - encodeurl "~1.0.2" - escape-html "~1.0.3" - parseurl "~1.3.3" - send "0.18.0" - -set-function-length@^1.2.1: - version "1.2.2" - resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" - integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== - dependencies: - define-data-property "^1.1.4" - es-errors "^1.3.0" - function-bind "^1.1.2" - get-intrinsic "^1.2.4" - gopd "^1.0.1" - has-property-descriptors "^1.0.2" - -setimmediate@^1.0.4: - version "1.0.5" - resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" - integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA== - -setprototypeof@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" - integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== - -setprototypeof@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" - integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== - -sha.js@^2.4.0, sha.js@^2.4.8: - version "2.4.11" - resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" - integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ== - dependencies: - inherits "^2.0.1" - safe-buffer "^5.0.1" - -shallow-clone@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" - integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== - dependencies: - kind-of "^6.0.2" - -shallowequal@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8" - integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ== - -shebang-command@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" - integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== - dependencies: - shebang-regex "^3.0.0" - -shebang-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" - integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== - -shell-quote@^1.7.3, shell-quote@^1.8.1: - version "1.8.1" - resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680" - integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA== - -shelljs@0.8.5, shelljs@^0.8.5: - version "0.8.5" - resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.5.tgz#de055408d8361bed66c669d2f000538ced8ee20c" - integrity sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow== - dependencies: - glob "^7.0.0" - interpret "^1.0.0" - rechoir "^0.6.2" - -should-equal@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/should-equal/-/should-equal-2.0.0.tgz#6072cf83047360867e68e98b09d71143d04ee0c3" - integrity sha512-ZP36TMrK9euEuWQYBig9W55WPC7uo37qzAEmbjHz4gfyuXrEUgF8cUvQVO+w+d3OMfPvSRQJ22lSm8MQJ43LTA== - dependencies: - should-type "^1.4.0" - -should-format@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/should-format/-/should-format-3.0.3.tgz#9bfc8f74fa39205c53d38c34d717303e277124f1" - integrity sha512-hZ58adtulAk0gKtua7QxevgUaXTTXxIi8t41L3zo9AHvjXO1/7sdLECuHeIN2SRtYXpNkmhoUP2pdeWgricQ+Q== - dependencies: - should-type "^1.3.0" - should-type-adaptors "^1.0.1" - -should-type-adaptors@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/should-type-adaptors/-/should-type-adaptors-1.1.0.tgz#401e7f33b5533033944d5cd8bf2b65027792e27a" - integrity sha512-JA4hdoLnN+kebEp2Vs8eBe9g7uy0zbRo+RMcU0EsNy+R+k049Ki+N5tT5Jagst2g7EAja+euFuoXFCa8vIklfA== - dependencies: - should-type "^1.3.0" - should-util "^1.0.0" - -should-type@^1.3.0, should-type@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/should-type/-/should-type-1.4.0.tgz#0756d8ce846dfd09843a6947719dfa0d4cff5cf3" - integrity sha512-MdAsTu3n25yDbIe1NeN69G4n6mUnJGtSJHygX3+oN0ZbO3DTiATnf7XnYJdGT42JCXurTb1JI0qOBR65shvhPQ== - -should-util@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/should-util/-/should-util-1.0.1.tgz#fb0d71338f532a3a149213639e2d32cbea8bcb28" - integrity sha512-oXF8tfxx5cDk8r2kYqlkUJzZpDBqVY/II2WhvU0n9Y3XYvAYRmeaf1PvvIvTgPnv4KJ+ES5M0PyDq5Jp+Ygy2g== - -should@^13.2.1: - version "13.2.3" - resolved "https://registry.yarnpkg.com/should/-/should-13.2.3.tgz#96d8e5acf3e97b49d89b51feaa5ae8d07ef58f10" - integrity sha512-ggLesLtu2xp+ZxI+ysJTmNjh2U0TsC+rQ/pfED9bUZZ4DKefP27D+7YJVVTvKsmjLpIi9jAa7itwDGkDDmt1GQ== - dependencies: - should-equal "^2.0.0" - should-format "^3.0.3" - should-type "^1.4.0" - should-type-adaptors "^1.0.1" - should-util "^1.0.0" - -side-channel@^1.0.4, side-channel@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" - integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== - dependencies: - call-bind "^1.0.7" - es-errors "^1.3.0" - get-intrinsic "^1.2.4" - object-inspect "^1.13.1" - -signal-exit@^3.0.2, signal-exit@^3.0.3: - version "3.0.7" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" - integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== - -signal-exit@^4.0.1: - version "4.1.0" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" - integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== - -sirv@^2.0.3: - version "2.0.4" - resolved "https://registry.yarnpkg.com/sirv/-/sirv-2.0.4.tgz#5dd9a725c578e34e449f332703eb2a74e46a29b0" - integrity sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ== - dependencies: - "@polka/url" "^1.0.0-next.24" - mrmime "^2.0.0" - totalist "^3.0.0" - -sisteransi@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" - integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== - -sitemap@7.1.1: - version "7.1.1" - resolved "https://registry.yarnpkg.com/sitemap/-/sitemap-7.1.1.tgz#eeed9ad6d95499161a3eadc60f8c6dce4bea2bef" - integrity sha512-mK3aFtjz4VdJN0igpIJrinf3EO8U8mxOPsTBzSsy06UtjZQJ3YY3o3Xa7zSc5nMqcMrRwlChHZ18Kxg0caiPBg== - dependencies: - "@types/node" "^17.0.5" - "@types/sax" "^1.2.1" - arg "^5.0.0" - sax "^1.2.4" - -sitemap@^7.1.1: - version "7.1.2" - resolved "https://registry.yarnpkg.com/sitemap/-/sitemap-7.1.2.tgz#6ce1deb43f6f177c68bc59cf93632f54e3ae6b72" - integrity sha512-ARCqzHJ0p4gWt+j7NlU5eDlIO9+Rkr/JhPFZKKQ1l5GCus7rJH4UdrlVAh0xC/gDS/Qir2UMxqYNHtsKr2rpCw== - dependencies: - "@types/node" "^17.0.5" - "@types/sax" "^1.2.1" - arg "^5.0.0" - sax "^1.2.4" - -skin-tone@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/skin-tone/-/skin-tone-2.0.0.tgz#4e3933ab45c0d4f4f781745d64b9f4c208e41237" - integrity sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA== - dependencies: - unicode-emoji-modifier-base "^1.0.0" - -slash@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" - integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== - -slash@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-4.0.0.tgz#2422372176c4c6c5addb5e2ada885af984b396a7" - integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew== - -slugify@^1.6.5: - version "1.6.6" - resolved "https://registry.yarnpkg.com/slugify/-/slugify-1.6.6.tgz#2d4ac0eacb47add6af9e04d3be79319cbcc7924b" - integrity sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw== - -snake-case@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-3.0.4.tgz#4f2bbd568e9935abdfd593f34c691dadb49c452c" - integrity sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg== - dependencies: - dot-case "^3.0.4" - tslib "^2.0.3" - -sockjs@^0.3.24: - version "0.3.24" - resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.24.tgz#c9bc8995f33a111bea0395ec30aa3206bdb5ccce" - integrity sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ== - dependencies: - faye-websocket "^0.11.3" - uuid "^8.3.2" - websocket-driver "^0.7.4" - -sort-css-media-queries@2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/sort-css-media-queries/-/sort-css-media-queries-2.2.0.tgz#aa33cf4a08e0225059448b6c40eddbf9f1c8334c" - integrity sha512-0xtkGhWCC9MGt/EzgnvbbbKhqWjl1+/rncmhTh5qCpbYguXh6S/qwePfv/JQ8jePXXmqingylxoC49pCkSPIbA== - -"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.1, source-map-js@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af" - integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg== - -source-map-support@~0.5.20: - version "0.5.21" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" - integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - -source-map@^0.6.0, source-map@~0.6.0: - version "0.6.1" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" - integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== - -source-map@^0.7.0: - version "0.7.4" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656" - integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== - -space-separated-tokens@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz#1ecd9d2350a3844572c3f4a312bceb018348859f" - integrity sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q== - -spdy-transport@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-3.0.0.tgz#00d4863a6400ad75df93361a1608605e5dcdcf31" - integrity sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw== - dependencies: - debug "^4.1.0" - detect-node "^2.0.4" - hpack.js "^2.1.6" - obuf "^1.1.2" - readable-stream "^3.0.6" - wbuf "^1.7.3" - -spdy@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/spdy/-/spdy-4.0.2.tgz#b74f466203a3eda452c02492b91fb9e84a27677b" - integrity sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA== - dependencies: - debug "^4.1.0" - handle-thing "^2.0.0" - http-deceiver "^1.2.7" - select-hose "^2.0.0" - spdy-transport "^3.0.0" - -sprintf-js@~1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" - integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== - -srcset@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/srcset/-/srcset-4.0.0.tgz#336816b665b14cd013ba545b6fe62357f86e65f4" - integrity sha512-wvLeHgcVHKO8Sc/H/5lkGreJQVeYMm9rlmt8PuR1xE31rIuXhuzznUUqAt8MqLhB3MqJdFzlNAfpcWnxiFUcPw== - -statuses@2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" - integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== - -"statuses@>= 1.4.0 < 2": - version "1.5.0" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" - integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== - -std-env@^3.0.1: - version "3.7.0" - resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.7.0.tgz#c9f7386ced6ecf13360b6c6c55b8aaa4ef7481d2" - integrity sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg== - -stream-browserify@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-3.0.0.tgz#22b0a2850cdf6503e73085da1fc7b7d0c2122f2f" - integrity sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA== - dependencies: - inherits "~2.0.4" - readable-stream "^3.5.0" - -stream-http@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-3.2.0.tgz#1872dfcf24cb15752677e40e5c3f9cc1926028b5" - integrity sha512-Oq1bLqisTyK3TSCXpPbT4sdeYNdmyZJv1LxpEm2vu1ZhK89kSE5YXwZc3cWk0MagGaKriBh9mCFbVGtO+vY29A== - dependencies: - builtin-status-codes "^3.0.0" - inherits "^2.0.4" - readable-stream "^3.6.0" - xtend "^4.0.2" - -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - 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" - -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - 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" - -string-width@^5.0.1, string-width@^5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" - integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== - dependencies: - eastasianwidth "^0.2.0" - emoji-regex "^9.2.2" - strip-ansi "^7.0.1" - -string_decoder@^1.1.1, string_decoder@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.0" - -string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" - integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== - dependencies: - safe-buffer "~5.1.0" - -stringify-entities@^4.0.0: - version "4.0.4" - resolved "https://registry.yarnpkg.com/stringify-entities/-/stringify-entities-4.0.4.tgz#b3b79ef5f277cc4ac73caeb0236c5ba939b3a4f3" - integrity sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg== - dependencies: - character-entities-html4 "^2.0.0" - character-entities-legacy "^3.0.0" - -stringify-object@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.3.0.tgz#703065aefca19300d3ce88af4f5b3956d7556629" - integrity sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw== - dependencies: - get-own-enumerable-property-symbols "^3.0.0" - is-obj "^1.0.1" - is-regexp "^1.0.0" - -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@^7.0.1: - version "7.1.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" - integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== - dependencies: - ansi-regex "^6.0.1" - -strip-bom-string@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/strip-bom-string/-/strip-bom-string-1.0.0.tgz#e5211e9224369fbb81d633a2f00044dc8cedad92" - integrity sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g== - -strip-final-newline@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" - integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== - -strip-json-comments@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" - integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== - -strip-json-comments@~2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" - integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ== - -style-to-object@^0.4.0, style-to-object@^0.4.1: - version "0.4.4" - resolved "https://registry.yarnpkg.com/style-to-object/-/style-to-object-0.4.4.tgz#266e3dfd56391a7eefb7770423612d043c3f33ec" - integrity sha512-HYNoHZa2GorYNyqiCaBgsxvcJIn7OHq6inEga+E6Ke3m5JkoqpQbnFssk4jwe+K7AhGa2fcha4wSOf1Kn01dMg== - dependencies: - inline-style-parser "0.1.1" - -style-to-object@^1.0.0: - version "1.0.6" - resolved "https://registry.yarnpkg.com/style-to-object/-/style-to-object-1.0.6.tgz#0c28aed8be1813d166c60d962719b2907c26547b" - integrity sha512-khxq+Qm3xEyZfKd/y9L3oIWQimxuc4STrQKtQn8aSDRHb8mFgpukgX1hdzfrMEW6JCjyJ8p89x+IUMVnCBI1PA== - dependencies: - inline-style-parser "0.2.3" - -stylehacks@^6.1.1: - version "6.1.1" - resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-6.1.1.tgz#543f91c10d17d00a440430362d419f79c25545a6" - integrity sha512-gSTTEQ670cJNoaeIp9KX6lZmm8LJ3jPB5yJmX8Zq/wQxOsAFXV3qjWzHas3YYk1qesuVIyYWWUpZ0vSE/dTSGg== - dependencies: - browserslist "^4.23.0" - postcss-selector-parser "^6.0.16" - -stylis@^4.1.3: - version "4.3.2" - resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.3.2.tgz#8f76b70777dd53eb669c6f58c997bf0a9972e444" - integrity sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg== - -sucrase@^3.31.0, sucrase@^3.32.0: - version "3.35.0" - resolved "https://registry.yarnpkg.com/sucrase/-/sucrase-3.35.0.tgz#57f17a3d7e19b36d8995f06679d121be914ae263" - integrity sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA== - 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" - -supports-color@^5.3.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" - integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== - dependencies: - has-flag "^3.0.0" - -supports-color@^7.1.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" - integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== - dependencies: - has-flag "^4.0.0" - -supports-color@^8.0.0: - version "8.1.1" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" - integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== - dependencies: - has-flag "^4.0.0" - -supports-preserve-symlinks-flag@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" - integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== - -svg-parser@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/svg-parser/-/svg-parser-2.0.4.tgz#fdc2e29e13951736140b76cb122c8ee6630eb6b5" - integrity sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ== - -svgo@^3.0.2, svgo@^3.2.0: - version "3.3.2" - resolved "https://registry.yarnpkg.com/svgo/-/svgo-3.3.2.tgz#ad58002652dffbb5986fc9716afe52d869ecbda8" - integrity sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw== - dependencies: - "@trysound/sax" "0.2.0" - commander "^7.2.0" - css-select "^5.1.0" - css-tree "^2.3.1" - css-what "^6.1.0" - csso "^5.0.5" - picocolors "^1.0.0" - -swagger2openapi@7.0.8, swagger2openapi@^7.0.8: - version "7.0.8" - resolved "https://registry.yarnpkg.com/swagger2openapi/-/swagger2openapi-7.0.8.tgz#12c88d5de776cb1cbba758994930f40ad0afac59" - integrity sha512-upi/0ZGkYgEcLeGieoz8gT74oWHA0E7JivX7aN9mAf+Tc7BQoRBvnIGHoPDw+f9TXTW4s6kGYCZJtauP6OYp7g== - dependencies: - call-me-maybe "^1.0.1" - node-fetch "^2.6.1" - node-fetch-h2 "^2.3.0" - node-readfiles "^0.2.0" - oas-kit-common "^1.0.8" - oas-resolver "^2.5.6" - oas-schema-walker "^1.1.5" - oas-validator "^5.0.8" - reftools "^1.1.9" - yaml "^1.10.0" - yargs "^17.0.1" - -swc-loader@^0.2.3: - version "0.2.6" - resolved "https://registry.yarnpkg.com/swc-loader/-/swc-loader-0.2.6.tgz#bf0cba8eeff34bb19620ead81d1277fefaec6bc8" - integrity sha512-9Zi9UP2YmDpgmQVbyOPJClY0dwf58JDyDMQ7uRc4krmc72twNI2fvlBWHLqVekBpPc7h5NJkGVT1zNDxFrqhvg== - dependencies: - "@swc/counter" "^0.1.3" - -tailwindcss@^3.2.4: - version "3.4.4" - resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.4.4.tgz#351d932273e6abfa75ce7d226b5bf3a6cb257c05" - integrity sha512-ZoyXOdJjISB7/BcLTR6SEsLgKtDStYyYZVLsUtWChO4Ps20CBad7lfJKVDiejocV4ME1hLmyY0WJE3hSDcmQ2A== - dependencies: - "@alloc/quick-lru" "^5.2.0" - arg "^5.0.2" - chokidar "^3.5.3" - didyoumean "^1.2.2" - dlv "^1.1.3" - fast-glob "^3.3.0" - glob-parent "^6.0.2" - is-glob "^4.0.3" - jiti "^1.21.0" - lilconfig "^2.1.0" - micromatch "^4.0.5" - normalize-path "^3.0.0" - object-hash "^3.0.0" - picocolors "^1.0.0" - postcss "^8.4.23" - postcss-import "^15.1.0" - postcss-js "^4.0.1" - postcss-load-config "^4.0.1" - postcss-nested "^6.0.1" - postcss-selector-parser "^6.0.11" - resolve "^1.22.2" - sucrase "^3.32.0" - -tapable@^1.0.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" - integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== - -tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0, tapable@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" - integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== - -tar-fs@2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784" - integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng== - dependencies: - chownr "^1.1.1" - mkdirp-classic "^0.5.2" - pump "^3.0.0" - tar-stream "^2.1.4" - -tar-stream@^2.1.4: - version "2.2.0" - resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287" - integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== - 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" - -terser-webpack-plugin@^5.3.10, terser-webpack-plugin@^5.3.9: - version "5.3.10" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz#904f4c9193c6fd2a03f693a2150c62a92f40d199" - integrity sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w== - 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" - -terser@^5.10.0, terser@^5.15.1, terser@^5.26.0: - version "5.31.1" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.31.1.tgz#735de3c987dd671e95190e6b98cfe2f07f3cf0d4" - integrity sha512-37upzU1+viGvuFtBo9NPufCb9dwM0+l9hMxYyWfBA+fbwrPqNJAhbZ6W47bBFnZHKHTUBnMvi87434qq+qnxOg== - dependencies: - "@jridgewell/source-map" "^0.3.3" - acorn "^8.8.2" - commander "^2.20.0" - source-map-support "~0.5.20" - -text-table@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" - integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== - -thenify-all@^1.0.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726" - integrity sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA== - dependencies: - thenify ">= 3.1.0 < 4" - -"thenify@>= 3.1.0 < 4": - version "3.3.1" - resolved "https://registry.yarnpkg.com/thenify/-/thenify-3.3.1.tgz#8932e686a4066038a016dd9e2ca46add9838a95f" - integrity sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw== - dependencies: - any-promise "^1.0.0" - -through@^2.3.8: - version "2.3.8" - resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" - integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== - -thunky@^1.0.2: - version "1.1.0" - resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" - integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== - -timers-browserify@^2.0.12: - version "2.0.12" - resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.12.tgz#44a45c11fbf407f34f97bccd1577c652361b00ee" - integrity sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ== - dependencies: - setimmediate "^1.0.4" - -tiny-invariant@^1.0.2: - version "1.3.3" - resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127" - integrity sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg== - -tiny-warning@^1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" - integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== - -to-fast-properties@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" - integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== - -to-regex-range@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" - integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== - dependencies: - is-number "^7.0.0" - -toggle-selection@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32" - integrity sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ== - -toidentifier@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" - integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== - -totalist@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/totalist/-/totalist-3.0.1.tgz#ba3a3d600c915b1a97872348f79c127475f6acf8" - integrity sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ== - -tr46@~0.0.3: - version "0.0.3" - resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" - integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== - -traverse@0.6.6: - version "0.6.6" - resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.6.6.tgz#cbdf560fd7b9af632502fed40f918c157ea97137" - integrity sha512-kdf4JKs8lbARxWdp7RKdNzoJBhGUcIalSYibuGyHJbmk40pOysQ0+QPvlkCOICOivDWU2IJo2rkrxyTK2AH4fw== - -trim-lines@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/trim-lines/-/trim-lines-3.0.1.tgz#d802e332a07df861c48802c04321017b1bd87338" - integrity sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg== - -trough@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/trough/-/trough-2.2.0.tgz#94a60bd6bd375c152c1df911a4b11d5b0256f50f" - integrity sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw== - -ts-dedent@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/ts-dedent/-/ts-dedent-2.2.0.tgz#39e4bd297cd036292ae2394eb3412be63f563bb5" - integrity sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ== - -ts-interface-checker@^0.1.9: - version "0.1.13" - resolved "https://registry.yarnpkg.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699" - integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA== - -tslib@^2.0.3, tslib@^2.1.0, tslib@^2.6.0: - version "2.6.3" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" - integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== - -tty-browserify@^0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.1.tgz#3f05251ee17904dfd0677546670db9651682b811" - integrity sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw== - -type-fest@^1.0.1: - version "1.4.0" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-1.4.0.tgz#e9fb813fe3bf1744ec359d55d1affefa76f14be1" - integrity sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA== - -type-fest@^2.13.0, type-fest@^2.14.0, type-fest@^2.5.0: - version "2.19.0" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b" - integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA== - -type-is@~1.6.18: - version "1.6.18" - resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" - integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== - dependencies: - media-typer "0.3.0" - mime-types "~2.1.24" - -typedarray-to-buffer@^3.1.5: - version "3.1.5" - resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" - integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== - dependencies: - is-typedarray "^1.0.0" - -unbzip2-stream@1.4.3: - version "1.4.3" - resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz#b0da04c4371311df771cdc215e87f2130991ace7" - integrity sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg== - dependencies: - buffer "^5.2.1" - through "^2.3.8" - -undici-types@~5.26.4: - version "5.26.5" - resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" - integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== - -unicode-canonical-property-names-ecmascript@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" - integrity sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ== - -unicode-emoji-modifier-base@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/unicode-emoji-modifier-base/-/unicode-emoji-modifier-base-1.0.0.tgz#dbbd5b54ba30f287e2a8d5a249da6c0cef369459" - integrity sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g== - -unicode-match-property-ecmascript@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz#54fd16e0ecb167cf04cf1f756bdcc92eba7976c3" - integrity sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q== - dependencies: - unicode-canonical-property-names-ecmascript "^2.0.0" - unicode-property-aliases-ecmascript "^2.0.0" - -unicode-match-property-value-ecmascript@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz#cb5fffdcd16a05124f5a4b0bf7c3770208acbbe0" - integrity sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA== - -unicode-property-aliases-ecmascript@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz#43d41e3be698bd493ef911077c9b131f827e8ccd" - integrity sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w== - -unified@^10.0.0: - version "10.1.2" - resolved "https://registry.yarnpkg.com/unified/-/unified-10.1.2.tgz#b1d64e55dafe1f0b98bb6c719881103ecf6c86df" - integrity sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q== - dependencies: - "@types/unist" "^2.0.0" - bail "^2.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" - -unified@^11.0.0, unified@^11.0.3, unified@^11.0.4: - version "11.0.4" - resolved "https://registry.yarnpkg.com/unified/-/unified-11.0.4.tgz#f4be0ac0fe4c88cb873687c07c64c49ed5969015" - integrity sha512-apMPnyLjAX+ty4OrNap7yumyVAMlKx5IWU2wlzzUdYJO9A8f1p9m/gywF/GM2ZDFcjQPrx59Mc90KwmxsoklxQ== - dependencies: - "@types/unist" "^3.0.0" - bail "^2.0.0" - devlop "^1.0.0" - extend "^3.0.0" - is-plain-obj "^4.0.0" - trough "^2.0.0" - vfile "^6.0.0" - -unique-string@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-3.0.0.tgz#84a1c377aff5fd7a8bc6b55d8244b2bd90d75b9a" - integrity sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ== - dependencies: - crypto-random-string "^4.0.0" - -unist-util-generated@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/unist-util-generated/-/unist-util-generated-2.0.1.tgz#e37c50af35d3ed185ac6ceacb6ca0afb28a85cae" - integrity sha512-qF72kLmPxAw0oN2fwpWIqbXAVyEqUzDHMsbtPvOudIlUzXYFIeQIuxXQCRCFh22B7cixvU0MG7m3MW8FTq/S+A== - -unist-util-is@^5.0.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-5.2.1.tgz#b74960e145c18dcb6226bc57933597f5486deae9" - integrity sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw== - dependencies: - "@types/unist" "^2.0.0" - -unist-util-is@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-6.0.0.tgz#b775956486aff107a9ded971d996c173374be424" - integrity sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw== - dependencies: - "@types/unist" "^3.0.0" - -unist-util-position-from-estree@^1.0.0, unist-util-position-from-estree@^1.1.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/unist-util-position-from-estree/-/unist-util-position-from-estree-1.1.2.tgz#8ac2480027229de76512079e377afbcabcfcce22" - integrity sha512-poZa0eXpS+/XpoQwGwl79UUdea4ol2ZuCYguVaJS4qzIOMDzbqz8a3erUCOmubSZkaOuGamb3tX790iwOIROww== - dependencies: - "@types/unist" "^2.0.0" - -unist-util-position-from-estree@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/unist-util-position-from-estree/-/unist-util-position-from-estree-2.0.0.tgz#d94da4df596529d1faa3de506202f0c9a23f2200" - integrity sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ== - dependencies: - "@types/unist" "^3.0.0" - -unist-util-position@^4.0.0: - version "4.0.4" - resolved "https://registry.yarnpkg.com/unist-util-position/-/unist-util-position-4.0.4.tgz#93f6d8c7d6b373d9b825844645877c127455f037" - integrity sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg== - dependencies: - "@types/unist" "^2.0.0" - -unist-util-position@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/unist-util-position/-/unist-util-position-5.0.0.tgz#678f20ab5ca1207a97d7ea8a388373c9cf896be4" - integrity sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA== - dependencies: - "@types/unist" "^3.0.0" - -unist-util-remove-position@^4.0.0: - version "4.0.2" - resolved "https://registry.yarnpkg.com/unist-util-remove-position/-/unist-util-remove-position-4.0.2.tgz#a89be6ea72e23b1a402350832b02a91f6a9afe51" - integrity sha512-TkBb0HABNmxzAcfLf4qsIbFbaPDvMO6wa3b3j4VcEzFVaw1LBKwnW4/sRJ/atSLSzoIg41JWEdnE7N6DIhGDGQ== - dependencies: - "@types/unist" "^2.0.0" - unist-util-visit "^4.0.0" - -unist-util-remove-position@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz#fea68a25658409c9460408bc6b4991b965b52163" - integrity sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q== - dependencies: - "@types/unist" "^3.0.0" - unist-util-visit "^5.0.0" - -unist-util-stringify-position@^3.0.0: - version "3.0.3" - resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz#03ad3348210c2d930772d64b489580c13a7db39d" - integrity sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg== - dependencies: - "@types/unist" "^2.0.0" - -unist-util-stringify-position@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz#449c6e21a880e0855bf5aabadeb3a740314abac2" - integrity sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ== - dependencies: - "@types/unist" "^3.0.0" - -unist-util-visit-parents@^5.1.1: - version "5.1.3" - resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz#b4520811b0ca34285633785045df7a8d6776cfeb" - integrity sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg== - dependencies: - "@types/unist" "^2.0.0" - unist-util-is "^5.0.0" - -unist-util-visit-parents@^6.0.0: - version "6.0.1" - resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz#4d5f85755c3b8f0dc69e21eca5d6d82d22162815" - integrity sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw== - dependencies: - "@types/unist" "^3.0.0" - unist-util-is "^6.0.0" - -unist-util-visit@^4.0.0: - version "4.1.2" - resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-4.1.2.tgz#125a42d1eb876283715a3cb5cceaa531828c72e2" - integrity sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg== - dependencies: - "@types/unist" "^2.0.0" - unist-util-is "^5.0.0" - unist-util-visit-parents "^5.1.1" - -unist-util-visit@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-5.0.0.tgz#a7de1f31f72ffd3519ea71814cccf5fd6a9217d6" - integrity sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg== - dependencies: - "@types/unist" "^3.0.0" - unist-util-is "^6.0.0" - unist-util-visit-parents "^6.0.0" - -universalify@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" - integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== - -unpipe@1.0.0, unpipe@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" - integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== - -update-browserslist-db@^1.0.16: - version "1.0.16" - resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz#f6d489ed90fb2f07d67784eb3f53d7891f736356" - integrity sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ== - dependencies: - escalade "^3.1.2" - picocolors "^1.0.1" - -update-notifier@^6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-6.0.2.tgz#a6990253dfe6d5a02bd04fbb6a61543f55026b60" - integrity sha512-EDxhTEVPZZRLWYcJ4ZXjGFN0oP7qYvbXWzEgRm/Yql4dHX5wDbvh89YHP6PK1lzZJYrMtXUuZZz8XGK+U6U1og== - dependencies: - boxen "^7.0.0" - chalk "^5.0.1" - configstore "^6.0.0" - has-yarn "^3.0.0" - import-lazy "^4.0.0" - is-ci "^3.0.1" - is-installed-globally "^0.4.0" - is-npm "^6.0.0" - is-yarn-global "^0.4.0" - latest-version "^7.0.0" - pupa "^3.1.0" - semver "^7.3.7" - semver-diff "^4.0.0" - xdg-basedir "^5.1.0" - -uri-js@^4.2.2, uri-js@^4.4.1: - version "4.4.1" - resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" - integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== - dependencies: - punycode "^2.1.0" - -url-loader@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-4.1.1.tgz#28505e905cae158cf07c92ca622d7f237e70a4e2" - integrity sha512-3BTV812+AVHHOJQO8O5MkWgZ5aosP7GnROJwvzLS9hWDj00lZ6Z0wNak423Lp9PBZN05N+Jk/N5Si8jRAlGyWA== - dependencies: - loader-utils "^2.0.0" - mime-types "^2.1.27" - schema-utils "^3.0.0" - -url@^0.11.0: - version "0.11.3" - resolved "https://registry.yarnpkg.com/url/-/url-0.11.3.tgz#6f495f4b935de40ce4a0a52faee8954244f3d3ad" - integrity sha512-6hxOLGfZASQK/cijlZnZJTq8OXAkt/3YGfQX45vvMYXpZoo8NdWZcY73K108Jf759lS1Bv/8wXnHDTSz17dSRw== - dependencies: - punycode "^1.4.1" - qs "^6.11.2" - -use-editable@^2.3.3: - version "2.3.3" - resolved "https://registry.yarnpkg.com/use-editable/-/use-editable-2.3.3.tgz#a292fe9ba4c291cd28d1cc2728c75a5fc8d9a33f" - integrity sha512-7wVD2JbfAFJ3DK0vITvXBdpd9JAz5BcKAAolsnLBuBn6UDDwBGuCIAGvR3yA2BNKm578vAMVHFCWaOcA+BhhiA== - -util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== - -util@^0.10.3: - version "0.10.4" - resolved "https://registry.yarnpkg.com/util/-/util-0.10.4.tgz#3aa0125bfe668a4672de58857d3ace27ecb76901" - integrity sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A== - dependencies: - inherits "2.0.3" - -util@^0.12.4, util@^0.12.5: - version "0.12.5" - resolved "https://registry.yarnpkg.com/util/-/util-0.12.5.tgz#5f17a6059b73db61a875668781a1c2b136bd6fbc" - integrity sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA== - dependencies: - inherits "^2.0.3" - is-arguments "^1.0.4" - is-generator-function "^1.0.7" - is-typed-array "^1.1.3" - which-typed-array "^1.1.2" - -utila@~0.4: - version "0.4.0" - resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c" - integrity sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA== - -utility-types@^3.10.0: - version "3.11.0" - resolved "https://registry.yarnpkg.com/utility-types/-/utility-types-3.11.0.tgz#607c40edb4f258915e901ea7995607fdf319424c" - integrity sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw== - -utils-merge@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" - integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== - -uuid@8.3.2, uuid@^8.3.2: - version "8.3.2" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" - integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== - -uuid@^9.0.0: - version "9.0.1" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" - integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== - -uvu@^0.5.0: - version "0.5.6" - resolved "https://registry.yarnpkg.com/uvu/-/uvu-0.5.6.tgz#2754ca20bcb0bb59b64e9985e84d2e81058502df" - integrity sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA== - dependencies: - dequal "^2.0.0" - diff "^5.0.0" - kleur "^4.0.3" - sade "^1.7.3" - -validate-peer-dependencies@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/validate-peer-dependencies/-/validate-peer-dependencies-2.2.0.tgz#47b8ff008f66a66fc5d8699123844522c1d874f4" - integrity sha512-8X1OWlERjiUY6P6tdeU9E0EwO8RA3bahoOVG7ulOZT5MqgNDUO/BQoVjYiHPcNe+v8glsboZRIw9iToMAA2zAA== - dependencies: - resolve-package-path "^4.0.3" - semver "^7.3.8" - -validate.io-array@^1.0.3: - version "1.0.6" - resolved "https://registry.yarnpkg.com/validate.io-array/-/validate.io-array-1.0.6.tgz#5b5a2cafd8f8b85abb2f886ba153f2d93a27774d" - integrity sha512-DeOy7CnPEziggrOO5CZhVKJw6S3Yi7e9e65R1Nl/RTN1vTQKnzjfvks0/8kQ40FP/dsjRAOd4hxmJ7uLa6vxkg== - -validate.io-function@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/validate.io-function/-/validate.io-function-1.0.2.tgz#343a19802ed3b1968269c780e558e93411c0bad7" - integrity sha512-LlFybRJEriSuBnUhQyG5bwglhh50EpTL2ul23MPIuR1odjO7XaMLFV8vHGwp7AZciFxtYOeiSCT5st+XSPONiQ== - -validate.io-integer-array@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/validate.io-integer-array/-/validate.io-integer-array-1.0.0.tgz#2cabde033293a6bcbe063feafe91eaf46b13a089" - integrity sha512-mTrMk/1ytQHtCY0oNO3dztafHYyGU88KL+jRxWuzfOmQb+4qqnWmI+gykvGp8usKZOM0H7keJHEbRaFiYA0VrA== - dependencies: - validate.io-array "^1.0.3" - validate.io-integer "^1.0.4" - -validate.io-integer@^1.0.4: - version "1.0.5" - resolved "https://registry.yarnpkg.com/validate.io-integer/-/validate.io-integer-1.0.5.tgz#168496480b95be2247ec443f2233de4f89878068" - integrity sha512-22izsYSLojN/P6bppBqhgUDjCkr5RY2jd+N2a3DCAUey8ydvrZ/OkGvFPR7qfOpwR2LC5p4Ngzxz36g5Vgr/hQ== - dependencies: - validate.io-number "^1.0.3" - -validate.io-number@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/validate.io-number/-/validate.io-number-1.0.3.tgz#f63ffeda248bf28a67a8d48e0e3b461a1665baf8" - integrity sha512-kRAyotcbNaSYoDnXvb4MHg/0a1egJdLwS6oJ38TJY7aw9n93Fl/3blIXdyYvPOp55CNxywooG/3BcrwNrBpcSg== - -value-equal@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c" - integrity sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw== - -vary@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" - integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== - -vfile-location@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/vfile-location/-/vfile-location-4.1.0.tgz#69df82fb9ef0a38d0d02b90dd84620e120050dd0" - integrity sha512-YF23YMyASIIJXpktBa4vIGLJ5Gs88UB/XePgqPmTa7cDA+JeO3yclbpheQYCHjVHBn/yePzrXuygIL+xbvRYHw== - dependencies: - "@types/unist" "^2.0.0" - vfile "^5.0.0" - -vfile-location@^5.0.0: - version "5.0.2" - resolved "https://registry.yarnpkg.com/vfile-location/-/vfile-location-5.0.2.tgz#220d9ca1ab6f8b2504a4db398f7ebc149f9cb464" - integrity sha512-NXPYyxyBSH7zB5U6+3uDdd6Nybz6o6/od9rk8bp9H8GR3L+cm/fC0uUTbqBmUTnMCUDslAGBOIKNfvvb+gGlDg== - dependencies: - "@types/unist" "^3.0.0" - vfile "^6.0.0" - -vfile-message@^3.0.0: - version "3.1.4" - resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-3.1.4.tgz#15a50816ae7d7c2d1fa87090a7f9f96612b59dea" - integrity sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw== - dependencies: - "@types/unist" "^2.0.0" - unist-util-stringify-position "^3.0.0" - -vfile-message@^4.0.0: - version "4.0.2" - resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-4.0.2.tgz#c883c9f677c72c166362fd635f21fc165a7d1181" - integrity sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw== - dependencies: - "@types/unist" "^3.0.0" - unist-util-stringify-position "^4.0.0" - -vfile@^5.0.0: - version "5.3.7" - resolved "https://registry.yarnpkg.com/vfile/-/vfile-5.3.7.tgz#de0677e6683e3380fafc46544cfe603118826ab7" - integrity sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g== - dependencies: - "@types/unist" "^2.0.0" - is-buffer "^2.0.0" - unist-util-stringify-position "^3.0.0" - vfile-message "^3.0.0" - -vfile@^6.0.0, vfile@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/vfile/-/vfile-6.0.1.tgz#1e8327f41eac91947d4fe9d237a2dd9209762536" - integrity sha512-1bYqc7pt6NIADBJ98UiG0Bn/CHIVOoZ/IyEkqIruLg0mE1BKzkOXY2D6CSqQIcKqgadppE5lrxgWXJmXd7zZJw== - dependencies: - "@types/unist" "^3.0.0" - unist-util-stringify-position "^4.0.0" - vfile-message "^4.0.0" - -vm-browserify@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" - integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== - -wait-on@6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/wait-on/-/wait-on-6.0.1.tgz#16bbc4d1e4ebdd41c5b4e63a2e16dbd1f4e5601e" - integrity sha512-zht+KASY3usTY5u2LgaNqn/Cd8MukxLGjdcZxT2ns5QzDmTFc4XoWBgC+C/na+sMRZTuVygQoMYwdcVjHnYIVw== - dependencies: - axios "^0.25.0" - joi "^17.6.0" - lodash "^4.17.21" - minimist "^1.2.5" - rxjs "^7.5.4" - -warning@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3" - integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w== - dependencies: - loose-envify "^1.0.0" - -watchpack@^2.4.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.1.tgz#29308f2cac150fa8e4c92f90e0ec954a9fed7fff" - integrity sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg== - dependencies: - glob-to-regexp "^0.4.1" - graceful-fs "^4.1.2" - -wbuf@^1.1.0, wbuf@^1.7.3: - version "1.7.3" - resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df" - integrity sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA== - dependencies: - minimalistic-assert "^1.0.0" - -web-namespaces@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-2.0.1.tgz#1010ff7c650eccb2592cebeeaf9a1b253fd40692" - integrity sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ== - -web-worker@^1.2.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/web-worker/-/web-worker-1.3.0.tgz#e5f2df5c7fe356755a5fb8f8410d4312627e6776" - integrity sha512-BSR9wyRsy/KOValMgd5kMyr3JzpdeoR9KVId8u5GVlTTAtNChlsE4yTxeY7zMdNSyOmoKBv8NH2qeRY9Tg+IaA== - -webidl-conversions@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" - integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== - -webpack-bundle-analyzer@^4.9.0: - version "4.10.2" - resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.2.tgz#633af2862c213730be3dbdf40456db171b60d5bd" - integrity sha512-vJptkMm9pk5si4Bv922ZbKLV8UTT4zib4FPgXMhgzUny0bfDDkLXAVQs3ly3fS4/TN9ROFtb0NFrm04UXFE/Vw== - dependencies: - "@discoveryjs/json-ext" "0.5.7" - acorn "^8.0.4" - acorn-walk "^8.0.0" - commander "^7.2.0" - debounce "^1.2.1" - escape-string-regexp "^4.0.0" - gzip-size "^6.0.0" - html-escaper "^2.0.2" - opener "^1.5.2" - picocolors "^1.0.0" - sirv "^2.0.3" - ws "^7.3.1" - -webpack-dev-middleware@^5.3.4: - version "5.3.4" - resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz#eb7b39281cbce10e104eb2b8bf2b63fce49a3517" - integrity sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q== - dependencies: - colorette "^2.0.10" - memfs "^3.4.3" - mime-types "^2.1.31" - range-parser "^1.2.1" - schema-utils "^4.0.0" - -webpack-dev-server@^4.15.1: - version "4.15.2" - resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz#9e0c70a42a012560860adb186986da1248333173" - integrity sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g== - dependencies: - "@types/bonjour" "^3.5.9" - "@types/connect-history-api-fallback" "^1.3.5" - "@types/express" "^4.17.13" - "@types/serve-index" "^1.9.1" - "@types/serve-static" "^1.13.10" - "@types/sockjs" "^0.3.33" - "@types/ws" "^8.5.5" - ansi-html-community "^0.0.8" - bonjour-service "^1.0.11" - chokidar "^3.5.3" - colorette "^2.0.10" - compression "^1.7.4" - connect-history-api-fallback "^2.0.0" - default-gateway "^6.0.3" - express "^4.17.3" - graceful-fs "^4.2.6" - html-entities "^2.3.2" - http-proxy-middleware "^2.0.3" - ipaddr.js "^2.0.1" - launch-editor "^2.6.0" - open "^8.0.9" - p-retry "^4.5.0" - rimraf "^3.0.2" - schema-utils "^4.0.0" - selfsigned "^2.1.1" - serve-index "^1.9.1" - sockjs "^0.3.24" - spdy "^4.0.2" - webpack-dev-middleware "^5.3.4" - ws "^8.13.0" - -webpack-merge@^5.9.0: - version "5.10.0" - resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.10.0.tgz#a3ad5d773241e9c682803abf628d4cd62b8a4177" - integrity sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA== - dependencies: - clone-deep "^4.0.1" - flat "^5.0.2" - wildcard "^2.0.0" - -webpack-sources@^3.2.3: - version "3.2.3" - resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" - integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== - -webpack@^5.61.0, webpack@^5.88.1: - version "5.92.0" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.92.0.tgz#cc114c71e6851d220b1feaae90159ed52c876bdf" - integrity sha512-Bsw2X39MYIgxouNATyVpCNVWBCuUwDgWtN78g6lSdPJRLaQ/PUVm/oXcaRAyY/sMFoKFQrsPeqvTizWtq7QPCA== - dependencies: - "@types/eslint-scope" "^3.7.3" - "@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.0" - 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" - -webpackbar@^5.0.2: - version "5.0.2" - resolved "https://registry.yarnpkg.com/webpackbar/-/webpackbar-5.0.2.tgz#d3dd466211c73852741dfc842b7556dcbc2b0570" - integrity sha512-BmFJo7veBDgQzfWXl/wwYXr/VFus0614qZ8i9znqcl9fnEdiVkdbi0TedLQ6xAK92HZHDJ0QmyQ0fmuZPAgCYQ== - dependencies: - chalk "^4.1.0" - consola "^2.15.3" - pretty-time "^1.1.0" - std-env "^3.0.1" - -websocket-driver@>=0.5.1, websocket-driver@^0.7.4: - version "0.7.4" - resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760" - integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== - dependencies: - http-parser-js ">=0.5.1" - safe-buffer ">=5.1.0" - websocket-extensions ">=0.1.1" - -websocket-extensions@>=0.1.1: - version "0.1.4" - resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" - integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== - -whatwg-url@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" - integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== - dependencies: - tr46 "~0.0.3" - webidl-conversions "^3.0.0" - -which-typed-array@^1.1.14, which-typed-array@^1.1.2: - version "1.1.15" - resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.15.tgz#264859e9b11a649b388bfaaf4f767df1f779b38d" - integrity sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA== - 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" - -which@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" - integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== - dependencies: - isexe "^2.0.0" - -which@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" - integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== - dependencies: - isexe "^2.0.0" - -widest-line@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-4.0.1.tgz#a0fc673aaba1ea6f0a0d35b3c2795c9a9cc2ebf2" - integrity sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig== - dependencies: - string-width "^5.0.1" - -wildcard@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.1.tgz#5ab10d02487198954836b6349f74fff961e10f67" - integrity sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ== - -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrap-ansi@^8.0.1, wrap-ansi@^8.1.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" - integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== - dependencies: - ansi-styles "^6.1.0" - string-width "^5.0.1" - strip-ansi "^7.0.1" - -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== - -write-file-atomic@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8" - integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q== - dependencies: - imurmurhash "^0.1.4" - is-typedarray "^1.0.0" - signal-exit "^3.0.2" - typedarray-to-buffer "^3.1.5" - -ws@8.9.0: - version "8.9.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.9.0.tgz#2a994bb67144be1b53fe2d23c53c028adeb7f45e" - integrity sha512-Ja7nszREasGaYUYCI2k4lCKIRTt+y7XuqVoHR44YpI49TtryyqbqvDMn5eqfW7e6HzTukDRIsXqzVHScqRcafg== - -ws@^7.3.1: - version "7.5.10" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9" - integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ== - -ws@^8.13.0: - version "8.17.1" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b" - integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ== - -xdg-basedir@^5.0.1, xdg-basedir@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-5.1.0.tgz#1efba19425e73be1bc6f2a6ceb52a3d2c884c0c9" - integrity sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ== - -xml-formatter@^2.6.1: - version "2.6.1" - resolved "https://registry.yarnpkg.com/xml-formatter/-/xml-formatter-2.6.1.tgz#066ef3a100bd58ee3b943f0c503be63176d3d497" - integrity sha512-dOiGwoqm8y22QdTNI7A+N03tyVfBlQ0/oehAzxIZtwnFAHGeSlrfjF73YQvzSsa/Kt6+YZasKsrdu6OIpuBggw== - dependencies: - xml-parser-xo "^3.2.0" - -xml-js@^1.6.11: - version "1.6.11" - resolved "https://registry.yarnpkg.com/xml-js/-/xml-js-1.6.11.tgz#927d2f6947f7f1c19a316dd8eea3614e8b18f8e9" - integrity sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g== - dependencies: - sax "^1.2.4" - -xml-parser-xo@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/xml-parser-xo/-/xml-parser-xo-3.2.0.tgz#c633ab55cf1976d6b03ab4a6a85045093ac32b73" - integrity sha512-8LRU6cq+d7mVsoDaMhnkkt3CTtAs4153p49fRo+HIB3I1FD1o5CeXRjRH29sQevIfVJIcPjKSsPU/+Ujhq09Rg== - -xtend@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" - integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== - -y18n@^5.0.5: - version "5.0.8" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" - integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== - -yallist@^3.0.2: - version "3.1.1" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" - integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== - -yallist@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" - integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== - -yaml-ast-parser@0.0.43: - version "0.0.43" - resolved "https://registry.yarnpkg.com/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz#e8a23e6fb4c38076ab92995c5dca33f3d3d7c9bb" - integrity sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A== - -yaml@1.10.2, yaml@^1.10.0, yaml@^1.7.2: - version "1.10.2" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" - integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== - -yaml@^2.3.4: - version "2.4.5" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.4.5.tgz#60630b206dd6d84df97003d33fc1ddf6296cca5e" - integrity sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg== - -yargs-parser@^21.1.1: - version "21.1.1" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" - integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== - -yargs@^17.0.1: - version "17.7.2" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" - 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" - -yauzl@^2.10.0: - version "2.10.0" - resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" - integrity sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g== - dependencies: - buffer-crc32 "~0.2.3" - fd-slicer "~1.1.0" - -yocto-queue@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" - integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== - -yocto-queue@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251" - integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g== - -zwitch@^2.0.0: - version "2.0.4" - resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-2.0.4.tgz#c827d4b0acb76fc3e685a4c6ec2902d51070e9d7" - integrity sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A== diff --git a/e2e/config/host.docker.internal/zitadel.yaml b/e2e/config/host.docker.internal/zitadel.yaml index 203dd16437..23f35302b4 100644 --- a/e2e/config/host.docker.internal/zitadel.yaml +++ b/e2e/config/host.docker.internal/zitadel.yaml @@ -60,6 +60,9 @@ Projections: DefaultInstance: LoginPolicy: MfaInitSkipLifetime: "0" + Features: + LoginV2: + Required: false SystemAPIUsers: - cypress: diff --git a/e2e/config/localhost/docker-compose.yaml b/e2e/config/localhost/docker-compose.yaml index 41334d92f9..cb055dd27c 100644 --- a/e2e/config/localhost/docker-compose.yaml +++ b/e2e/config/localhost/docker-compose.yaml @@ -5,12 +5,12 @@ services: image: '${ZITADEL_IMAGE:-ghcr.io/zitadel/zitadel:latest}' build: context: ../../.. - dockerfile: ./build/Dockerfile + dockerfile: ./build/zitadel/Dockerfile target: artifact cache_from: - - type=gha + - type=gha cache_to: - - type=gha,mode=max + - type=gha,mode=max command: 'start-from-init --masterkey "MasterkeyNeedsToHave32Characters" --tlsMode disabled --config /zitadel.yaml --steps /zitadel.yaml' depends_on: db: @@ -18,15 +18,15 @@ services: volumes: - ./zitadel.yaml:/zitadel.yaml ports: - - "8080:8080" + - "8080:8080" healthcheck: - test: ["CMD", "/app/zitadel", "ready", "--config", "/zitadel.yaml" ] + test: [ "CMD", "/app/zitadel", "ready", "--config", "/zitadel.yaml" ] interval: '10s' timeout: '5s' retries: 5 start_period: '10s' extra_hosts: - - "host.docker.internal:host-gateway" + - "host.docker.internal:host-gateway" db: restart: 'always' @@ -35,7 +35,7 @@ services: - POSTGRES_USER=postgres - POSTGRES_PASSWORD=postgres healthcheck: - test: ["CMD-SHELL", "pg_isready", "-d", "zitadel", "-U", "postgres"] + test: [ "CMD-SHELL", "pg_isready", "-d", "zitadel", "-U", "postgres" ] interval: '10s' timeout: '30s' retries: 5 diff --git a/e2e/config/localhost/zitadel.yaml b/e2e/config/localhost/zitadel.yaml index 966bb4f6b7..701e7b806b 100644 --- a/e2e/config/localhost/zitadel.yaml +++ b/e2e/config/localhost/zitadel.yaml @@ -52,6 +52,9 @@ Quotas: DefaultInstance: LoginPolicy: MfaInitSkipLifetime: "0" + Features: + LoginV2: + Required: false SystemAPIUsers: - cypress: diff --git a/e2e/package.json b/e2e/package.json index 01c2995907..bf268a7de4 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -2,18 +2,19 @@ "name": "zitadel-e2e", "version": "0.0.0", "scripts": { - "open": "npx cypress open", - "e2e": "npx cypress run", - "open:golang": "npm run open --", - "e2e:golang": "npm run e2e --", - "open:golangangular": "CYPRESS_BASE_URL=http://localhost:4200 CYPRESS_BACKEND_URL=http://localhost:8080 npm run open --", - "e2e:golangangular": "CYPRESS_BASE_URL=http://localhost:4200 CYPRESS_BACKEND_URL=http://localhost:8080 npm run e2e --", - "open:angulargolang": "npm run open:golangangular --", - "e2e:angulargolang": "npm run e2e:golangangular --", - "open:angular": "CYPRESS_BASE_URL=http://localhost:4200 CYPRESS_BACKEND_URL=http://localhost:8080 CYPRESS_WEBHOOK_HANDLER_HOST=host.docker.internal npm run open --", - "e2e:angular": "CYPRESS_BASE_URL=http://localhost:4200 CYPRESS_BACKEND_URL=http://localhost:8080 CYPRESS_WEBHOOK_HANDLER_HOST=host.docker.internal npm run e2e --", + "open": "pnpm exec cypress open", + "test:e2e": "pnpm exec cypress run", + "test:open:golang": "pnpm run open --", + "test:e2e:golang": "pnpm run e2e --", + "test:open:golangangular": "CYPRESS_BASE_URL=http://localhost:4200 CYPRESS_BACKEND_URL=http://localhost:8080 pnpm run open --", + "test:e2e:golangangular": "CYPRESS_BASE_URL=http://localhost:4200 CYPRESS_BACKEND_URL=http://localhost:8080 pnpm run e2e --", + "test:open:angulargolang": "pnpm run open:golangangular --", + "test:e2e:angulargolang": "pnpm run e2e:golangangular --", + "test:open:angular": "CYPRESS_BASE_URL=http://localhost:4200 CYPRESS_BACKEND_URL=http://localhost:8080 CYPRESS_WEBHOOK_HANDLER_HOST=host.docker.internal pnpm run open --", + "test:e2e:angular": "CYPRESS_BASE_URL=http://localhost:4200 CYPRESS_BACKEND_URL=http://localhost:8080 CYPRESS_WEBHOOK_HANDLER_HOST=host.docker.internal pnpm run e2e --", "lint": "prettier --check cypress", - "lint:fix": "prettier --write cypress" + "lint:fix": "prettier --write cypress", + "clean": "rm -rf .turbo node_modules" }, "private": true, "dependencies": { @@ -29,6 +30,6 @@ }, "devDependencies": { "@types/node": "^22.3.0", - "cypress": "^13.13.3" + "cypress": "^14.5.3" } } diff --git a/e2e/turbo.json b/e2e/turbo.json new file mode 100644 index 0000000000..972f2b327a --- /dev/null +++ b/e2e/turbo.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://turbo.build/schema.json", + "extends": ["//"], + "tasks": { + "e2e": { + "cache": false, + "persistent": false + }, + "e2e:golang": { + "cache": false, + "persistent": false + }, + "e2e:golangangular": { + "cache": false, + "persistent": false + }, + "e2e:angular": { + "cache": false, + "persistent": false + }, + "lint": { + "outputs": [] + } + } +} diff --git a/e2e/yarn.lock b/e2e/yarn.lock deleted file mode 100644 index 53932921e9..0000000000 --- a/e2e/yarn.lock +++ /dev/null @@ -1,1683 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@colors/colors@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" - integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== - -"@cypress/request@^3.0.1": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@cypress/request/-/request-3.0.1.tgz#72d7d5425236a2413bd3d8bb66d02d9dc3168960" - integrity sha512-TWivJlJi8ZDx2wGOw1dbLuHJKUYX7bWySw377nlnGOW3hP9/MUKIsEdXT/YngWxVdgNCHRBmFlBipE+5/2ZZlQ== - 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" - -"@cypress/xvfb@^1.2.4": - version "1.2.4" - resolved "https://registry.yarnpkg.com/@cypress/xvfb/-/xvfb-1.2.4.tgz#2daf42e8275b39f4aa53c14214e557bd14e7748a" - integrity sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q== - dependencies: - debug "^3.1.0" - lodash.once "^4.1.1" - -"@hapi/hoek@^9.0.0", "@hapi/hoek@^9.3.0": - version "9.3.0" - resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.3.0.tgz#8368869dcb735be2e7f5cb7647de78e167a251fb" - integrity sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ== - -"@hapi/topo@^5.1.0": - version "5.1.0" - resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-5.1.0.tgz#dc448e332c6c6e37a4dc02fd84ba8d44b9afb012" - integrity sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg== - dependencies: - "@hapi/hoek" "^9.0.0" - -"@sideway/address@^4.1.5": - version "4.1.5" - resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.5.tgz#4bc149a0076623ced99ca8208ba780d65a99b9d5" - integrity sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q== - dependencies: - "@hapi/hoek" "^9.0.0" - -"@sideway/formula@^3.0.1": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.1.tgz#80fcbcbaf7ce031e0ef2dd29b1bfc7c3f583611f" - integrity sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg== - -"@sideway/pinpoint@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@sideway/pinpoint/-/pinpoint-2.0.0.tgz#cff8ffadc372ad29fd3f78277aeb29e632cc70df" - integrity sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ== - -"@types/node@*", "@types/node@^22.3.0": - version "22.3.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-22.3.0.tgz#7f8da0e2b72c27c4f9bd3cb5ef805209d04d4f9e" - integrity sha512-nrWpWVaDZuaVc5X84xJ0vNrLvomM205oQyLsRt7OHNZbSHslcWsvgFR7O7hire2ZonjLrWBbedmotmIlJDVd6g== - dependencies: - undici-types "~6.18.2" - -"@types/pg@^8.11.6": - version "8.11.6" - resolved "https://registry.yarnpkg.com/@types/pg/-/pg-8.11.6.tgz#a2d0fb0a14b53951a17df5197401569fb9c0c54b" - integrity sha512-/2WmmBXHLsfRqzfHW7BNZ8SbYzE8OSk7i3WjFYvfgRHj7S1xj+16Je5fUKv3lVdVzk/zn9TXOqf+avFCFIE0yQ== - dependencies: - "@types/node" "*" - pg-protocol "*" - pg-types "^4.0.1" - -"@types/sinonjs__fake-timers@8.1.1": - version "8.1.1" - resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz#b49c2c70150141a15e0fa7e79cf1f92a72934ce3" - integrity sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g== - -"@types/sizzle@^2.3.2": - version "2.3.8" - resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.8.tgz#518609aefb797da19bf222feb199e8f653ff7627" - integrity sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg== - -"@types/yauzl@^2.9.1": - version "2.10.3" - resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.10.3.tgz#e9b2808b4f109504a03cda958259876f61017999" - integrity sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q== - dependencies: - "@types/node" "*" - -aggregate-error@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" - integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== - dependencies: - clean-stack "^2.0.0" - indent-string "^4.0.0" - -ansi-colors@^4.1.1: - version "4.1.3" - resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" - integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw== - -ansi-escapes@^4.3.0: - version "4.3.2" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" - integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== - dependencies: - type-fest "^0.21.3" - -ansi-regex@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" - integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== - -ansi-styles@^4.0.0, ansi-styles@^4.1.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" - integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== - dependencies: - color-convert "^2.0.1" - -arch@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/arch/-/arch-2.2.0.tgz#1bc47818f305764f23ab3306b0bfc086c5a29d11" - integrity sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ== - -asn1@~0.2.3: - version "0.2.6" - resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d" - integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ== - dependencies: - safer-buffer "~2.1.0" - -assert-plus@1.0.0, assert-plus@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" - integrity sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw== - -astral-regex@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" - integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== - -async@^3.2.0: - version "3.2.5" - resolved "https://registry.yarnpkg.com/async/-/async-3.2.5.tgz#ebd52a8fdaf7a2289a24df399f8d8485c8a46b66" - integrity sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg== - -asynckit@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" - integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== - -at-least-node@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" - integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== - -aws-sign2@~0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" - integrity sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA== - -aws4@^1.8.0: - version "1.13.1" - resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.13.1.tgz#bb5f8b8a20739f6ae1caeaf7eea2c7913df8048e" - integrity sha512-u5w79Rd7SU4JaIlA/zFqG+gOiuq25q5VLyZ8E+ijJeILuTxVzZgp2CaGw/UTw6pXYN9XMO9yiqj/nEHmhTG5CA== - -axios@^1.6.1: - version "1.8.4" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.8.4.tgz#78990bb4bc63d2cae072952d374835950a82f447" - integrity sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw== - dependencies: - follow-redirects "^1.15.6" - form-data "^4.0.0" - proxy-from-env "^1.1.0" - -base64-js@^1.3.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" - integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== - -bcrypt-pbkdf@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" - integrity sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w== - dependencies: - tweetnacl "^0.14.3" - -blob-util@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/blob-util/-/blob-util-2.0.2.tgz#3b4e3c281111bb7f11128518006cdc60b403a1eb" - integrity sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ== - -bluebird@^3.7.2: - version "3.7.2" - resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" - integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== - -buffer-crc32@~0.2.3: - version "0.2.13" - resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" - integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ== - -buffer-equal-constant-time@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" - integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA== - -buffer@^5.7.1: - version "5.7.1" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" - integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== - dependencies: - base64-js "^1.3.1" - ieee754 "^1.1.13" - -cachedir@^2.3.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/cachedir/-/cachedir-2.4.0.tgz#7fef9cf7367233d7c88068fe6e34ed0d355a610d" - integrity sha512-9EtFOZR8g22CL7BWjJ9BUx1+A/djkofnyW3aOXZORNW2kxoUpx2h+uN2cOqwPmFhnpVmxg+KW2OjOSgChTEvsQ== - -call-bind@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" - integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== - dependencies: - 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" - -caseless@~0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" - integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw== - -chalk@^4.1.0, chalk@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" - integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - -check-more-types@^2.24.0: - version "2.24.0" - resolved "https://registry.yarnpkg.com/check-more-types/-/check-more-types-2.24.0.tgz#1420ffb10fd444dcfc79b43891bbfffd32a84600" - integrity sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA== - -ci-info@^3.2.0: - version "3.9.0" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4" - integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== - -clean-stack@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" - integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== - -cli-cursor@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" - integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== - dependencies: - restore-cursor "^3.1.0" - -cli-table3@~0.6.1: - version "0.6.5" - resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.5.tgz#013b91351762739c16a9567c21a04632e449bf2f" - integrity sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ== - dependencies: - string-width "^4.2.0" - optionalDependencies: - "@colors/colors" "1.5.0" - -cli-truncate@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-2.1.0.tgz#c39e28bf05edcde5be3b98992a22deed5a2b93c7" - integrity sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg== - dependencies: - slice-ansi "^3.0.0" - string-width "^4.2.0" - -cliui@^8.0.1: - version "8.0.1" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" - 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" - -color-convert@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" - integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== - dependencies: - color-name "~1.1.4" - -color-name@~1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" - integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== - -colorette@^2.0.16: - version "2.0.20" - resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" - integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== - -combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: - version "1.0.8" - resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" - integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== - dependencies: - delayed-stream "~1.0.0" - -commander@^6.2.1: - version "6.2.1" - resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" - integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== - -common-tags@^1.8.0: - version "1.8.2" - resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.2.tgz#94ebb3c076d26032745fd54face7f688ef5ac9c6" - integrity sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA== - -core-util-is@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" - integrity sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ== - -cross-spawn@^7.0.0: - version "7.0.6" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" - integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== - dependencies: - path-key "^3.1.0" - shebang-command "^2.0.0" - which "^2.0.1" - -cypress-wait-until@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/cypress-wait-until/-/cypress-wait-until-3.0.2.tgz#c90dddfa4c46a2c422f5b91d486531c560bae46e" - integrity sha512-iemies796dD5CgjG5kV0MnpEmKSH+s7O83ZoJLVzuVbZmm4lheMsZqAVT73hlMx4QlkwhxbyUzhOBUOZwoOe0w== - -cypress@^13.13.3: - version "13.13.3" - resolved "https://registry.yarnpkg.com/cypress/-/cypress-13.13.3.tgz#21ee054bb4e00b3858f2e33b4f8f4e69128470a9" - integrity sha512-hUxPrdbJXhUOTzuML+y9Av7CKoYznbD83pt8g3klgpioEha0emfx4WNIuVRx0C76r0xV2MIwAW9WYiXfVJYFQw== - dependencies: - "@cypress/request" "^3.0.1" - "@cypress/xvfb" "^1.2.4" - "@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.7.1" - 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.1" - 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.3" - untildify "^4.0.0" - yauzl "^2.10.0" - -dashdash@^1.12.0: - version "1.14.1" - resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" - integrity sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g== - dependencies: - assert-plus "^1.0.0" - -dateformat@^4.5.1: - version "4.6.3" - resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-4.6.3.tgz#556fa6497e5217fedb78821424f8a1c22fa3f4b5" - integrity sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA== - -dayjs@^1.10.4: - version "1.11.12" - resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.12.tgz#5245226cc7f40a15bf52e0b99fd2a04669ccac1d" - integrity sha512-Rt2g+nTbLlDWZTwwrIXjy9MeiZmSDI375FvZs72ngxx8PDC6YXOeR3q5LAuPzjZQxhiWdRKac7RKV+YyQYfYIg== - -debug@^3.1.0: - version "3.2.7" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" - integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== - dependencies: - ms "^2.1.1" - -debug@^4.1.1, debug@^4.3.4: - version "4.3.6" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.6.tgz#2ab2c38fbaffebf8aa95fdfe6d88438c7a13c52b" - integrity sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg== - dependencies: - ms "2.1.2" - -define-data-property@^1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" - integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== - dependencies: - es-define-property "^1.0.0" - es-errors "^1.3.0" - gopd "^1.0.1" - -delayed-stream@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" - integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== - -diff@^5.0.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/diff/-/diff-5.2.0.tgz#26ded047cd1179b78b9537d5ef725503ce1ae531" - integrity sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A== - -ecc-jsbn@~0.1.1: - version "0.1.2" - resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" - integrity sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw== - dependencies: - jsbn "~0.1.0" - safer-buffer "^2.1.0" - -ecdsa-sig-formatter@1.0.11: - version "1.0.11" - resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" - integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== - dependencies: - safe-buffer "^5.0.1" - -emoji-regex@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" - integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== - -end-of-stream@^1.1.0: - version "1.4.4" - resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" - integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== - dependencies: - once "^1.4.0" - -enquirer@^2.3.6: - version "2.4.1" - resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.4.1.tgz#93334b3fbd74fc7097b224ab4a8fb7e40bf4ae56" - integrity sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ== - dependencies: - ansi-colors "^4.1.1" - strip-ansi "^6.0.1" - -es-define-property@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" - integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== - dependencies: - get-intrinsic "^1.2.4" - -es-errors@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" - integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== - -escalade@^3.1.1: - version "3.1.2" - resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27" - integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA== - -escape-html@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" - integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== - -escape-string-regexp@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== - -eventemitter2@6.4.7: - version "6.4.7" - resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.7.tgz#a7f6c4d7abf28a14c1ef3442f21cb306a054271d" - integrity sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg== - -execa@4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-4.1.0.tgz#4e5491ad1572f2f17a77d388c6c857135b22847a" - integrity sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA== - 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" - -executable@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/executable/-/executable-4.1.1.tgz#41532bff361d3e57af4d763b70582db18f5d133c" - integrity sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg== - dependencies: - pify "^2.2.0" - -extend@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" - integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== - -extract-zip@2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a" - integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg== - dependencies: - debug "^4.1.1" - get-stream "^5.1.0" - yauzl "^2.10.0" - optionalDependencies: - "@types/yauzl" "^2.9.1" - -extsprintf@1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" - integrity sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g== - -extsprintf@^1.2.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.1.tgz#8d172c064867f235c0c84a596806d279bf4bcc07" - integrity sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA== - -fd-slicer@~1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" - integrity sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g== - dependencies: - pend "~1.2.0" - -figures@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" - integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== - dependencies: - escape-string-regexp "^1.0.5" - -follow-redirects@^1.15.6: - version "1.15.6" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" - integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== - -forever-agent@~0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" - integrity sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw== - -form-data@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" - integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.8" - mime-types "^2.1.12" - -form-data@~2.3.2: - version "2.3.3" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" - integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.6" - mime-types "^2.1.12" - -fs-extra@^10.0.0: - version "10.1.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf" - integrity sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ== - dependencies: - graceful-fs "^4.2.0" - jsonfile "^6.0.1" - universalify "^2.0.0" - -fs-extra@^9.1.0: - version "9.1.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" - integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== - dependencies: - at-least-node "^1.0.0" - graceful-fs "^4.2.0" - jsonfile "^6.0.1" - universalify "^2.0.0" - -fsu@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/fsu/-/fsu-1.1.1.tgz#bd36d3579907c59d85b257a75b836aa9e0c31834" - integrity sha512-xQVsnjJ/5pQtcKh+KjUoZGzVWn4uNkchxTF6Lwjr4Gf7nQr8fmUfhKJ62zE77+xQg9xnxi5KUps7XGs+VC986A== - -function-bind@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" - integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== - -get-caller-file@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" - integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== - -get-intrinsic@^1.1.3, get-intrinsic@^1.2.4: - version "1.2.4" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" - integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== - dependencies: - es-errors "^1.3.0" - function-bind "^1.1.2" - has-proto "^1.0.1" - has-symbols "^1.0.3" - hasown "^2.0.0" - -get-stream@^5.0.0, get-stream@^5.1.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" - integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== - dependencies: - pump "^3.0.0" - -getos@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/getos/-/getos-3.2.1.tgz#0134d1f4e00eb46144c5a9c0ac4dc087cbb27dc5" - integrity sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q== - dependencies: - async "^3.2.0" - -getpass@^0.1.1: - version "0.1.7" - resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" - integrity sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng== - dependencies: - assert-plus "^1.0.0" - -global-dirs@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-3.0.1.tgz#0c488971f066baceda21447aecb1a8b911d22485" - integrity sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA== - dependencies: - ini "2.0.0" - -gopd@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" - integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== - dependencies: - get-intrinsic "^1.1.3" - -graceful-fs@^4.1.6, graceful-fs@^4.2.0: - version "4.2.11" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" - integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== - -has-flag@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" - integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== - -has-property-descriptors@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" - integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== - dependencies: - es-define-property "^1.0.0" - -has-proto@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" - integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== - -has-symbols@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" - integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== - -hasown@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" - integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== - dependencies: - function-bind "^1.1.2" - -http-signature@~1.3.6: - version "1.3.6" - resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.3.6.tgz#cb6fbfdf86d1c974f343be94e87f7fc128662cf9" - integrity sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw== - dependencies: - assert-plus "^1.0.0" - jsprim "^2.0.2" - sshpk "^1.14.1" - -human-signals@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" - integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== - -ieee754@^1.1.13: - version "1.2.1" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" - integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== - -indent-string@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" - integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== - -ini@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ini/-/ini-2.0.0.tgz#e5fd556ecdd5726be978fa1001862eacb0a94bc5" - integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA== - -is-ci@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-3.0.1.tgz#db6ecbed1bd659c43dac0f45661e7674103d1867" - integrity sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ== - dependencies: - ci-info "^3.2.0" - -is-fullwidth-code-point@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" - integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== - -is-installed-globally@~0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.4.0.tgz#9a0fd407949c30f86eb6959ef1b7994ed0b7b520" - integrity sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ== - dependencies: - global-dirs "^3.0.0" - is-path-inside "^3.0.2" - -is-path-inside@^3.0.2: - version "3.0.3" - resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" - integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== - -is-stream@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" - integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== - -is-typedarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" - integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== - -is-unicode-supported@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" - integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== - -isexe@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== - -isstream@~0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" - integrity sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g== - -joi@^17.11.0: - version "17.13.3" - resolved "https://registry.yarnpkg.com/joi/-/joi-17.13.3.tgz#0f5cc1169c999b30d344366d384b12d92558bcec" - integrity sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA== - dependencies: - "@hapi/hoek" "^9.3.0" - "@hapi/topo" "^5.1.0" - "@sideway/address" "^4.1.5" - "@sideway/formula" "^3.0.1" - "@sideway/pinpoint" "^2.0.0" - -"js-tokens@^3.0.0 || ^4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" - integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== - -jsbn@~0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" - integrity sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg== - -json-schema@0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5" - integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA== - -json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" - integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== - -jsonfile@^6.0.1: - version "6.1.0" - resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" - integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== - dependencies: - universalify "^2.0.0" - optionalDependencies: - graceful-fs "^4.1.6" - -jsonwebtoken@^9.0.2: - version "9.0.2" - resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz#65ff91f4abef1784697d40952bb1998c504caaf3" - integrity sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ== - dependencies: - jws "^3.2.2" - lodash.includes "^4.3.0" - lodash.isboolean "^3.0.3" - lodash.isinteger "^4.0.4" - lodash.isnumber "^3.0.3" - lodash.isplainobject "^4.0.6" - lodash.isstring "^4.0.1" - lodash.once "^4.0.0" - ms "^2.1.1" - semver "^7.5.4" - -jsprim@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-2.0.2.tgz#77ca23dbcd4135cd364800d22ff82c2185803d4d" - integrity sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ== - dependencies: - assert-plus "1.0.0" - extsprintf "1.3.0" - json-schema "0.4.0" - verror "1.10.0" - -jwa@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" - integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== - dependencies: - buffer-equal-constant-time "1.0.1" - ecdsa-sig-formatter "1.0.11" - safe-buffer "^5.0.1" - -jws@^3.2.2: - version "3.2.2" - resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" - integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== - dependencies: - jwa "^1.4.1" - safe-buffer "^5.0.1" - -lazy-ass@^1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/lazy-ass/-/lazy-ass-1.6.0.tgz#7999655e8646c17f089fdd187d150d3324d54513" - integrity sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw== - -listr2@^3.8.3: - version "3.14.0" - resolved "https://registry.yarnpkg.com/listr2/-/listr2-3.14.0.tgz#23101cc62e1375fd5836b248276d1d2b51fdbe9e" - integrity sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g== - 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" - -lodash.includes@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" - integrity sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w== - -lodash.isboolean@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" - integrity sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg== - -lodash.isempty@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/lodash.isempty/-/lodash.isempty-4.4.0.tgz#6f86cbedd8be4ec987be9aaf33c9684db1b31e7e" - integrity sha512-oKMuF3xEeqDltrGMfDxAPGIVMSSRv8tbRSODbrs4KGsRRLEhrW8N8Rd4DRgB2+621hY8A8XwwrTVhXWpxFvMzg== - -lodash.isfunction@^3.0.9: - version "3.0.9" - resolved "https://registry.yarnpkg.com/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz#06de25df4db327ac931981d1bdb067e5af68d051" - integrity sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw== - -lodash.isinteger@^4.0.4: - version "4.0.4" - resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" - integrity sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA== - -lodash.isnumber@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" - integrity sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw== - -lodash.isobject@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/lodash.isobject/-/lodash.isobject-3.0.2.tgz#3c8fb8d5b5bf4bf90ae06e14f2a530a4ed935e1d" - integrity sha512-3/Qptq2vr7WeJbB4KHUSKlq8Pl7ASXi3UG6CMbBm8WRtXi8+GHm7mKaU3urfpSEzWe2wCIChs6/sdocUsTKJiA== - -lodash.isplainobject@^4.0.6: - version "4.0.6" - resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" - integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA== - -lodash.isstring@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" - integrity sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw== - -lodash.once@^4.0.0, lodash.once@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" - integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg== - -lodash@^4.17.21: - version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" - integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== - -log-symbols@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" - integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== - dependencies: - chalk "^4.1.0" - is-unicode-supported "^0.1.0" - -log-update@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/log-update/-/log-update-4.0.0.tgz#589ecd352471f2a1c0c570287543a64dfd20e0a1" - integrity sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg== - dependencies: - ansi-escapes "^4.3.0" - cli-cursor "^3.1.0" - slice-ansi "^4.0.0" - wrap-ansi "^6.2.0" - -loose-envify@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" - integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== - dependencies: - js-tokens "^3.0.0 || ^4.0.0" - -merge-stream@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" - integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== - -mime-db@1.52.0: - version "1.52.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" - integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== - -mime-types@^2.1.12, mime-types@~2.1.19: - version "2.1.35" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" - integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== - dependencies: - mime-db "1.52.0" - -mimic-fn@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" - integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== - -minimist@^1.2.8: - version "1.2.8" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" - integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== - -mochawesome-report-generator@^6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/mochawesome-report-generator/-/mochawesome-report-generator-6.2.0.tgz#65a30a11235ba7a68e1cf0ca1df80d764b93ae78" - integrity sha512-Ghw8JhQFizF0Vjbtp9B0i//+BOkV5OWcQCPpbO0NGOoxV33o+gKDYU0Pr2pGxkIHnqZ+g5mYiXF7GMNgAcDpSg== - dependencies: - chalk "^4.1.2" - dateformat "^4.5.1" - escape-html "^1.0.3" - fs-extra "^10.0.0" - fsu "^1.1.1" - lodash.isfunction "^3.0.9" - opener "^1.5.2" - prop-types "^15.7.2" - tcomb "^3.2.17" - tcomb-validation "^3.3.0" - validator "^13.6.0" - yargs "^17.2.1" - -mochawesome@^7.1.3: - version "7.1.3" - resolved "https://registry.yarnpkg.com/mochawesome/-/mochawesome-7.1.3.tgz#07b358138f37f5b07b51a1b255d84babfa36fa83" - integrity sha512-Vkb3jR5GZ1cXohMQQ73H3cZz7RoxGjjUo0G5hu0jLaW+0FdUxUwg3Cj29bqQdh0rFcnyV06pWmqmi5eBPnEuNQ== - dependencies: - chalk "^4.1.2" - diff "^5.0.0" - json-stringify-safe "^5.0.1" - lodash.isempty "^4.4.0" - lodash.isfunction "^3.0.9" - lodash.isobject "^3.0.2" - lodash.isstring "^4.0.1" - mochawesome-report-generator "^6.2.0" - strip-ansi "^6.0.1" - uuid "^8.3.2" - -ms@2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" - integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== - -ms@^2.1.1: - version "2.1.3" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" - integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== - -npm-run-path@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" - integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== - dependencies: - path-key "^3.0.0" - -object-assign@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" - integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== - -object-inspect@^1.13.1: - version "1.13.2" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.2.tgz#dea0088467fb991e67af4058147a24824a3043ff" - integrity sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g== - -obuf@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" - integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== - -once@^1.3.1, once@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== - dependencies: - wrappy "1" - -onetime@^5.1.0: - version "5.1.2" - resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" - integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== - dependencies: - mimic-fn "^2.1.0" - -opener@^1.5.2: - version "1.5.2" - resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598" - integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A== - -ospath@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/ospath/-/ospath-1.2.2.tgz#1276639774a3f8ef2572f7fe4280e0ea4550c07b" - integrity sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA== - -p-map@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" - integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== - dependencies: - aggregate-error "^3.0.0" - -path-key@^3.0.0, path-key@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" - integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== - -pend@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" - integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg== - -performance-now@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" - integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow== - -pg-cloudflare@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz#e6d5833015b170e23ae819e8c5d7eaedb472ca98" - integrity sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q== - -pg-connection-string@^2.6.4: - version "2.6.4" - resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.6.4.tgz#f543862adfa49fa4e14bc8a8892d2a84d754246d" - integrity sha512-v+Z7W/0EO707aNMaAEfiGnGL9sxxumwLl2fJvCQtMn9Fxsg+lPpPkdcyBSv/KFgpGdYkMfn+EI1Or2EHjpgLCA== - -pg-int8@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c" - integrity sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw== - -pg-numeric@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/pg-numeric/-/pg-numeric-1.0.2.tgz#816d9a44026086ae8ae74839acd6a09b0636aa3a" - integrity sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw== - -pg-pool@^3.6.2: - version "3.6.2" - resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.6.2.tgz#3a592370b8ae3f02a7c8130d245bc02fa2c5f3f2" - integrity sha512-Htjbg8BlwXqSBQ9V8Vjtc+vzf/6fVUuak/3/XXKA9oxZprwW3IMDQTGHP+KDmVL7rtd+R1QjbnCFPuTHm3G4hg== - -pg-protocol@*, pg-protocol@^1.6.1: - version "1.6.1" - resolved "https://registry.yarnpkg.com/pg-protocol/-/pg-protocol-1.6.1.tgz#21333e6d83b01faaebfe7a33a7ad6bfd9ed38cb3" - integrity sha512-jPIlvgoD63hrEuihvIg+tJhoGjUsLPn6poJY9N5CnlPd91c2T18T/9zBtLxZSb1EhYxBRoZJtzScCaWlYLtktg== - -pg-types@^2.1.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-2.2.0.tgz#2d0250d636454f7cfa3b6ae0382fdfa8063254a3" - integrity sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA== - dependencies: - pg-int8 "1.0.1" - postgres-array "~2.0.0" - postgres-bytea "~1.0.0" - postgres-date "~1.0.4" - postgres-interval "^1.1.0" - -pg-types@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-4.0.2.tgz#399209a57c326f162461faa870145bb0f918b76d" - integrity sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng== - dependencies: - pg-int8 "1.0.1" - pg-numeric "1.0.2" - postgres-array "~3.0.1" - postgres-bytea "~3.0.0" - postgres-date "~2.1.0" - postgres-interval "^3.0.0" - postgres-range "^1.1.1" - -pg@^8.12.0: - version "8.12.0" - resolved "https://registry.yarnpkg.com/pg/-/pg-8.12.0.tgz#9341724db571022490b657908f65aee8db91df79" - integrity sha512-A+LHUSnwnxrnL/tZ+OLfqR1SxLN3c/pgDztZ47Rpbsd4jUytsTtwQo/TLPRzPJMp/1pbhYVhH9cuSZLAajNfjQ== - dependencies: - pg-connection-string "^2.6.4" - pg-pool "^3.6.2" - pg-protocol "^1.6.1" - pg-types "^2.1.0" - pgpass "1.x" - optionalDependencies: - pg-cloudflare "^1.1.1" - -pgpass@1.x: - version "1.0.5" - resolved "https://registry.yarnpkg.com/pgpass/-/pgpass-1.0.5.tgz#9b873e4a564bb10fa7a7dbd55312728d422a223d" - integrity sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug== - dependencies: - split2 "^4.1.0" - -pify@^2.2.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" - integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog== - -postgres-array@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-2.0.0.tgz#48f8fce054fbc69671999329b8834b772652d82e" - integrity sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA== - -postgres-array@~3.0.1: - version "3.0.2" - resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-3.0.2.tgz#68d6182cb0f7f152a7e60dc6a6889ed74b0a5f98" - integrity sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog== - -postgres-bytea@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/postgres-bytea/-/postgres-bytea-1.0.0.tgz#027b533c0aa890e26d172d47cf9ccecc521acd35" - integrity sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w== - -postgres-bytea@~3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/postgres-bytea/-/postgres-bytea-3.0.0.tgz#9048dc461ac7ba70a6a42d109221619ecd1cb089" - integrity sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw== - dependencies: - obuf "~1.1.2" - -postgres-date@~1.0.4: - version "1.0.7" - resolved "https://registry.yarnpkg.com/postgres-date/-/postgres-date-1.0.7.tgz#51bc086006005e5061c591cee727f2531bf641a8" - integrity sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q== - -postgres-date@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/postgres-date/-/postgres-date-2.1.0.tgz#b85d3c1fb6fb3c6c8db1e9942a13a3bf625189d0" - integrity sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA== - -postgres-interval@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/postgres-interval/-/postgres-interval-1.2.0.tgz#b460c82cb1587507788819a06aa0fffdb3544695" - integrity sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ== - dependencies: - xtend "^4.0.0" - -postgres-interval@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/postgres-interval/-/postgres-interval-3.0.0.tgz#baf7a8b3ebab19b7f38f07566c7aab0962f0c86a" - integrity sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw== - -postgres-range@^1.1.1: - version "1.1.4" - resolved "https://registry.yarnpkg.com/postgres-range/-/postgres-range-1.1.4.tgz#a59c5f9520909bcec5e63e8cf913a92e4c952863" - integrity sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w== - -prettier@^3.3.3: - version "3.3.3" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.3.tgz#30c54fe0be0d8d12e6ae61dbb10109ea00d53105" - integrity sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew== - -pretty-bytes@^5.6.0: - version "5.6.0" - resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" - integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg== - -process@^0.11.10: - version "0.11.10" - resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" - integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== - -prop-types@^15.7.2: - version "15.8.1" - resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" - integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== - dependencies: - loose-envify "^1.4.0" - object-assign "^4.1.1" - react-is "^16.13.1" - -proxy-from-env@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.0.0.tgz#33c50398f70ea7eb96d21f7b817630a55791c7ee" - integrity sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A== - -proxy-from-env@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" - integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== - -psl@^1.1.33: - version "1.9.0" - resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" - integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag== - -pump@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" - integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== - dependencies: - end-of-stream "^1.1.0" - once "^1.3.1" - -punycode@^2.1.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" - integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== - -qs@6.10.4: - version "6.10.4" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.4.tgz#6a3003755add91c0ec9eacdc5f878b034e73f9e7" - integrity sha512-OQiU+C+Ds5qiH91qh/mg0w+8nwQuLjM4F4M/PbmhDOoYehPh+Fb0bDjtR1sOvy7YKxvj28Y/M0PhP5uVX0kB+g== - dependencies: - side-channel "^1.0.4" - -querystringify@^2.1.1: - version "2.2.0" - resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" - integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== - -react-is@^16.13.1: - version "16.13.1" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" - integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== - -request-progress@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/request-progress/-/request-progress-3.0.0.tgz#4ca754081c7fec63f505e4faa825aa06cd669dbe" - integrity sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg== - dependencies: - throttleit "^1.0.0" - -require-directory@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" - integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== - -requires-port@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" - integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== - -restore-cursor@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" - integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== - dependencies: - onetime "^5.1.0" - signal-exit "^3.0.2" - -rfdc@^1.3.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.4.1.tgz#778f76c4fb731d93414e8f925fbecf64cce7f6ca" - integrity sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA== - -rxjs@^7.5.1, rxjs@^7.8.1: - version "7.8.1" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543" - integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg== - dependencies: - tslib "^2.1.0" - -safe-buffer@^5.0.1, safe-buffer@^5.1.2: - version "5.2.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== - -safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: - version "2.1.2" - resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" - integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== - -semver@^7.5.3, semver@^7.5.4: - version "7.6.3" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" - integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== - -set-function-length@^1.2.1: - version "1.2.2" - resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" - integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== - dependencies: - define-data-property "^1.1.4" - es-errors "^1.3.0" - function-bind "^1.1.2" - get-intrinsic "^1.2.4" - gopd "^1.0.1" - has-property-descriptors "^1.0.2" - -shebang-command@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" - integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== - dependencies: - shebang-regex "^3.0.0" - -shebang-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" - integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== - -side-channel@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" - integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== - dependencies: - call-bind "^1.0.7" - es-errors "^1.3.0" - get-intrinsic "^1.2.4" - object-inspect "^1.13.1" - -signal-exit@^3.0.2: - version "3.0.7" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" - integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== - -slice-ansi@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-3.0.0.tgz#31ddc10930a1b7e0b67b08c96c2f49b77a789787" - integrity sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ== - dependencies: - ansi-styles "^4.0.0" - astral-regex "^2.0.0" - is-fullwidth-code-point "^3.0.0" - -slice-ansi@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" - integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ== - dependencies: - ansi-styles "^4.0.0" - astral-regex "^2.0.0" - is-fullwidth-code-point "^3.0.0" - -split2@^4.1.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4" - integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg== - -sshpk@^1.14.1: - version "1.18.0" - resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.18.0.tgz#1663e55cddf4d688b86a46b77f0d5fe363aba028" - integrity sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ== - 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" - -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - 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" - -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-final-newline@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" - integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== - -supports-color@^7.1.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" - integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== - dependencies: - has-flag "^4.0.0" - -supports-color@^8.1.1: - version "8.1.1" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" - integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== - dependencies: - has-flag "^4.0.0" - -tcomb-validation@^3.3.0: - version "3.4.1" - resolved "https://registry.yarnpkg.com/tcomb-validation/-/tcomb-validation-3.4.1.tgz#a7696ec176ce56a081d9e019f8b732a5a8894b65" - integrity sha512-urVVMQOma4RXwiVCa2nM2eqrAomHROHvWPuj6UkDGz/eb5kcy0x6P0dVt6kzpUZtYMNoAqJLWmz1BPtxrtjtrA== - dependencies: - tcomb "^3.0.0" - -tcomb@^3.0.0, tcomb@^3.2.17: - version "3.2.29" - resolved "https://registry.yarnpkg.com/tcomb/-/tcomb-3.2.29.tgz#32404fe9456d90c2cf4798682d37439f1ccc386c" - integrity sha512-di2Hd1DB2Zfw6StGv861JoAF5h/uQVu/QJp2g8KVbtfKnoHdBQl5M32YWq6mnSYBQ1vFFrns5B1haWJL7rKaOQ== - -throttleit@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.1.tgz#304ec51631c3b770c65c6c6f76938b384000f4d5" - integrity sha512-vDZpf9Chs9mAdfY046mcPt8fg5QSZr37hEH4TXYBnDF+izxgrbRGUAAaBvIk/fJm9aOFCGFd1EsNg5AZCbnQCQ== - -through@^2.3.8: - version "2.3.8" - resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" - integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== - -tmp@~0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.3.tgz#eb783cc22bc1e8bebd0671476d46ea4eb32a79ae" - integrity sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w== - -tough-cookie@^4.1.3: - version "4.1.4" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.4.tgz#945f1461b45b5a8c76821c33ea49c3ac192c1b36" - integrity sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag== - dependencies: - psl "^1.1.33" - punycode "^2.1.1" - universalify "^0.2.0" - url-parse "^1.5.3" - -tslib@^2.1.0: - version "2.6.3" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" - integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== - -tunnel-agent@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" - integrity sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w== - dependencies: - safe-buffer "^5.0.1" - -tweetnacl@^0.14.3, tweetnacl@~0.14.0: - version "0.14.5" - resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" - integrity sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA== - -type-fest@^0.21.3: - version "0.21.3" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" - integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== - -typescript@^5.5.4: - version "5.5.4" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.4.tgz#d9852d6c82bad2d2eda4fd74a5762a8f5909e9ba" - integrity sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q== - -undici-types@~6.18.2: - version "6.18.2" - resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.18.2.tgz#8b678cf939d4fc9ec56be3c68ed69c619dee28b0" - integrity sha512-5ruQbENj95yDYJNS3TvcaxPMshV7aizdv/hWYjGIKoANWKjhWNBsr2YEuYZKodQulB1b8l7ILOuDQep3afowQQ== - -universalify@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0" - integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg== - -universalify@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" - integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== - -untildify@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b" - integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw== - -url-parse@^1.5.3: - version "1.5.10" - resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1" - integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ== - dependencies: - querystringify "^2.1.1" - requires-port "^1.0.0" - -uuid@^10.0.0: - version "10.0.0" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-10.0.0.tgz#5a95aa454e6e002725c79055fd42aaba30ca6294" - integrity sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ== - -uuid@^8.3.2: - version "8.3.2" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" - integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== - -validator@^13.6.0: - version "13.12.0" - resolved "https://registry.yarnpkg.com/validator/-/validator-13.12.0.tgz#7d78e76ba85504da3fee4fd1922b385914d4b35f" - integrity sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg== - -verror@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" - integrity sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw== - dependencies: - assert-plus "^1.0.0" - core-util-is "1.0.2" - extsprintf "^1.2.0" - -wait-on@^7.2.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/wait-on/-/wait-on-7.2.0.tgz#d76b20ed3fc1e2bebc051fae5c1ff93be7892928" - integrity sha512-wCQcHkRazgjG5XoAq9jbTMLpNIjoSlZslrJ2+N9MxDsGEv1HnFoVjOCexL0ESva7Y9cu350j+DWADdk54s4AFQ== - dependencies: - axios "^1.6.1" - joi "^17.11.0" - lodash "^4.17.21" - minimist "^1.2.8" - rxjs "^7.8.1" - -which@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" - integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== - dependencies: - isexe "^2.0.0" - -wrap-ansi@^6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" - integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== - -xtend@^4.0.0: - version "4.0.2" - resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" - integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== - -y18n@^5.0.5: - version "5.0.8" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" - integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== - -yargs-parser@^21.1.1: - version "21.1.1" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" - integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== - -yargs@^17.2.1: - version "17.7.2" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" - 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" - -yauzl@^2.10.0: - version "2.10.0" - resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" - integrity sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g== - dependencies: - buffer-crc32 "~0.2.3" - fd-slicer "~1.1.0" diff --git a/go.mod b/go.mod index 830e8ebd68..eb4856d087 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,9 @@ toolchain go1.24.1 require ( cloud.google.com/go/profiler v0.4.2 cloud.google.com/go/storage v1.54.0 + connectrpc.com/connect v1.18.1 + connectrpc.com/grpcreflect v1.3.0 + dario.cat/mergo v1.0.2 github.com/BurntSushi/toml v1.5.0 github.com/DATA-DOG/go-sqlmock v1.5.2 github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.27.0 @@ -34,6 +37,7 @@ require ( github.com/go-webauthn/webauthn v0.10.2 github.com/goccy/go-json v0.10.5 github.com/golang/protobuf v1.5.4 + github.com/google/go-cmp v0.7.0 github.com/gorilla/csrf v1.7.2 github.com/gorilla/mux v1.8.1 github.com/gorilla/schema v1.4.1 @@ -64,6 +68,8 @@ require ( github.com/riverqueue/river v0.22.0 github.com/riverqueue/river/riverdriver v0.22.0 github.com/riverqueue/river/rivertype v0.22.0 + github.com/riverqueue/rivercontrib/otelriver v0.5.0 + github.com/robfig/cron/v3 v3.0.1 github.com/rs/cors v1.11.1 github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 github.com/shopspring/decimal v1.3.1 @@ -76,7 +82,7 @@ require ( github.com/twilio/twilio-go v1.26.1 github.com/zitadel/exifremove v0.1.0 github.com/zitadel/logging v0.6.2 - github.com/zitadel/oidc/v3 v3.39.1 + github.com/zitadel/oidc/v3 v3.42.0 github.com/zitadel/passwap v0.9.0 github.com/zitadel/saml v0.3.5 github.com/zitadel/schema v1.3.1 @@ -95,8 +101,8 @@ require ( golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 golang.org/x/net v0.40.0 golang.org/x/oauth2 v0.30.0 - golang.org/x/sync v0.15.0 - golang.org/x/text v0.26.0 + golang.org/x/sync v0.16.0 + golang.org/x/text v0.27.0 google.golang.org/api v0.233.0 google.golang.org/genproto/googleapis/api v0.0.0-20250512202823-5a2f75b736a9 google.golang.org/grpc v1.72.1 @@ -113,7 +119,7 @@ require ( github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 // indirect github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 // indirect - github.com/bmatcuk/doublestar/v4 v4.8.1 // indirect + github.com/bmatcuk/doublestar/v4 v4.9.0 // indirect github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 // indirect github.com/crewjam/httperr v0.2.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect @@ -145,7 +151,6 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/riverqueue/river/rivershared v0.22.0 // indirect - github.com/riverqueue/rivercontrib/otelriver v0.5.0 // indirect github.com/sagikazarmark/locafero v0.7.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect diff --git a/go.sum b/go.sum index 27bd104e44..48f90f954a 100644 --- a/go.sum +++ b/go.sum @@ -24,6 +24,12 @@ cloud.google.com/go/storage v1.54.0 h1:Du3XEyliAiftfyW0bwfdppm2MMLdpVAfiIg4T2nAI cloud.google.com/go/storage v1.54.0/go.mod h1:hIi9Boe8cHxTyaeqh7KMMwKg088VblFK46C2x/BWaZE= cloud.google.com/go/trace v1.11.3 h1:c+I4YFjxRQjvAhRmSsmjpASUKq88chOX854ied0K/pE= cloud.google.com/go/trace v1.11.3/go.mod h1:pt7zCYiDSQjC9Y2oqCsh9jF4GStB/hmjrYLsxRR27q8= +connectrpc.com/connect v1.18.1 h1:PAg7CjSAGvscaf6YZKUefjoih5Z/qYkyaTrBW8xvYPw= +connectrpc.com/connect v1.18.1/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8= +connectrpc.com/grpcreflect v1.3.0 h1:Y4V+ACf8/vOb1XOc251Qun7jMB75gCUNw6llvB9csXc= +connectrpc.com/grpcreflect v1.3.0/go.mod h1:nfloOtCS8VUQOQ1+GTdFzVg2CJo4ZGaat8JIovCtDYs= +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8= github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= @@ -98,8 +104,8 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38= -github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/bmatcuk/doublestar/v4 v4.9.0 h1:DBvuZxjdKkRP/dr4GVV4w2fnmrk5Hxc90T51LZjv0JA= +github.com/bmatcuk/doublestar/v4 v4.9.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/boombuler/barcode v1.0.2 h1:79yrbttoZrLGkL/oOI8hBrUKucwOL0oOjUgEguGMcJ4= github.com/boombuler/barcode v1.0.2/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= @@ -804,8 +810,10 @@ github.com/zitadel/exifremove v0.1.0 h1:qD50ezWsfeeqfcvs79QyyjVfK+snN12v0U0deaU8 github.com/zitadel/exifremove v0.1.0/go.mod h1:rzKJ3woL/Rz2KthVBiSBKIBptNTvgmk9PLaeUKTm+ek= github.com/zitadel/logging v0.6.2 h1:MW2kDDR0ieQynPZ0KIZPrh9ote2WkxfBif5QoARDQcU= github.com/zitadel/logging v0.6.2/go.mod h1:z6VWLWUkJpnNVDSLzrPSQSQyttysKZ6bCRongw0ROK4= -github.com/zitadel/oidc/v3 v3.39.1 h1:6QwGwI3yxh4somT7fwRCeT1KOn/HOGv0PA0dFciwJjE= -github.com/zitadel/oidc/v3 v3.39.1/go.mod h1:aH8brOrzoliAybVdfq2xIdGvbtl0j/VsKRNa7WE72gI= +github.com/zitadel/oidc/v3 v3.41.1-0.20250718152526-16ebef905b40 h1:MmUhfhwIcPStWqsTW+Pw+kYa5SNY7TxwzktUDohwO78= +github.com/zitadel/oidc/v3 v3.41.1-0.20250718152526-16ebef905b40/go.mod h1:Y/rY7mHTzMGrZgf7REgQZFWxySlaSVqqFdBmNZq+9wA= +github.com/zitadel/oidc/v3 v3.42.0 h1:cqlCYIEapmDprp5a5hUl9ivkUOu1SQxOqbrKdalHqGk= +github.com/zitadel/oidc/v3 v3.42.0/go.mod h1:Y/rY7mHTzMGrZgf7REgQZFWxySlaSVqqFdBmNZq+9wA= github.com/zitadel/passwap v0.9.0 h1:QvDK8OHKdb73C0m+mwXvu87UJSBqix3oFwTVENHdv80= github.com/zitadel/passwap v0.9.0/go.mod h1:6QzwFjDkIr3FfudzSogTOx5Ydhq4046dRJtDM/kX+G8= github.com/zitadel/saml v0.3.5 h1:L1RKWS5y66cGepVxUGjx/WSBOtrtSpRA/J3nn5BJLOY= @@ -942,8 +950,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/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.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= -golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.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-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -986,8 +994,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= -golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= +golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/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.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= diff --git a/internal/api/api.go b/internal/api/api.go index 62d3e14b35..18c554ca37 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -7,16 +7,18 @@ import ( "sort" "strings" + "connectrpc.com/grpcreflect" "github.com/gorilla/mux" "github.com/improbable-eng/grpc-web/go/grpcweb" "github.com/zitadel/logging" "google.golang.org/grpc" "google.golang.org/grpc/health" healthpb "google.golang.org/grpc/health/grpc_health_v1" - "google.golang.org/grpc/reflection" "github.com/zitadel/zitadel/internal/api/authz" + grpc_api "github.com/zitadel/zitadel/internal/api/grpc" "github.com/zitadel/zitadel/internal/api/grpc/server" + "github.com/zitadel/zitadel/internal/api/grpc/server/connect_middleware" http_util "github.com/zitadel/zitadel/internal/api/http" http_mw "github.com/zitadel/zitadel/internal/api/http/middleware" "github.com/zitadel/zitadel/internal/api/ui/login" @@ -24,10 +26,16 @@ import ( "github.com/zitadel/zitadel/internal/telemetry/metrics" "github.com/zitadel/zitadel/internal/telemetry/tracing" "github.com/zitadel/zitadel/internal/zerrors" + system_pb "github.com/zitadel/zitadel/pkg/grpc/system" +) + +var ( + metricTypes = []metrics.MetricType{metrics.MetricTypeTotalCount, metrics.MetricTypeRequestCount, metrics.MetricTypeStatusCode} ) type API struct { port uint16 + externalDomain string grpcServer *grpc.Server verifier authz.APITokenVerifier health healthCheck @@ -37,16 +45,23 @@ type API struct { healthServer *health.Server accessInterceptor *http_mw.AccessInterceptor queries *query.Queries + authConfig authz.Config + systemAuthZ authz.Config + connectServices map[string][]string } func (a *API) ListGrpcServices() []string { serviceInfo := a.grpcServer.GetServiceInfo() - services := make([]string, len(serviceInfo)) + services := make([]string, len(serviceInfo)+len(a.connectServices)) i := 0 for servicename := range serviceInfo { services[i] = servicename i++ } + for prefix := range a.connectServices { + services[i] = strings.Trim(prefix, "/") + i++ + } sort.Strings(services) return services } @@ -59,6 +74,11 @@ func (a *API) ListGrpcMethods() []string { methods = append(methods, "/"+servicename+"/"+method.Name) } } + for service, methodList := range a.connectServices { + for _, method := range methodList { + methods = append(methods, service+method) + } + } sort.Strings(methods) return methods } @@ -82,12 +102,16 @@ func New( ) (_ *API, err error) { api := &API{ port: port, + externalDomain: externalDomain, verifier: verifier, health: queries, router: router, queries: queries, accessInterceptor: accessInterceptor, hostHeaders: hostHeaders, + authConfig: authZ, + systemAuthZ: systemAuthz, + connectServices: make(map[string][]string), } api.grpcServer = server.CreateServer(api.verifier, systemAuthz, authZ, queries, externalDomain, tlsConfig, accessInterceptor.AccessService()) @@ -100,10 +124,15 @@ func New( api.RegisterHandlerOnPrefix("/debug", api.healthHandler()) api.router.Handle("/", http.RedirectHandler(login.HandlerPrefix, http.StatusFound)) - reflection.Register(api.grpcServer) return api, nil } +func (a *API) serverReflection() { + reflector := grpcreflect.NewStaticReflector(a.ListGrpcServices()...) + a.RegisterHandlerOnPrefix(grpcreflect.NewHandlerV1(reflector)) + a.RegisterHandlerOnPrefix(grpcreflect.NewHandlerV1Alpha(reflector)) +} + // RegisterServer registers a grpc service on the grpc server, // creates a new grpc gateway and registers it as a separate http handler // @@ -131,17 +160,50 @@ func (a *API) RegisterServer(ctx context.Context, grpcServer server.WithGatewayP // and its gateway on the gateway handler // // used for >= v2 api (e.g. user, session, ...) -func (a *API) RegisterService(ctx context.Context, grpcServer server.Server) error { - grpcServer.RegisterServer(a.grpcServer) - err := server.RegisterGateway(ctx, a.grpcGateway, grpcServer) - if err != nil { - return err +func (a *API) RegisterService(ctx context.Context, srv server.Server) error { + switch service := srv.(type) { + case server.GrpcServer: + service.RegisterServer(a.grpcServer) + case server.ConnectServer: + a.registerConnectServer(service) } - a.verifier.RegisterServer(grpcServer.AppName(), grpcServer.MethodPrefix(), grpcServer.AuthMethods()) - a.healthServer.SetServingStatus(grpcServer.MethodPrefix(), healthpb.HealthCheckResponse_SERVING) + if withGateway, ok := srv.(server.WithGateway); ok { + err := server.RegisterGateway(ctx, a.grpcGateway, withGateway) + if err != nil { + return err + } + } + a.verifier.RegisterServer(srv.AppName(), srv.MethodPrefix(), srv.AuthMethods()) + a.healthServer.SetServingStatus(srv.MethodPrefix(), healthpb.HealthCheckResponse_SERVING) return nil } +func (a *API) registerConnectServer(service server.ConnectServer) { + prefix, handler := service.RegisterConnectServer( + connect_middleware.CallDurationHandler(), + connect_middleware.MetricsHandler(metricTypes, grpc_api.Probes...), + connect_middleware.NoCacheInterceptor(), + connect_middleware.InstanceInterceptor(a.queries, a.externalDomain, system_pb.SystemService_ServiceDesc.ServiceName, healthpb.Health_ServiceDesc.ServiceName), + connect_middleware.AccessStorageInterceptor(a.accessInterceptor.AccessService()), + connect_middleware.ErrorHandler(), + connect_middleware.LimitsInterceptor(system_pb.SystemService_ServiceDesc.ServiceName), + connect_middleware.AuthorizationInterceptor(a.verifier, a.systemAuthZ, a.authConfig), + connect_middleware.TranslationHandler(), + connect_middleware.QuotaExhaustedInterceptor(a.accessInterceptor.AccessService(), system_pb.SystemService_ServiceDesc.ServiceName), + connect_middleware.ExecutionHandler(a.queries), + connect_middleware.ValidationHandler(), + connect_middleware.ServiceHandler(), + connect_middleware.ActivityInterceptor(), + ) + methods := service.FileDescriptor().Services().Get(0).Methods() + methodNames := make([]string, methods.Len()) + for i := 0; i < methods.Len(); i++ { + methodNames[i] = string(methods.Get(i).Name()) + } + a.connectServices[prefix] = methodNames + a.RegisterHandlerPrefixes(http_mw.CORSInterceptor(handler), prefix) +} + // HandleFunc allows registering a [http.HandlerFunc] on an exact // path, instead of prefix like RegisterHandlerOnPrefix. func (a *API) HandleFunc(path string, f http.HandlerFunc) { @@ -173,6 +235,9 @@ func (a *API) registerHealthServer() { } func (a *API) RouteGRPC() { + // since all services are now registered, we can build the grpc server reflection and register the handler + a.serverReflection() + http2Route := a.router. MatcherFunc(func(r *http.Request, _ *mux.RouteMatch) bool { return r.ProtoMajor == 2 diff --git a/internal/api/authz/authorization.go b/internal/api/authz/authorization.go index ea20a2438f..25130584a0 100644 --- a/internal/api/authz/authorization.go +++ b/internal/api/authz/authorization.go @@ -7,8 +7,6 @@ import ( "slices" "strings" - "github.com/zitadel/logging" - "github.com/zitadel/zitadel/internal/telemetry/tracing" "github.com/zitadel/zitadel/internal/zerrors" ) @@ -26,14 +24,13 @@ func CheckUserAuthorization(ctx context.Context, req interface{}, token, orgID, ctx, span := tracing.NewServerInterceptorSpan(ctx) defer func() { span.EndWithError(err) }() - ctxData, err := VerifyTokenAndCreateCtxData(ctx, token, orgID, orgDomain, verifier) + ctxData, err := VerifyTokenAndCreateCtxData(ctx, token, orgID, orgDomain, verifier, systemRolePermissionMapping) if err != nil { return nil, err } if requiredAuthOption.Permission == authenticated { return func(parent context.Context) context.Context { - parent = addGetSystemUserRolesToCtx(parent, systemRolePermissionMapping, ctxData) return context.WithValue(parent, dataKey, ctxData) }, nil } @@ -54,7 +51,6 @@ func CheckUserAuthorization(ctx context.Context, req interface{}, token, orgID, parent = context.WithValue(parent, dataKey, ctxData) parent = context.WithValue(parent, allPermissionsKey, allPermissions) parent = context.WithValue(parent, requestPermissionsKey, requestedPermissions) - parent = addGetSystemUserRolesToCtx(parent, systemRolePermissionMapping, ctxData) return parent }, nil } @@ -131,42 +127,32 @@ func GetAllPermissionCtxIDs(perms []string) []string { return ctxIDs } -type SystemUserPermissionsDBQuery struct { - MemberType string `json:"member_type"` - AggregateID string `json:"aggregate_id"` - ObjectID string `json:"object_id"` - Permissions []string `json:"permissions"` +type SystemUserPermissions struct { + MemberType MemberType `json:"member_type"` + AggregateID string `json:"aggregate_id"` + ObjectID string `json:"object_id"` + Permissions []string `json:"permissions"` } -func addGetSystemUserRolesToCtx(ctx context.Context, systemUserRoleMap []RoleMapping, ctxData CtxData) context.Context { - if len(ctxData.SystemMemberships) == 0 { - return ctx +// systemMembershipsToUserPermissions converts system memberships based on roles, +// to SystemUserPermissions, using the passed role mapping. +func systemMembershipsToUserPermissions(memberships Memberships, roleMap []RoleMapping) []SystemUserPermissions { + if memberships == nil { + return nil } - systemUserPermissions := make([]SystemUserPermissionsDBQuery, len(ctxData.SystemMemberships)) - for i, systemPerm := range ctxData.SystemMemberships { + systemUserPermissions := make([]SystemUserPermissions, len(memberships)) + for i, systemPerm := range memberships { permissions := make([]string, 0, len(systemPerm.Roles)) for _, role := range systemPerm.Roles { - permissions = append(permissions, getPermissionsFromRole(systemUserRoleMap, role)...) + permissions = append(permissions, getPermissionsFromRole(roleMap, role)...) } slices.Sort(permissions) - permissions = slices.Compact(permissions) + permissions = slices.Compact(permissions) // remove duplicates - systemUserPermissions[i].MemberType = systemPerm.MemberType.String() + systemUserPermissions[i].MemberType = systemPerm.MemberType systemUserPermissions[i].AggregateID = systemPerm.AggregateID + systemUserPermissions[i].ObjectID = systemPerm.ObjectID systemUserPermissions[i].Permissions = permissions } - return context.WithValue(ctx, systemUserRolesKey, systemUserPermissions) -} - -func GetSystemUserPermissions(ctx context.Context) []SystemUserPermissionsDBQuery { - getSystemUserRolesFuncValue := ctx.Value(systemUserRolesKey) - if getSystemUserRolesFuncValue == nil { - return nil - } - systemUserRoles, ok := getSystemUserRolesFuncValue.([]SystemUserPermissionsDBQuery) - if !ok { - logging.WithFields("Authz").Error("unable to cast []SystemUserPermissionsDBQuery") - return nil - } - return systemUserRoles + return systemUserPermissions } diff --git a/internal/api/authz/authorization_test.go b/internal/api/authz/authorization_test.go index 4b81c73d81..af49dcc5c6 100644 --- a/internal/api/authz/authorization_test.go +++ b/internal/api/authz/authorization_test.go @@ -3,6 +3,8 @@ package authz import ( "testing" + "github.com/stretchr/testify/assert" + "github.com/zitadel/zitadel/internal/zerrors" ) @@ -276,3 +278,127 @@ func Test_GetPermissionCtxIDs(t *testing.T) { }) } } + +func Test_systemMembershipsToUserPermissions(t *testing.T) { + roleMap := []RoleMapping{ + { + Role: "FOO_BAR", + Permissions: []string{"foo.bar.read", "foo.bar.write"}, + }, + { + Role: "BAR_FOO", + Permissions: []string{"bar.foo.read", "bar.foo.write", "foo.bar.read"}, + }, + } + + type args struct { + memberships Memberships + roleMap []RoleMapping + } + tests := []struct { + name string + args args + want []SystemUserPermissions + }{ + { + name: "nil memberships", + args: args{ + memberships: nil, + roleMap: roleMap, + }, + want: nil, + }, + { + name: "empty memberships", + args: args{ + memberships: Memberships{}, + roleMap: roleMap, + }, + want: []SystemUserPermissions{}, + }, + { + name: "single membership", + args: args{ + memberships: Memberships{ + { + MemberType: MemberTypeSystem, + AggregateID: "1", + ObjectID: "2", + Roles: []string{"FOO_BAR"}, + }, + }, + roleMap: roleMap, + }, + want: []SystemUserPermissions{ + { + MemberType: MemberTypeSystem, + AggregateID: "1", + ObjectID: "2", + Permissions: []string{"foo.bar.read", "foo.bar.write"}, + }, + }, + }, + { + name: "multiple memberships", + args: args{ + memberships: Memberships{ + { + MemberType: MemberTypeSystem, + AggregateID: "1", + ObjectID: "2", + Roles: []string{"FOO_BAR"}, + }, + { + MemberType: MemberTypeIAM, + AggregateID: "1", + ObjectID: "2", + Roles: []string{"BAR_FOO"}, + }, + }, + roleMap: roleMap, + }, + want: []SystemUserPermissions{ + { + MemberType: MemberTypeSystem, + AggregateID: "1", + ObjectID: "2", + Permissions: []string{"foo.bar.read", "foo.bar.write"}, + }, + { + MemberType: MemberTypeIAM, + AggregateID: "1", + ObjectID: "2", + Permissions: []string{"bar.foo.read", "bar.foo.write", "foo.bar.read"}, + }, + }, + }, + { + name: "multiple roles", + args: args{ + memberships: Memberships{ + { + MemberType: MemberTypeSystem, + AggregateID: "1", + ObjectID: "2", + Roles: []string{"FOO_BAR", "BAR_FOO"}, + }, + }, + roleMap: roleMap, + }, + want: []SystemUserPermissions{ + { + MemberType: MemberTypeSystem, + AggregateID: "1", + ObjectID: "2", + Permissions: []string{"bar.foo.read", "bar.foo.write", "foo.bar.read", "foo.bar.write"}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := systemMembershipsToUserPermissions(tt.args.memberships, tt.args.roleMap) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/internal/api/authz/context.go b/internal/api/authz/context.go index d6528cd017..ff2fa8d445 100644 --- a/internal/api/authz/context.go +++ b/internal/api/authz/context.go @@ -1,4 +1,4 @@ -//go:generate enumer -type MemberType -trimprefix MemberType +//go:generate enumer -type MemberType -trimprefix MemberType -json -sql package authz @@ -22,17 +22,17 @@ const ( dataKey key = 2 allPermissionsKey key = 3 instanceKey key = 4 - systemUserRolesKey key = 5 ) type CtxData struct { - UserID string - OrgID string - ProjectID string - AgentID string - PreferredLanguage string - ResourceOwner string - SystemMemberships Memberships + UserID string + OrgID string + ProjectID string + AgentID string + PreferredLanguage string + ResourceOwner string + SystemMemberships Memberships + SystemUserPermissions []SystemUserPermissions } func (ctxData CtxData) IsZero() bool { @@ -98,7 +98,7 @@ func (s SystemTokenVerifierFunc) VerifySystemToken(ctx context.Context, token st return s(ctx, token, orgID) } -func VerifyTokenAndCreateCtxData(ctx context.Context, token, orgID, orgDomain string, t APITokenVerifier) (_ CtxData, err error) { +func VerifyTokenAndCreateCtxData(ctx context.Context, token, orgID, orgDomain string, t APITokenVerifier, systemRoleMap []RoleMapping) (_ CtxData, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() tokenWOBearer, err := extractBearerToken(token) @@ -133,13 +133,14 @@ func VerifyTokenAndCreateCtxData(ctx context.Context, token, orgID, orgDomain st } } return CtxData{ - UserID: userID, - OrgID: orgID, - ProjectID: projectID, - AgentID: agentID, - PreferredLanguage: prefLang, - ResourceOwner: resourceOwner, - SystemMemberships: sysMemberships, + UserID: userID, + OrgID: orgID, + ProjectID: projectID, + AgentID: agentID, + PreferredLanguage: prefLang, + ResourceOwner: resourceOwner, + SystemMemberships: sysMemberships, + SystemUserPermissions: systemMembershipsToUserPermissions(sysMemberships, systemRoleMap), }, nil } diff --git a/internal/api/authz/context_mock.go b/internal/api/authz/context_mock.go index 6891030bd3..d26b371bc6 100644 --- a/internal/api/authz/context_mock.go +++ b/internal/api/authz/context_mock.go @@ -1,10 +1,28 @@ package authz -import "context" +import ( + "context" -func NewMockContext(instanceID, orgID, userID string) context.Context { + "golang.org/x/text/language" +) + +type MockContextInstanceOpts func(i *instance) + +func WithMockDefaultLanguage(lang language.Tag) MockContextInstanceOpts { + return func(i *instance) { + i.defaultLanguage = lang + } +} + +func NewMockContext(instanceID, orgID, userID string, opts ...MockContextInstanceOpts) context.Context { ctx := context.WithValue(context.Background(), dataKey, CtxData{UserID: userID, OrgID: orgID}) - return context.WithValue(ctx, instanceKey, &instance{id: instanceID}) + + i := &instance{id: instanceID} + for _, o := range opts { + o(i) + } + + return context.WithValue(ctx, instanceKey, i) } func NewMockContextWithAgent(instanceID, orgID, userID, agentID string) context.Context { diff --git a/internal/api/authz/membertype_enumer.go b/internal/api/authz/membertype_enumer.go index 5de4c92292..9354194660 100644 --- a/internal/api/authz/membertype_enumer.go +++ b/internal/api/authz/membertype_enumer.go @@ -1,8 +1,10 @@ -// Code generated by "enumer -type MemberType -trimprefix MemberType"; DO NOT EDIT. +// Code generated by "enumer -type MemberType -trimprefix MemberType -json -sql"; DO NOT EDIT. package authz import ( + "database/sql/driver" + "encoding/json" "fmt" "strings" ) @@ -92,3 +94,50 @@ func (i MemberType) IsAMemberType() bool { } return false } + +// MarshalJSON implements the json.Marshaler interface for MemberType +func (i MemberType) MarshalJSON() ([]byte, error) { + return json.Marshal(i.String()) +} + +// UnmarshalJSON implements the json.Unmarshaler interface for MemberType +func (i *MemberType) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return fmt.Errorf("MemberType should be a string, got %s", data) + } + + var err error + *i, err = MemberTypeString(s) + return err +} + +func (i MemberType) Value() (driver.Value, error) { + return i.String(), nil +} + +func (i *MemberType) Scan(value interface{}) error { + if value == nil { + return nil + } + + var str string + switch v := value.(type) { + case []byte: + str = string(v) + case string: + str = v + case fmt.Stringer: + str = v.String() + default: + return fmt.Errorf("invalid value of MemberType: %[1]T(%[1]v)", value) + } + + val, err := MemberTypeString(str) + if err != nil { + return err + } + + *i = val + return nil +} diff --git a/internal/api/grpc/action/v2/execution.go b/internal/api/grpc/action/v2/execution.go new file mode 100644 index 0000000000..6941f62ace --- /dev/null +++ b/internal/api/grpc/action/v2/execution.go @@ -0,0 +1,92 @@ +package action + +import ( + "context" + + "connectrpc.com/connect" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/repository/execution" + "github.com/zitadel/zitadel/internal/zerrors" + "github.com/zitadel/zitadel/pkg/grpc/action/v2" +) + +func (s *Server) SetExecution(ctx context.Context, req *connect.Request[action.SetExecutionRequest]) (*connect.Response[action.SetExecutionResponse], error) { + reqTargets := req.Msg.GetTargets() + targets := make([]*execution.Target, len(reqTargets)) + for i, target := range reqTargets { + targets[i] = &execution.Target{Type: domain.ExecutionTargetTypeTarget, Target: target} + } + set := &command.SetExecution{ + Targets: targets, + } + var err error + var details *domain.ObjectDetails + instanceID := authz.GetInstance(ctx).InstanceID() + switch t := req.Msg.GetCondition().GetConditionType().(type) { + case *action.Condition_Request: + cond := executionConditionFromRequest(t.Request) + details, err = s.command.SetExecutionRequest(ctx, cond, set, instanceID) + case *action.Condition_Response: + cond := executionConditionFromResponse(t.Response) + details, err = s.command.SetExecutionResponse(ctx, cond, set, instanceID) + case *action.Condition_Event: + cond := executionConditionFromEvent(t.Event) + details, err = s.command.SetExecutionEvent(ctx, cond, set, instanceID) + case *action.Condition_Function: + details, err = s.command.SetExecutionFunction(ctx, command.ExecutionFunctionCondition(t.Function.GetName()), set, instanceID) + default: + err = zerrors.ThrowInvalidArgument(nil, "ACTION-5r5Ju", "Errors.Execution.ConditionInvalid") + } + if err != nil { + return nil, err + } + return connect.NewResponse(&action.SetExecutionResponse{ + SetDate: timestamppb.New(details.EventDate), + }), nil +} + +func (s *Server) ListExecutionFunctions(ctx context.Context, _ *connect.Request[action.ListExecutionFunctionsRequest]) (*connect.Response[action.ListExecutionFunctionsResponse], error) { + return connect.NewResponse(&action.ListExecutionFunctionsResponse{ + Functions: s.ListActionFunctions(), + }), nil +} + +func (s *Server) ListExecutionMethods(ctx context.Context, _ *connect.Request[action.ListExecutionMethodsRequest]) (*connect.Response[action.ListExecutionMethodsResponse], error) { + return connect.NewResponse(&action.ListExecutionMethodsResponse{ + Methods: s.ListGRPCMethods(), + }), nil +} + +func (s *Server) ListExecutionServices(ctx context.Context, _ *connect.Request[action.ListExecutionServicesRequest]) (*connect.Response[action.ListExecutionServicesResponse], error) { + return connect.NewResponse(&action.ListExecutionServicesResponse{ + Services: s.ListGRPCServices(), + }), nil +} + +func executionConditionFromRequest(request *action.RequestExecution) *command.ExecutionAPICondition { + return &command.ExecutionAPICondition{ + Method: request.GetMethod(), + Service: request.GetService(), + All: request.GetAll(), + } +} + +func executionConditionFromResponse(response *action.ResponseExecution) *command.ExecutionAPICondition { + return &command.ExecutionAPICondition{ + Method: response.GetMethod(), + Service: response.GetService(), + All: response.GetAll(), + } +} + +func executionConditionFromEvent(event *action.EventExecution) *command.ExecutionEventCondition { + return &command.ExecutionEventCondition{ + Event: event.GetEvent(), + Group: event.GetGroup(), + All: event.GetAll(), + } +} diff --git a/internal/api/grpc/action/v2/integration_test/execution_target_test.go b/internal/api/grpc/action/v2/integration_test/execution_target_test.go new file mode 100644 index 0000000000..f5bd7c50ec --- /dev/null +++ b/internal/api/grpc/action/v2/integration_test/execution_target_test.go @@ -0,0 +1,1310 @@ +//go:build integration + +package action_test + +import ( + "context" + "encoding/base64" + "net/http" + "net/url" + "strings" + "testing" + "time" + + "github.com/brianvoe/gofakeit/v6" + "github.com/crewjam/saml" + "github.com/crewjam/saml/samlsp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/zitadel/oidc/v3/pkg/client/rp" + "github.com/zitadel/oidc/v3/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/op" + "golang.org/x/text/language" + "google.golang.org/protobuf/types/known/durationpb" + + "github.com/zitadel/zitadel/internal/api/grpc/server/middleware" + oidc_api "github.com/zitadel/zitadel/internal/api/oidc" + saml_api "github.com/zitadel/zitadel/internal/api/saml" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/integration" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/pkg/grpc/action/v2" + "github.com/zitadel/zitadel/pkg/grpc/app" + "github.com/zitadel/zitadel/pkg/grpc/management" + "github.com/zitadel/zitadel/pkg/grpc/metadata" + oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2" + saml_pb "github.com/zitadel/zitadel/pkg/grpc/saml/v2" + "github.com/zitadel/zitadel/pkg/grpc/session/v2" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +const ( + redirectURIImplicit = "http://localhost:9999/callback" +) + +var ( + loginV2 = &app.LoginVersion{Version: &app.LoginVersion_LoginV2{LoginV2: &app.LoginV2{BaseUri: nil}}} +) + +func TestServer_ExecutionTarget(t *testing.T) { + instance := integration.NewInstance(CTX) + isolatedIAMOwnerCTX := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + fullMethod := action.ActionService_GetTarget_FullMethodName + + tests := []struct { + name string + ctx context.Context + dep func(context.Context, *action.GetTargetRequest, *action.GetTargetResponse) (closeF func(), calledF func() bool) + clean func(context.Context) + req *action.GetTargetRequest + want *action.GetTargetResponse + wantErr bool + }{ + { + name: "GetTarget, request and response, ok", + ctx: isolatedIAMOwnerCTX, + dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) (func(), func() bool) { + + orgID := instance.DefaultOrg.Id + projectID := "" + userID := instance.Users.Get(integration.UserTypeIAMOwner).ID + + // create target for target changes + targetCreatedName := gofakeit.Name() + targetCreatedURL := "https://nonexistent" + + targetCreated := instance.CreateTarget(ctx, t, targetCreatedName, targetCreatedURL, domain.TargetTypeCall, false) + + // request received by target + wantRequest := &middleware.ContextInfoRequest{FullMethod: fullMethod, InstanceID: instance.ID(), OrgID: orgID, ProjectID: projectID, UserID: userID, Request: middleware.Message{Message: request}} + changedRequest := &action.GetTargetRequest{Id: targetCreated.GetId()} + // replace original request with different targetID + urlRequest, closeRequest, calledRequest, _ := integration.TestServerCallProto(wantRequest, 0, http.StatusOK, changedRequest) + + targetRequest := waitForTarget(ctx, t, instance, urlRequest, domain.TargetTypeCall, false) + + waitForExecutionOnCondition(ctx, t, instance, conditionRequestFullMethod(fullMethod), []string{targetRequest.GetId()}) + + // expected response from the GetTarget + expectedResponse := &action.GetTargetResponse{ + Target: &action.Target{ + Id: targetCreated.GetId(), + CreationDate: targetCreated.GetCreationDate(), + ChangeDate: targetCreated.GetCreationDate(), + Name: targetCreatedName, + TargetType: &action.Target_RestCall{ + RestCall: &action.RESTCall{ + InterruptOnError: false, + }, + }, + Timeout: durationpb.New(5 * time.Second), + Endpoint: targetCreatedURL, + SigningKey: targetCreated.GetSigningKey(), + }, + } + + changedResponse := &action.GetTargetResponse{ + Target: &action.Target{ + Id: "changed", + CreationDate: targetCreated.GetCreationDate(), + ChangeDate: targetCreated.GetCreationDate(), + Name: targetCreatedName, + TargetType: &action.Target_RestCall{ + RestCall: &action.RESTCall{ + InterruptOnError: false, + }, + }, + Timeout: durationpb.New(5 * time.Second), + Endpoint: targetCreatedURL, + SigningKey: targetCreated.GetSigningKey(), + }, + } + // content for update + response.Target = &action.Target{ + Id: "changed", + CreationDate: targetCreated.GetCreationDate(), + ChangeDate: targetCreated.GetCreationDate(), + Name: targetCreatedName, + TargetType: &action.Target_RestCall{ + RestCall: &action.RESTCall{ + InterruptOnError: false, + }, + }, + Timeout: durationpb.New(5 * time.Second), + Endpoint: targetCreatedURL, + SigningKey: targetCreated.GetSigningKey(), + } + + // response received by target + wantResponse := &middleware.ContextInfoResponse{ + FullMethod: fullMethod, + InstanceID: instance.ID(), + OrgID: orgID, + ProjectID: projectID, + UserID: userID, + Request: middleware.Message{Message: changedRequest}, + Response: middleware.Message{Message: expectedResponse}, + } + // after request with different targetID, return changed response + targetResponseURL, closeResponse, calledResponse, _ := integration.TestServerCallProto(wantResponse, 0, http.StatusOK, changedResponse) + + targetResponse := waitForTarget(ctx, t, instance, targetResponseURL, domain.TargetTypeCall, false) + waitForExecutionOnCondition(ctx, t, instance, conditionResponseFullMethod(fullMethod), []string{targetResponse.GetId()}) + return func() { + closeRequest() + closeResponse() + }, func() bool { + if calledRequest() != 1 { + return false + } + if calledResponse() != 1 { + return false + } + return true + } + }, + clean: func(ctx context.Context) { + instance.DeleteExecution(ctx, t, conditionRequestFullMethod(fullMethod)) + instance.DeleteExecution(ctx, t, conditionResponseFullMethod(fullMethod)) + }, + req: &action.GetTargetRequest{ + Id: "something", + }, + want: &action.GetTargetResponse{ + // defined in the dependency function + }, + }, + { + name: "GetTarget, request, interrupt", + ctx: isolatedIAMOwnerCTX, + dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) (func(), func() bool) { + orgID := instance.DefaultOrg.Id + projectID := "" + userID := instance.Users.Get(integration.UserTypeIAMOwner).ID + + // request received by target + wantRequest := &middleware.ContextInfoRequest{FullMethod: fullMethod, InstanceID: instance.ID(), OrgID: orgID, ProjectID: projectID, UserID: userID, Request: middleware.Message{Message: request}} + urlRequest, closeRequest, calledRequest, _ := integration.TestServerCallProto(wantRequest, 0, http.StatusInternalServerError, nil) + + targetRequest := waitForTarget(ctx, t, instance, urlRequest, domain.TargetTypeCall, true) + waitForExecutionOnCondition(ctx, t, instance, conditionRequestFullMethod(fullMethod), []string{targetRequest.GetId()}) + // GetTarget with used target + request.Id = targetRequest.GetId() + return func() { + closeRequest() + }, func() bool { + return calledRequest() == 1 + } + }, + clean: func(ctx context.Context) { + instance.DeleteExecution(ctx, t, conditionRequestFullMethod(fullMethod)) + }, + req: &action.GetTargetRequest{}, + wantErr: true, + }, + { + name: "GetTarget, response, interrupt", + ctx: isolatedIAMOwnerCTX, + dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) (func(), func() bool) { + orgID := instance.DefaultOrg.Id + projectID := "" + userID := instance.Users.Get(integration.UserTypeIAMOwner).ID + + // create target for target changes + targetCreatedName := gofakeit.Name() + targetCreatedURL := "https://nonexistent" + + targetCreated := instance.CreateTarget(ctx, t, targetCreatedName, targetCreatedURL, domain.TargetTypeCall, false) + + // GetTarget with used target + request.Id = targetCreated.GetId() + + // expected response from the GetTarget + expectedResponse := &action.GetTargetResponse{ + Target: &action.Target{ + Id: targetCreated.GetId(), + CreationDate: targetCreated.GetCreationDate(), + ChangeDate: targetCreated.GetCreationDate(), + Name: targetCreatedName, + TargetType: &action.Target_RestCall{ + RestCall: &action.RESTCall{ + InterruptOnError: false, + }, + }, + Timeout: durationpb.New(5 * time.Second), + Endpoint: targetCreatedURL, + SigningKey: targetCreated.GetSigningKey(), + }, + } + + // response received by target + wantResponse := &middleware.ContextInfoResponse{ + FullMethod: fullMethod, + InstanceID: instance.ID(), + OrgID: orgID, + ProjectID: projectID, + UserID: userID, + Request: middleware.Message{Message: request}, + Response: middleware.Message{Message: expectedResponse}, + } + // after request with different targetID, return changed response + targetResponseURL, closeResponse, calledResponse, _ := integration.TestServerCallProto(wantResponse, 0, http.StatusInternalServerError, nil) + + targetResponse := waitForTarget(ctx, t, instance, targetResponseURL, domain.TargetTypeCall, true) + waitForExecutionOnCondition(ctx, t, instance, conditionResponseFullMethod(fullMethod), []string{targetResponse.GetId()}) + return func() { + closeResponse() + }, func() bool { + return calledResponse() == 1 + } + }, + clean: func(ctx context.Context) { + instance.DeleteExecution(ctx, t, conditionResponseFullMethod(fullMethod)) + }, + req: &action.GetTargetRequest{}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + closeF, calledF := tt.dep(tt.ctx, tt.req, tt.want) + defer closeF() + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tt.ctx, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, err := instance.Client.ActionV2.GetTarget(tt.ctx, tt.req) + if tt.wantErr { + require.Error(ttt, err) + return + } + require.NoError(ttt, err) + assert.EqualExportedValues(ttt, tt.want.GetTarget(), got.GetTarget()) + + }, retryDuration, tick, "timeout waiting for expected execution result") + + if tt.clean != nil { + tt.clean(tt.ctx) + } + require.True(t, calledF()) + }) + } +} + +func TestServer_ExecutionTarget_Event(t *testing.T) { + instance := integration.NewInstance(CTX) + isolatedIAMOwnerCTX := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + + event := "session.added" + urlRequest, closeF, calledF, resetF := integration.TestServerCall(nil, 0, http.StatusOK, nil) + defer closeF() + + targetResponse := waitForTarget(isolatedIAMOwnerCTX, t, instance, urlRequest, domain.TargetTypeWebhook, true) + waitForExecutionOnCondition(isolatedIAMOwnerCTX, t, instance, conditionEvent(event), []string{targetResponse.GetId()}) + + tests := []struct { + name string + ctx context.Context + eventCount int + expectedCalls int + clean func(context.Context) + wantErr bool + }{ + { + name: "event, 1 session.added, ok", + ctx: isolatedIAMOwnerCTX, + eventCount: 1, + expectedCalls: 1, + }, + { + name: "event, 5 session.added, ok", + ctx: isolatedIAMOwnerCTX, + eventCount: 5, + expectedCalls: 5, + }, + { + name: "event, 50 session.added, ok", + ctx: isolatedIAMOwnerCTX, + eventCount: 50, + expectedCalls: 50, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // reset the count of the target + resetF() + + for i := 0; i < tt.eventCount; i++ { + _, err := instance.Client.SessionV2.CreateSession(tt.ctx, &session.CreateSessionRequest{}) + require.NoError(t, err) + } + + // wait for called target + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tt.ctx, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + assert.True(ttt, calledF() == tt.expectedCalls) + }, retryDuration, tick, "timeout waiting for expected execution result") + }) + } +} + +func TestServer_ExecutionTarget_Event_LongerThanTargetTimeout(t *testing.T) { + instance := integration.NewInstance(CTX) + isolatedIAMOwnerCTX := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + + event := "session.added" + // call takes longer than timeout of target + urlRequest, closeF, calledF, resetF := integration.TestServerCall(nil, 5*time.Second, http.StatusOK, nil) + defer closeF() + + targetResponse := waitForTarget(isolatedIAMOwnerCTX, t, instance, urlRequest, domain.TargetTypeWebhook, true) + waitForExecutionOnCondition(isolatedIAMOwnerCTX, t, instance, conditionEvent(event), []string{targetResponse.GetId()}) + + tests := []struct { + name string + ctx context.Context + eventCount int + expectedCalls int + clean func(context.Context) + wantErr bool + }{ + { + name: "event, 1 session.added, error logs", + ctx: isolatedIAMOwnerCTX, + eventCount: 1, + expectedCalls: 1, + }, + { + name: "event, 5 session.added, error logs", + ctx: isolatedIAMOwnerCTX, + eventCount: 5, + expectedCalls: 5, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // reset the count of the target + resetF() + + for i := 0; i < tt.eventCount; i++ { + _, err := instance.Client.SessionV2.CreateSession(tt.ctx, &session.CreateSessionRequest{}) + require.NoError(t, err) + } + + // wait for called target + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tt.ctx, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + assert.True(ttt, calledF() == tt.expectedCalls) + }, retryDuration, tick, "timeout waiting for expected execution result") + }) + } +} + +func TestServer_ExecutionTarget_Event_LongerThanTransactionTimeout(t *testing.T) { + instance := integration.NewInstance(CTX) + isolatedIAMOwnerCTX := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + + event := "session.added" + urlRequest, closeF, calledF, resetF := integration.TestServerCall(nil, 1*time.Second, http.StatusOK, nil) + defer closeF() + + targetResponse := waitForTarget(isolatedIAMOwnerCTX, t, instance, urlRequest, domain.TargetTypeWebhook, true) + waitForExecutionOnCondition(isolatedIAMOwnerCTX, t, instance, conditionEvent(event), []string{targetResponse.GetId()}) + + tests := []struct { + name string + ctx context.Context + eventCount int + expectedCalls int + clean func(context.Context) + wantErr bool + }{ + { + name: "event, 1 session.added, ok", + ctx: isolatedIAMOwnerCTX, + eventCount: 1, + expectedCalls: 1, + }, + { + name: "event, 5 session.added, ok", + ctx: isolatedIAMOwnerCTX, + eventCount: 5, + expectedCalls: 5, + }, + { + name: "event, 5 session.added, ok", + ctx: isolatedIAMOwnerCTX, + eventCount: 5, + expectedCalls: 5, + }, + { + name: "event, 20 session.added, ok", + ctx: isolatedIAMOwnerCTX, + eventCount: 20, + expectedCalls: 20, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // reset the count of the target + resetF() + + for i := 0; i < tt.eventCount; i++ { + _, err := instance.Client.SessionV2.CreateSession(tt.ctx, &session.CreateSessionRequest{}) + require.NoError(t, err) + } + + // wait for called target + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tt.ctx, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + assert.True(ttt, calledF() == tt.expectedCalls) + }, retryDuration, tick, "timeout waiting for expected execution result") + }) + } +} + +func waitForExecutionOnCondition(ctx context.Context, t *testing.T, instance *integration.Instance, condition *action.Condition, targets []string) { + instance.SetExecution(ctx, t, condition, targets) + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, err := instance.Client.ActionV2.ListExecutions(ctx, &action.ListExecutionsRequest{ + Filters: []*action.ExecutionSearchFilter{ + {Filter: &action.ExecutionSearchFilter_InConditionsFilter{ + InConditionsFilter: &action.InConditionsFilter{Conditions: []*action.Condition{condition}}, + }}, + }, + }) + if !assert.NoError(ttt, err) { + return + } + if !assert.Len(ttt, got.GetExecutions(), 1) { + return + } + gotTargets := got.GetExecutions()[0].GetTargets() + // always first check length, otherwise its failed anyway + if assert.Len(ttt, gotTargets, len(targets)) { + for i := range targets { + assert.EqualExportedValues(ttt, targets[i], gotTargets[i]) + } + } + }, retryDuration, tick, "timeout waiting for expected execution result") +} + +func waitForTarget(ctx context.Context, t *testing.T, instance *integration.Instance, endpoint string, ty domain.TargetType, interrupt bool) *action.CreateTargetResponse { + resp := instance.CreateTarget(ctx, t, "", endpoint, ty, interrupt) + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, err := instance.Client.ActionV2.ListTargets(ctx, &action.ListTargetsRequest{ + Filters: []*action.TargetSearchFilter{ + {Filter: &action.TargetSearchFilter_InTargetIdsFilter{ + InTargetIdsFilter: &action.InTargetIDsFilter{TargetIds: []string{resp.GetId()}}, + }}, + }, + }) + if !assert.NoError(ttt, err) { + return + } + if !assert.Len(ttt, got.GetTargets(), 1) { + return + } + config := got.GetTargets()[0] + assert.Equal(ttt, config.GetEndpoint(), endpoint) + switch ty { + case domain.TargetTypeWebhook: + if !assert.NotNil(ttt, config.GetRestWebhook()) { + return + } + assert.Equal(ttt, interrupt, config.GetRestWebhook().GetInterruptOnError()) + case domain.TargetTypeAsync: + assert.NotNil(ttt, config.GetRestAsync()) + case domain.TargetTypeCall: + if !assert.NotNil(ttt, config.GetRestCall()) { + return + } + assert.Equal(ttt, interrupt, config.GetRestCall().GetInterruptOnError()) + } + }, retryDuration, tick, "timeout waiting for expected execution result") + return resp +} + +func conditionRequestFullMethod(fullMethod string) *action.Condition { + return &action.Condition{ + ConditionType: &action.Condition_Request{ + Request: &action.RequestExecution{ + Condition: &action.RequestExecution_Method{ + Method: fullMethod, + }, + }, + }, + } +} + +func conditionResponseFullMethod(fullMethod string) *action.Condition { + return &action.Condition{ + ConditionType: &action.Condition_Response{ + Response: &action.ResponseExecution{ + Condition: &action.ResponseExecution_Method{ + Method: fullMethod, + }, + }, + }, + } +} + +func conditionEvent(event string) *action.Condition { + return &action.Condition{ + ConditionType: &action.Condition_Event{ + Event: &action.EventExecution{ + Condition: &action.EventExecution_Event{ + Event: event, + }, + }, + }, + } +} + +func conditionFunction(function string) *action.Condition { + return &action.Condition{ + ConditionType: &action.Condition_Function{ + Function: &action.FunctionExecution{ + Name: function, + }, + }, + } +} + +func TestServer_ExecutionTargetPreUserinfo(t *testing.T) { + instance := integration.NewInstance(CTX) + isolatedIAMCtx := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + ctxLoginClient := instance.WithAuthorizationToken(CTX, integration.UserTypeLogin) + + client, err := instance.CreateOIDCImplicitFlowClient(isolatedIAMCtx, t, redirectURIImplicit, loginV2) + require.NoError(t, err) + + type want struct { + addedClaims map[string]any + addedLogClaims map[string][]string + setUserMetadata []*metadata.Metadata + } + tests := []struct { + name string + ctx context.Context + dep func(ctx context.Context, t *testing.T, req *oidc_pb.CreateCallbackRequest) (string, func()) + req *oidc_pb.CreateCallbackRequest + want want + wantErr bool + }{ + { + name: "append claim", + ctx: ctxLoginClient, + dep: func(ctx context.Context, t *testing.T, req *oidc_pb.CreateCallbackRequest) (string, func()) { + response := &oidc_api.ContextInfoResponse{ + AppendClaims: []*oidc_api.AppendClaim{ + {Key: "added", Value: "value"}, + }, + } + return expectPreUserinfoExecution(ctx, t, instance, client.GetClientId(), req, response) + }, + req: &oidc_pb.CreateCallbackRequest{ + AuthRequestId: func() string { + authRequestID, err := instance.CreateOIDCAuthRequestImplicitWithoutLoginClientHeader(isolatedIAMCtx, client.GetClientId(), redirectURIImplicit) + require.NoError(t, err) + return authRequestID + }(), + }, + want: want{ + addedClaims: map[string]any{ + "added": "value", + }, + }, + wantErr: false, + }, + { + name: "append log claim", + ctx: ctxLoginClient, + dep: func(ctx context.Context, t *testing.T, req *oidc_pb.CreateCallbackRequest) (string, func()) { + response := &oidc_api.ContextInfoResponse{ + AppendLogClaims: []string{ + "addedLog", + }, + } + return expectPreUserinfoExecution(ctx, t, instance, client.GetClientId(), req, response) + }, + req: &oidc_pb.CreateCallbackRequest{ + AuthRequestId: func() string { + authRequestID, err := instance.CreateOIDCAuthRequestImplicitWithoutLoginClientHeader(isolatedIAMCtx, client.GetClientId(), redirectURIImplicit) + require.NoError(t, err) + return authRequestID + }(), + }, + want: want{ + addedLogClaims: map[string][]string{ + "urn:zitadel:iam:action:function/preuserinfo:log": {"addedLog"}, + }, + }, + wantErr: false, + }, + { + name: "set user metadata", + ctx: ctxLoginClient, + dep: func(ctx context.Context, t *testing.T, req *oidc_pb.CreateCallbackRequest) (string, func()) { + response := &oidc_api.ContextInfoResponse{ + SetUserMetadata: []*domain.Metadata{ + {Key: "key", Value: []byte("value")}, + }, + } + return expectPreUserinfoExecution(ctx, t, instance, client.GetClientId(), req, response) + }, + req: &oidc_pb.CreateCallbackRequest{ + AuthRequestId: func() string { + authRequestID, err := instance.CreateOIDCAuthRequestImplicitWithoutLoginClientHeader(isolatedIAMCtx, client.GetClientId(), redirectURIImplicit) + require.NoError(t, err) + return authRequestID + }(), + }, + want: want{ + setUserMetadata: []*metadata.Metadata{ + {Key: "key", Value: []byte("value")}, + }, + }, + wantErr: false, + }, + { + name: "full usage", + ctx: ctxLoginClient, + dep: func(ctx context.Context, t *testing.T, req *oidc_pb.CreateCallbackRequest) (string, func()) { + response := &oidc_api.ContextInfoResponse{ + SetUserMetadata: []*domain.Metadata{ + {Key: "key1", Value: []byte("value1")}, + {Key: "key2", Value: []byte("value2")}, + {Key: "key3", Value: []byte("value3")}, + }, + AppendLogClaims: []string{ + "addedLog1", + "addedLog2", + "addedLog3", + }, + AppendClaims: []*oidc_api.AppendClaim{ + {Key: "added1", Value: "value1"}, + {Key: "added2", Value: "value2"}, + {Key: "added3", Value: "value3"}, + }, + } + return expectPreUserinfoExecution(ctx, t, instance, client.GetClientId(), req, response) + }, + req: &oidc_pb.CreateCallbackRequest{ + AuthRequestId: func() string { + authRequestID, err := instance.CreateOIDCAuthRequestImplicitWithoutLoginClientHeader(isolatedIAMCtx, client.GetClientId(), redirectURIImplicit) + require.NoError(t, err) + return authRequestID + }(), + }, + want: want{ + addedClaims: map[string]any{ + "added1": "value1", + "added2": "value2", + "added3": "value3", + }, + setUserMetadata: []*metadata.Metadata{ + {Key: "key1", Value: []byte("value1")}, + {Key: "key2", Value: []byte("value2")}, + {Key: "key3", Value: []byte("value3")}, + }, + addedLogClaims: map[string][]string{ + "urn:zitadel:iam:action:function/preuserinfo:log": {"addedLog1", "addedLog2", "addedLog3"}, + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + userID, closeF := tt.dep(isolatedIAMCtx, t, tt.req) + defer closeF() + + got, err := instance.Client.OIDCv2.CreateCallback(tt.ctx, tt.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + callbackUrl, err := url.Parse(strings.Replace(got.GetCallbackUrl(), "#", "?", 1)) + require.NoError(t, err) + claims := getIDTokenClaimsFromCallbackURL(tt.ctx, t, instance, client.GetClientId(), callbackUrl) + + for k, v := range tt.want.addedClaims { + value, ok := claims[k] + if !assert.True(t, ok) { + return + } + assert.Equal(t, v, value) + } + for k, v := range tt.want.addedLogClaims { + value, ok := claims[k] + if !assert.True(t, ok) { + return + } + assert.ElementsMatch(t, v, value) + } + if len(tt.want.setUserMetadata) > 0 { + checkForSetMetadata(isolatedIAMCtx, t, instance, userID, tt.want.setUserMetadata) + } + }) + } +} + +func expectPreUserinfoExecution(ctx context.Context, t *testing.T, instance *integration.Instance, clientID string, req *oidc_pb.CreateCallbackRequest, response *oidc_api.ContextInfoResponse) (string, func()) { + userEmail := gofakeit.Email() + userPhone := "+41" + gofakeit.Phone() + userResp := instance.CreateHumanUserVerified(ctx, instance.DefaultOrg.Id, userEmail, userPhone) + + sessionResp := createSession(ctx, t, instance, userResp.GetUserId()) + req.CallbackKind = &oidc_pb.CreateCallbackRequest_Session{ + Session: &oidc_pb.Session{ + SessionId: sessionResp.GetSessionId(), + SessionToken: sessionResp.GetSessionToken(), + }, + } + expectedContextInfo := contextInfoForUserOIDC(instance, "function/preuserinfo", clientID, userResp, userEmail, userPhone) + + targetURL, closeF, _, _ := integration.TestServerCall(expectedContextInfo, 0, http.StatusOK, response) + + targetResp := waitForTarget(ctx, t, instance, targetURL, domain.TargetTypeCall, true) + waitForExecutionOnCondition(ctx, t, instance, conditionFunction("preuserinfo"), []string{targetResp.GetId()}) + return userResp.GetUserId(), closeF +} + +func createSession(ctx context.Context, t *testing.T, instance *integration.Instance, userID string) *session.CreateSessionResponse { + sessionResp, err := instance.Client.SessionV2.CreateSession(ctx, &session.CreateSessionRequest{ + Checks: &session.Checks{ + User: &session.CheckUser{ + Search: &session.CheckUser_UserId{ + UserId: userID, + }, + }, + }, + }) + require.NoError(t, err) + return sessionResp +} + +func checkForSetMetadata(ctx context.Context, t *testing.T, instance *integration.Instance, userID string, metadataExpected []*metadata.Metadata) { + integration.WaitForAndTickWithMaxDuration(ctx, time.Minute) + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, time.Minute) + assert.EventuallyWithT(t, func(ct *assert.CollectT) { + metadataResp, err := instance.Client.Mgmt.ListUserMetadata(ctx, &management.ListUserMetadataRequest{Id: userID}) + if !assert.NoError(ct, err) { + return + } + for _, dataExpected := range metadataExpected { + found := false + for _, dataCheck := range metadataResp.GetResult() { + if dataExpected.Key == dataCheck.Key { + found = true + if !assert.Equal(ct, dataExpected.Value, dataCheck.Value) { + return + } + } + } + if !assert.True(ct, found) { + return + } + } + }, retryDuration, tick) +} + +func getIDTokenClaimsFromCallbackURL(ctx context.Context, t *testing.T, instance *integration.Instance, clientID string, callbackURL *url.URL) map[string]any { + accessToken := callbackURL.Query().Get("access_token") + idToken := callbackURL.Query().Get("id_token") + + provider, err := instance.CreateRelyingParty(ctx, clientID, redirectURIImplicit, oidc.ScopeOpenID, oidc.ScopeProfile, oidc.ScopeEmail, oidc.ScopePhone) + require.NoError(t, err) + claims, err := rp.VerifyTokens[*oidc.IDTokenClaims](context.Background(), accessToken, idToken, provider.IDTokenVerifier()) + require.NoError(t, err) + return claims.Claims +} + +type CustomAccessTokenClaims struct { + oidc.TokenClaims + Added1 string `json:"added1,omitempty"` + Added2 string `json:"added2,omitempty"` + Added3 string `json:"added3,omitempty"` + Log []string `json:"urn:zitadel:iam:action:function/preaccesstoken:log,omitempty"` +} + +func getAccessTokenClaims(ctx context.Context, t *testing.T, instance *integration.Instance, callbackURL *url.URL) *CustomAccessTokenClaims { + accessToken := callbackURL.Query().Get("access_token") + + verifier := op.NewAccessTokenVerifier(instance.OIDCIssuer(), rp.NewRemoteKeySet(http.DefaultClient, instance.OIDCIssuer()+"/oauth/v2/keys")) + + claims, err := op.VerifyAccessToken[*CustomAccessTokenClaims](ctx, accessToken, verifier) + require.NoError(t, err) + return claims +} + +func contextInfoForUserOIDC(instance *integration.Instance, function string, clientID string, userResp *user.AddHumanUserResponse, email, phone string) *oidc_api.ContextInfo { + return &oidc_api.ContextInfo{ + Function: function, + UserInfo: &oidc.UserInfo{ + Subject: userResp.GetUserId(), + }, + User: &query.User{ + ID: userResp.GetUserId(), + CreationDate: userResp.Details.ChangeDate.AsTime(), + ChangeDate: userResp.Details.ChangeDate.AsTime(), + ResourceOwner: instance.DefaultOrg.GetId(), + Sequence: userResp.Details.Sequence, + State: 1, + Username: email, + PreferredLoginName: email, + Human: &query.Human{ + FirstName: "Mickey", + LastName: "Mouse", + NickName: "Mickey", + DisplayName: "Mickey Mouse", + AvatarKey: "", + PreferredLanguage: language.Dutch, + Gender: 2, + Email: domain.EmailAddress(email), + IsEmailVerified: true, + Phone: domain.PhoneNumber(phone), + IsPhoneVerified: true, + PasswordChangeRequired: false, + PasswordChanged: time.Time{}, + MFAInitSkipped: time.Time{}, + }, + }, + UserMetadata: nil, + Application: &oidc_api.ContextInfoApplication{ + ClientID: clientID, + }, + Org: &query.UserInfoOrg{ + ID: instance.DefaultOrg.GetId(), + Name: instance.DefaultOrg.GetName(), + PrimaryDomain: instance.DefaultOrg.GetPrimaryDomain(), + }, + UserGrants: nil, + Response: nil, + } +} + +func TestServer_ExecutionTargetPreAccessToken(t *testing.T) { + instance := integration.NewInstance(CTX) + isolatedIAMCtx := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + ctxLoginClient := instance.WithAuthorizationToken(CTX, integration.UserTypeLogin) + + client, err := instance.CreateOIDCImplicitFlowClient(isolatedIAMCtx, t, redirectURIImplicit, loginV2) + require.NoError(t, err) + + type want struct { + addedClaims *CustomAccessTokenClaims + addedLogClaims map[string][]string + setUserMetadata []*metadata.Metadata + } + tests := []struct { + name string + ctx context.Context + dep func(ctx context.Context, t *testing.T, req *oidc_pb.CreateCallbackRequest) (string, func()) + req *oidc_pb.CreateCallbackRequest + want want + wantErr bool + }{ + { + name: "append claim", + ctx: ctxLoginClient, + dep: func(ctx context.Context, t *testing.T, req *oidc_pb.CreateCallbackRequest) (string, func()) { + response := &oidc_api.ContextInfoResponse{ + AppendClaims: []*oidc_api.AppendClaim{ + {Key: "added1", Value: "value"}, + }, + } + return expectPreAccessTokenExecution(ctx, t, instance, client.GetClientId(), req, response) + }, + req: &oidc_pb.CreateCallbackRequest{ + AuthRequestId: func() string { + authRequestID, err := instance.CreateOIDCAuthRequestImplicitWithoutLoginClientHeader(isolatedIAMCtx, client.GetClientId(), redirectURIImplicit) + require.NoError(t, err) + return authRequestID + }(), + }, + want: want{ + addedClaims: &CustomAccessTokenClaims{ + Added1: "value", + }, + }, + wantErr: false, + }, + { + name: "append log claim", + ctx: ctxLoginClient, + dep: func(ctx context.Context, t *testing.T, req *oidc_pb.CreateCallbackRequest) (string, func()) { + response := &oidc_api.ContextInfoResponse{ + AppendLogClaims: []string{ + "addedLog", + }, + } + return expectPreAccessTokenExecution(ctx, t, instance, client.GetClientId(), req, response) + }, + req: &oidc_pb.CreateCallbackRequest{ + AuthRequestId: func() string { + authRequestID, err := instance.CreateOIDCAuthRequestImplicitWithoutLoginClientHeader(isolatedIAMCtx, client.GetClientId(), redirectURIImplicit) + require.NoError(t, err) + return authRequestID + }(), + }, + want: want{ + addedClaims: &CustomAccessTokenClaims{ + Log: []string{"addedLog"}, + }, + }, + wantErr: false, + }, + { + name: "set user metadata", + ctx: ctxLoginClient, + dep: func(ctx context.Context, t *testing.T, req *oidc_pb.CreateCallbackRequest) (string, func()) { + response := &oidc_api.ContextInfoResponse{ + SetUserMetadata: []*domain.Metadata{ + {Key: "key", Value: []byte("value")}, + }, + } + return expectPreAccessTokenExecution(ctx, t, instance, client.GetClientId(), req, response) + }, + req: &oidc_pb.CreateCallbackRequest{ + AuthRequestId: func() string { + authRequestID, err := instance.CreateOIDCAuthRequestImplicitWithoutLoginClientHeader(isolatedIAMCtx, client.GetClientId(), redirectURIImplicit) + require.NoError(t, err) + return authRequestID + }(), + }, + want: want{ + setUserMetadata: []*metadata.Metadata{ + {Key: "key", Value: []byte("value")}, + }, + }, + wantErr: false, + }, + { + name: "full usage", + ctx: ctxLoginClient, + dep: func(ctx context.Context, t *testing.T, req *oidc_pb.CreateCallbackRequest) (string, func()) { + response := &oidc_api.ContextInfoResponse{ + SetUserMetadata: []*domain.Metadata{ + {Key: "key1", Value: []byte("value1")}, + {Key: "key2", Value: []byte("value2")}, + {Key: "key3", Value: []byte("value3")}, + }, + AppendLogClaims: []string{ + "addedLog1", + "addedLog2", + "addedLog3", + }, + AppendClaims: []*oidc_api.AppendClaim{ + {Key: "added1", Value: "value1"}, + {Key: "added2", Value: "value2"}, + {Key: "added3", Value: "value3"}, + }, + } + return expectPreAccessTokenExecution(ctx, t, instance, client.GetClientId(), req, response) + }, + req: &oidc_pb.CreateCallbackRequest{ + AuthRequestId: func() string { + authRequestID, err := instance.CreateOIDCAuthRequestImplicitWithoutLoginClientHeader(isolatedIAMCtx, client.GetClientId(), redirectURIImplicit) + require.NoError(t, err) + return authRequestID + }(), + }, + want: want{ + addedClaims: &CustomAccessTokenClaims{ + Added1: "value1", + Added2: "value2", + Added3: "value3", + Log: []string{"addedLog1", "addedLog2", "addedLog3"}, + }, + setUserMetadata: []*metadata.Metadata{ + {Key: "key1", Value: []byte("value1")}, + {Key: "key2", Value: []byte("value2")}, + {Key: "key3", Value: []byte("value3")}, + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + userID, closeF := tt.dep(isolatedIAMCtx, t, tt.req) + defer closeF() + + got, err := instance.Client.OIDCv2.CreateCallback(tt.ctx, tt.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + + callbackUrl, err := url.Parse(strings.Replace(got.GetCallbackUrl(), "#", "?", 1)) + require.NoError(t, err) + claims := getAccessTokenClaims(tt.ctx, t, instance, callbackUrl) + + if tt.want.addedClaims != nil { + assert.Equal(t, tt.want.addedClaims.Added1, claims.Added1) + assert.Equal(t, tt.want.addedClaims.Added2, claims.Added2) + assert.Equal(t, tt.want.addedClaims.Added3, claims.Added3) + assert.Equal(t, tt.want.addedClaims.Log, claims.Log) + } + if len(tt.want.setUserMetadata) > 0 { + checkForSetMetadata(isolatedIAMCtx, t, instance, userID, tt.want.setUserMetadata) + } + + }) + } +} + +func expectPreAccessTokenExecution(ctx context.Context, t *testing.T, instance *integration.Instance, clientID string, req *oidc_pb.CreateCallbackRequest, response *oidc_api.ContextInfoResponse) (string, func()) { + userEmail := gofakeit.Email() + userPhone := "+41" + gofakeit.Phone() + userResp := instance.CreateHumanUserVerified(ctx, instance.DefaultOrg.Id, userEmail, userPhone) + + sessionResp := createSession(ctx, t, instance, userResp.GetUserId()) + req.CallbackKind = &oidc_pb.CreateCallbackRequest_Session{ + Session: &oidc_pb.Session{ + SessionId: sessionResp.GetSessionId(), + SessionToken: sessionResp.GetSessionToken(), + }, + } + expectedContextInfo := contextInfoForUserOIDC(instance, "function/preaccesstoken", clientID, userResp, userEmail, userPhone) + + targetURL, closeF, _, _ := integration.TestServerCall(expectedContextInfo, 0, http.StatusOK, response) + + targetResp := waitForTarget(ctx, t, instance, targetURL, domain.TargetTypeCall, true) + waitForExecutionOnCondition(ctx, t, instance, conditionFunction("preaccesstoken"), []string{targetResp.GetId()}) + return userResp.GetUserId(), closeF +} + +func TestServer_ExecutionTargetPreSAMLResponse(t *testing.T) { + instance := integration.NewInstance(CTX) + isolatedIAMCtx := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + ctxLoginClient := instance.WithAuthorizationToken(CTX, integration.UserTypeLogin) + + idpMetadata, err := instance.GetSAMLIDPMetadata() + require.NoError(t, err) + + acsPost := idpMetadata.IDPSSODescriptors[0].SingleSignOnServices[1] + _, _, spMiddlewarePost := createSAMLApplication(isolatedIAMCtx, t, instance, idpMetadata, saml.HTTPPostBinding, false, false) + + type want struct { + addedAttributes map[string][]saml.AttributeValue + setUserMetadata []*metadata.Metadata + } + tests := []struct { + name string + ctx context.Context + dep func(ctx context.Context, t *testing.T, req *saml_pb.CreateResponseRequest) (string, func()) + req *saml_pb.CreateResponseRequest + want want + wantErr bool + }{ + { + name: "append attribute", + ctx: ctxLoginClient, + dep: func(ctx context.Context, t *testing.T, req *saml_pb.CreateResponseRequest) (string, func()) { + response := &saml_api.ContextInfoResponse{ + AppendAttribute: []*saml_api.AppendAttribute{ + {Name: "added", NameFormat: "format", Value: []string{"value"}}, + }, + } + return expectPreSAMLResponseExecution(ctx, t, instance, req, response) + }, + req: &saml_pb.CreateResponseRequest{ + SamlRequestId: func() string { + _, samlRequestID, err := instance.CreateSAMLAuthRequest(spMiddlewarePost, instance.Users[integration.UserTypeOrgOwner].ID, acsPost, gofakeit.BitcoinAddress(), saml.HTTPPostBinding) + require.NoError(t, err) + return samlRequestID + }(), + }, + want: want{ + addedAttributes: map[string][]saml.AttributeValue{ + "added": {saml.AttributeValue{Value: "value"}}, + }, + }, + wantErr: false, + }, + { + name: "set user metadata", + ctx: ctxLoginClient, + dep: func(ctx context.Context, t *testing.T, req *saml_pb.CreateResponseRequest) (string, func()) { + response := &saml_api.ContextInfoResponse{ + SetUserMetadata: []*domain.Metadata{ + {Key: "key", Value: []byte("value")}, + }, + } + return expectPreSAMLResponseExecution(ctx, t, instance, req, response) + }, + req: &saml_pb.CreateResponseRequest{ + SamlRequestId: func() string { + _, samlRequestID, err := instance.CreateSAMLAuthRequest(spMiddlewarePost, instance.Users[integration.UserTypeOrgOwner].ID, acsPost, gofakeit.BitcoinAddress(), saml.HTTPPostBinding) + require.NoError(t, err) + return samlRequestID + }(), + }, + want: want{ + setUserMetadata: []*metadata.Metadata{ + {Key: "key", Value: []byte("value")}, + }, + }, + wantErr: false, + }, + { + name: "set user metadata", + ctx: ctxLoginClient, + dep: func(ctx context.Context, t *testing.T, req *saml_pb.CreateResponseRequest) (string, func()) { + response := &saml_api.ContextInfoResponse{ + AppendAttribute: []*saml_api.AppendAttribute{ + {Name: "added1", NameFormat: "format", Value: []string{"value1"}}, + {Name: "added2", NameFormat: "format", Value: []string{"value2"}}, + {Name: "added3", NameFormat: "format", Value: []string{"value3"}}, + }, + SetUserMetadata: []*domain.Metadata{ + {Key: "key1", Value: []byte("value1")}, + {Key: "key2", Value: []byte("value2")}, + {Key: "key3", Value: []byte("value3")}, + }, + } + return expectPreSAMLResponseExecution(ctx, t, instance, req, response) + }, + req: &saml_pb.CreateResponseRequest{ + SamlRequestId: func() string { + _, samlRequestID, err := instance.CreateSAMLAuthRequest(spMiddlewarePost, instance.Users[integration.UserTypeOrgOwner].ID, acsPost, gofakeit.BitcoinAddress(), saml.HTTPPostBinding) + require.NoError(t, err) + return samlRequestID + }(), + }, + want: want{ + addedAttributes: map[string][]saml.AttributeValue{ + "added1": {saml.AttributeValue{Value: "value1"}}, + "added2": {saml.AttributeValue{Value: "value2"}}, + "added3": {saml.AttributeValue{Value: "value3"}}, + }, + setUserMetadata: []*metadata.Metadata{ + {Key: "key1", Value: []byte("value1")}, + {Key: "key2", Value: []byte("value2")}, + {Key: "key3", Value: []byte("value3")}, + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + userID, closeF := tt.dep(isolatedIAMCtx, t, tt.req) + defer closeF() + + got, err := instance.Client.SAMLv2.CreateResponse(tt.ctx, tt.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + attributes := getSAMLResponseAttributes(t, got.GetPost().GetSamlResponse(), spMiddlewarePost) + for k, v := range tt.want.addedAttributes { + found := false + for _, attribute := range attributes { + if attribute.Name == k { + found = true + assert.Equal(t, v, attribute.Values) + } + } + if !assert.True(t, found) { + return + } + } + if len(tt.want.setUserMetadata) > 0 { + checkForSetMetadata(isolatedIAMCtx, t, instance, userID, tt.want.setUserMetadata) + } + }) + } +} + +func expectPreSAMLResponseExecution(ctx context.Context, t *testing.T, instance *integration.Instance, req *saml_pb.CreateResponseRequest, response *saml_api.ContextInfoResponse) (string, func()) { + userEmail := gofakeit.Email() + userPhone := "+41" + gofakeit.Phone() + userResp := instance.CreateHumanUserVerified(ctx, instance.DefaultOrg.Id, userEmail, userPhone) + + sessionResp := createSession(ctx, t, instance, userResp.GetUserId()) + req.ResponseKind = &saml_pb.CreateResponseRequest_Session{ + Session: &saml_pb.Session{ + SessionId: sessionResp.GetSessionId(), + SessionToken: sessionResp.GetSessionToken(), + }, + } + expectedContextInfo := contextInfoForUserSAML(instance, "function/presamlresponse", userResp, userEmail, userPhone) + + targetURL, closeF, _, _ := integration.TestServerCall(expectedContextInfo, 0, http.StatusOK, response) + + targetResp := waitForTarget(ctx, t, instance, targetURL, domain.TargetTypeCall, true) + waitForExecutionOnCondition(ctx, t, instance, conditionFunction("presamlresponse"), []string{targetResp.GetId()}) + + return userResp.GetUserId(), closeF +} + +func createSAMLSP(t *testing.T, idpMetadata *saml.EntityDescriptor, binding string) (string, *samlsp.Middleware) { + rootURL := "example." + gofakeit.DomainName() + spMiddleware, err := integration.CreateSAMLSP("https://"+rootURL, idpMetadata, binding) + require.NoError(t, err) + return rootURL, spMiddleware +} + +func createSAMLApplication(ctx context.Context, t *testing.T, instance *integration.Instance, idpMetadata *saml.EntityDescriptor, binding string, projectRoleCheck, hasProjectCheck bool) (string, string, *samlsp.Middleware) { + project := instance.CreateProject(ctx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), projectRoleCheck, hasProjectCheck) + rootURL, sp := createSAMLSP(t, idpMetadata, binding) + _, err := instance.CreateSAMLClient(ctx, project.GetId(), sp) + require.NoError(t, err) + return project.GetId(), rootURL, sp +} + +func getSAMLResponseAttributes(t *testing.T, samlResponse string, sp *samlsp.Middleware) []saml.Attribute { + data, err := base64.StdEncoding.DecodeString(samlResponse) + require.NoError(t, err) + sp.ServiceProvider.AllowIDPInitiated = true + assertion, err := sp.ServiceProvider.ParseXMLResponse(data, []string{}) + require.NoError(t, err) + return assertion.AttributeStatements[0].Attributes +} + +func contextInfoForUserSAML(instance *integration.Instance, function string, userResp *user.AddHumanUserResponse, email, phone string) *saml_api.ContextInfo { + return &saml_api.ContextInfo{ + Function: function, + User: &query.User{ + ID: userResp.GetUserId(), + CreationDate: userResp.Details.ChangeDate.AsTime(), + ChangeDate: userResp.Details.ChangeDate.AsTime(), + ResourceOwner: instance.DefaultOrg.GetId(), + Sequence: userResp.Details.Sequence, + State: 1, + Type: domain.UserTypeHuman, + Username: email, + PreferredLoginName: email, + LoginNames: []string{email}, + Human: &query.Human{ + FirstName: "Mickey", + LastName: "Mouse", + NickName: "Mickey", + DisplayName: "Mickey Mouse", + AvatarKey: "", + PreferredLanguage: language.Dutch, + Gender: 2, + Email: domain.EmailAddress(email), + IsEmailVerified: true, + Phone: domain.PhoneNumber(phone), + IsPhoneVerified: true, + PasswordChangeRequired: false, + PasswordChanged: time.Time{}, + MFAInitSkipped: time.Time{}, + }, + }, + UserGrants: nil, + Response: nil, + } +} diff --git a/internal/api/grpc/action/v2/integration_test/execution_test.go b/internal/api/grpc/action/v2/integration_test/execution_test.go new file mode 100644 index 0000000000..da31fe5a0b --- /dev/null +++ b/internal/api/grpc/action/v2/integration_test/execution_test.go @@ -0,0 +1,565 @@ +//go:build integration + +package action_test + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/integration" + "github.com/zitadel/zitadel/pkg/grpc/action/v2" +) + +func TestServer_SetExecution_Request(t *testing.T) { + instance := integration.NewInstance(CTX) + isolatedIAMOwnerCTX := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false) + + tests := []struct { + name string + ctx context.Context + req *action.SetExecutionRequest + wantSetDate bool + wantErr bool + }{ + { + name: "missing permission", + ctx: instance.WithAuthorizationToken(context.Background(), integration.UserTypeOrgOwner), + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Request{ + Request: &action.RequestExecution{ + Condition: &action.RequestExecution_All{All: true}, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "no condition, error", + ctx: isolatedIAMOwnerCTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Request{ + Request: &action.RequestExecution{}, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantErr: true, + }, + { + name: "method, not existing", + ctx: isolatedIAMOwnerCTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Request{ + Request: &action.RequestExecution{ + Condition: &action.RequestExecution_Method{ + Method: "/zitadel.session.v2.NotExistingService/List", + }, + }, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantErr: true, + }, + { + name: "method, ok", + ctx: isolatedIAMOwnerCTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Request{ + Request: &action.RequestExecution{ + Condition: &action.RequestExecution_Method{ + Method: "/zitadel.session.v2.SessionService/ListSessions", + }, + }, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantSetDate: true, + }, + { + name: "service, not existing", + ctx: isolatedIAMOwnerCTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Request{ + Request: &action.RequestExecution{ + Condition: &action.RequestExecution_Service{ + Service: "NotExistingService", + }, + }, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantErr: true, + }, + { + name: "service, ok", + ctx: isolatedIAMOwnerCTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Request{ + Request: &action.RequestExecution{ + Condition: &action.RequestExecution_Service{ + Service: "zitadel.session.v2.SessionService", + }, + }, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantSetDate: true, + }, + { + name: "all, ok", + ctx: isolatedIAMOwnerCTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Request{ + Request: &action.RequestExecution{ + Condition: &action.RequestExecution_All{ + All: true, + }, + }, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantSetDate: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // We want to have the same response no matter how often we call the function + creationDate := time.Now().UTC() + got, err := instance.Client.ActionV2.SetExecution(tt.ctx, tt.req) + setDate := time.Now().UTC() + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + + assertSetExecutionResponse(t, creationDate, setDate, tt.wantSetDate, got) + + // cleanup to not impact other requests + instance.DeleteExecution(tt.ctx, t, tt.req.GetCondition()) + }) + } +} + +func assertSetExecutionResponse(t *testing.T, creationDate, setDate time.Time, expectedSetDate bool, actualResp *action.SetExecutionResponse) { + if expectedSetDate { + if !setDate.IsZero() { + assert.WithinRange(t, actualResp.GetSetDate().AsTime(), creationDate, setDate) + } else { + assert.WithinRange(t, actualResp.GetSetDate().AsTime(), creationDate, time.Now().UTC()) + } + } else { + assert.Nil(t, actualResp.SetDate) + } +} + +func TestServer_SetExecution_Response(t *testing.T) { + instance := integration.NewInstance(CTX) + isolatedIAMOwnerCTX := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false) + + tests := []struct { + name string + ctx context.Context + req *action.SetExecutionRequest + wantSetDate bool + wantErr bool + }{ + { + name: "missing permission", + ctx: instance.WithAuthorizationToken(context.Background(), integration.UserTypeOrgOwner), + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Response{ + Response: &action.ResponseExecution{ + Condition: &action.ResponseExecution_All{All: true}, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "no condition, error", + ctx: isolatedIAMOwnerCTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Response{ + Response: &action.ResponseExecution{}, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantErr: true, + }, + { + name: "method, not existing", + ctx: isolatedIAMOwnerCTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Response{ + Response: &action.ResponseExecution{ + Condition: &action.ResponseExecution_Method{ + Method: "/zitadel.session.v2.NotExistingService/List", + }, + }, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantErr: true, + }, + { + name: "method, ok", + ctx: isolatedIAMOwnerCTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Response{ + Response: &action.ResponseExecution{ + Condition: &action.ResponseExecution_Method{ + Method: "/zitadel.session.v2.SessionService/ListSessions", + }, + }, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantSetDate: true, + }, + { + name: "service, not existing", + ctx: isolatedIAMOwnerCTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Response{ + Response: &action.ResponseExecution{ + Condition: &action.ResponseExecution_Service{ + Service: "NotExistingService", + }, + }, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantErr: true, + }, + { + name: "service, ok", + ctx: isolatedIAMOwnerCTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Response{ + Response: &action.ResponseExecution{ + Condition: &action.ResponseExecution_Service{ + Service: "zitadel.session.v2.SessionService", + }, + }, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantSetDate: true, + }, + { + name: "all, ok", + ctx: isolatedIAMOwnerCTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Response{ + Response: &action.ResponseExecution{ + Condition: &action.ResponseExecution_All{ + All: true, + }, + }, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantSetDate: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creationDate := time.Now().UTC() + got, err := instance.Client.ActionV2.SetExecution(tt.ctx, tt.req) + setDate := time.Now().UTC() + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + + assertSetExecutionResponse(t, creationDate, setDate, tt.wantSetDate, got) + + // cleanup to not impact other requests + instance.DeleteExecution(tt.ctx, t, tt.req.GetCondition()) + }) + } +} + +func TestServer_SetExecution_Event(t *testing.T) { + instance := integration.NewInstance(CTX) + isolatedIAMOwnerCTX := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false) + + tests := []struct { + name string + ctx context.Context + req *action.SetExecutionRequest + wantSetDate bool + wantErr bool + }{ + { + name: "missing permission", + ctx: instance.WithAuthorizationToken(context.Background(), integration.UserTypeOrgOwner), + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Event{ + Event: &action.EventExecution{ + Condition: &action.EventExecution_All{ + All: true, + }, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "no condition, error", + ctx: isolatedIAMOwnerCTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Event{ + Event: &action.EventExecution{}, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantErr: true, + }, + { + name: "event, not existing", + ctx: isolatedIAMOwnerCTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Event{ + Event: &action.EventExecution{ + Condition: &action.EventExecution_Event{ + Event: "user.human.notexisting", + }, + }, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantErr: true, + }, + { + name: "event, ok", + ctx: isolatedIAMOwnerCTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Event{ + Event: &action.EventExecution{ + Condition: &action.EventExecution_Event{ + Event: "user.human.added", + }, + }, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantSetDate: true, + }, + { + name: "group, not existing", + ctx: isolatedIAMOwnerCTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Event{ + Event: &action.EventExecution{ + Condition: &action.EventExecution_Group{ + Group: "user.notexisting", + }, + }, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantErr: true, + }, + { + name: "group, level 1, ok", + ctx: isolatedIAMOwnerCTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Event{ + Event: &action.EventExecution{ + Condition: &action.EventExecution_Group{ + Group: "user", + }, + }, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantSetDate: true, + }, + { + name: "group, level 2, ok", + ctx: isolatedIAMOwnerCTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Event{ + Event: &action.EventExecution{ + Condition: &action.EventExecution_Group{ + Group: "user.human", + }, + }, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantSetDate: true, + }, + { + name: "all, ok", + ctx: isolatedIAMOwnerCTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Event{ + Event: &action.EventExecution{ + Condition: &action.EventExecution_All{ + All: true, + }, + }, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantSetDate: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creationDate := time.Now().UTC() + got, err := instance.Client.ActionV2.SetExecution(tt.ctx, tt.req) + setDate := time.Now().UTC() + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + + assertSetExecutionResponse(t, creationDate, setDate, tt.wantSetDate, got) + + // cleanup to not impact other requests + instance.DeleteExecution(tt.ctx, t, tt.req.GetCondition()) + }) + } +} + +func TestServer_SetExecution_Function(t *testing.T) { + instance := integration.NewInstance(CTX) + isolatedIAMOwnerCTX := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false) + + tests := []struct { + name string + ctx context.Context + req *action.SetExecutionRequest + wantSetDate bool + wantErr bool + }{ + { + name: "missing permission", + ctx: instance.WithAuthorizationToken(context.Background(), integration.UserTypeOrgOwner), + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Response{ + Response: &action.ResponseExecution{ + Condition: &action.ResponseExecution_All{All: true}, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "no condition, error", + ctx: isolatedIAMOwnerCTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Response{ + Response: &action.ResponseExecution{}, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantErr: true, + }, + { + name: "function, not existing", + ctx: isolatedIAMOwnerCTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Function{ + Function: &action.FunctionExecution{Name: "xxx"}, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantErr: true, + }, + { + name: "function, ok", + ctx: isolatedIAMOwnerCTX, + req: &action.SetExecutionRequest{ + Condition: &action.Condition{ + ConditionType: &action.Condition_Function{ + Function: &action.FunctionExecution{Name: "presamlresponse"}, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + wantSetDate: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creationDate := time.Now().UTC() + got, err := instance.Client.ActionV2.SetExecution(tt.ctx, tt.req) + setDate := time.Now().UTC() + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + + assertSetExecutionResponse(t, creationDate, setDate, tt.wantSetDate, got) + + // cleanup to not impact other requests + instance.DeleteExecution(tt.ctx, t, tt.req.GetCondition()) + }) + } +} diff --git a/internal/api/grpc/action/v2/integration_test/query_test.go b/internal/api/grpc/action/v2/integration_test/query_test.go new file mode 100644 index 0000000000..2a93a4ad4b --- /dev/null +++ b/internal/api/grpc/action/v2/integration_test/query_test.go @@ -0,0 +1,784 @@ +//go:build integration + +package action_test + +import ( + "context" + "testing" + "time" + + "github.com/brianvoe/gofakeit/v6" + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/durationpb" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/integration" + "github.com/zitadel/zitadel/pkg/grpc/action/v2" + "github.com/zitadel/zitadel/pkg/grpc/filter/v2" +) + +func TestServer_GetTarget(t *testing.T) { + instance := integration.NewInstance(CTX) + isolatedIAMOwnerCTX := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + type args struct { + ctx context.Context + dep func(context.Context, *action.GetTargetRequest, *action.GetTargetResponse) error + req *action.GetTargetRequest + } + tests := []struct { + name string + args args + want *action.GetTargetResponse + wantErr bool + }{ + { + name: "missing permission", + args: args{ + ctx: instance.WithAuthorizationToken(context.Background(), integration.UserTypeOrgOwner), + req: &action.GetTargetRequest{}, + }, + wantErr: true, + }, + { + name: "not found", + args: args{ + ctx: isolatedIAMOwnerCTX, + req: &action.GetTargetRequest{Id: "notexisting"}, + }, + wantErr: true, + }, + { + name: "get, ok", + args: args{ + ctx: isolatedIAMOwnerCTX, + dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) error { + name := gofakeit.Name() + resp := instance.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeWebhook, false) + request.Id = resp.GetId() + response.Target.Id = resp.GetId() + response.Target.Name = name + response.Target.CreationDate = resp.GetCreationDate() + response.Target.ChangeDate = resp.GetCreationDate() + response.Target.SigningKey = resp.GetSigningKey() + return nil + }, + req: &action.GetTargetRequest{}, + }, + want: &action.GetTargetResponse{ + Target: &action.Target{ + Endpoint: "https://example.com", + TargetType: &action.Target_RestWebhook{ + RestWebhook: &action.RESTWebhook{}, + }, + Timeout: durationpb.New(5 * time.Second), + }, + }, + }, + { + name: "get, async, ok", + args: args{ + ctx: isolatedIAMOwnerCTX, + dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) error { + name := gofakeit.Name() + resp := instance.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeAsync, false) + request.Id = resp.GetId() + response.Target.Id = resp.GetId() + response.Target.Name = name + response.Target.CreationDate = resp.GetCreationDate() + response.Target.ChangeDate = resp.GetCreationDate() + response.Target.SigningKey = resp.GetSigningKey() + return nil + }, + req: &action.GetTargetRequest{}, + }, + want: &action.GetTargetResponse{ + Target: &action.Target{ + Endpoint: "https://example.com", + TargetType: &action.Target_RestAsync{ + RestAsync: &action.RESTAsync{}, + }, + Timeout: durationpb.New(5 * time.Second), + }, + }, + }, + { + name: "get, webhook interruptOnError, ok", + args: args{ + ctx: isolatedIAMOwnerCTX, + dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) error { + name := gofakeit.Name() + resp := instance.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeWebhook, true) + request.Id = resp.GetId() + response.Target.Id = resp.GetId() + response.Target.Name = name + response.Target.CreationDate = resp.GetCreationDate() + response.Target.ChangeDate = resp.GetCreationDate() + response.Target.SigningKey = resp.GetSigningKey() + return nil + }, + req: &action.GetTargetRequest{}, + }, + want: &action.GetTargetResponse{ + Target: &action.Target{ + Endpoint: "https://example.com", + TargetType: &action.Target_RestWebhook{ + RestWebhook: &action.RESTWebhook{ + InterruptOnError: true, + }, + }, + Timeout: durationpb.New(5 * time.Second), + }, + }, + }, + { + name: "get, call, ok", + args: args{ + ctx: isolatedIAMOwnerCTX, + dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) error { + name := gofakeit.Name() + resp := instance.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeCall, false) + request.Id = resp.GetId() + response.Target.Id = resp.GetId() + response.Target.Name = name + response.Target.CreationDate = resp.GetCreationDate() + response.Target.ChangeDate = resp.GetCreationDate() + response.Target.SigningKey = resp.GetSigningKey() + return nil + }, + req: &action.GetTargetRequest{}, + }, + want: &action.GetTargetResponse{ + Target: &action.Target{ + Endpoint: "https://example.com", + TargetType: &action.Target_RestCall{ + RestCall: &action.RESTCall{ + InterruptOnError: false, + }, + }, + Timeout: durationpb.New(5 * time.Second), + }, + }, + }, + { + name: "get, call interruptOnError, ok", + args: args{ + ctx: isolatedIAMOwnerCTX, + dep: func(ctx context.Context, request *action.GetTargetRequest, response *action.GetTargetResponse) error { + name := gofakeit.Name() + resp := instance.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeCall, true) + request.Id = resp.GetId() + response.Target.Id = resp.GetId() + response.Target.Name = name + response.Target.CreationDate = resp.GetCreationDate() + response.Target.ChangeDate = resp.GetCreationDate() + response.Target.SigningKey = resp.GetSigningKey() + return nil + }, + req: &action.GetTargetRequest{}, + }, + want: &action.GetTargetResponse{ + Target: &action.Target{ + Endpoint: "https://example.com", + TargetType: &action.Target_RestCall{ + RestCall: &action.RESTCall{ + InterruptOnError: true, + }, + }, + Timeout: durationpb.New(5 * time.Second), + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.args.dep != nil { + err := tt.args.dep(tt.args.ctx, tt.args.req, tt.want) + require.NoError(t, err) + } + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(isolatedIAMOwnerCTX, 2*time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, err := instance.Client.ActionV2.GetTarget(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(ttt, err, "Error: "+err.Error()) + return + } + assert.NoError(ttt, err) + assert.EqualExportedValues(ttt, tt.want, got) + }, retryDuration, tick, "timeout waiting for expected target Executions") + }) + } +} + +func TestServer_ListTargets(t *testing.T) { + instance := integration.NewInstance(CTX) + isolatedIAMOwnerCTX := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + type args struct { + ctx context.Context + dep func(context.Context, *action.ListTargetsRequest, *action.ListTargetsResponse) + req *action.ListTargetsRequest + } + tests := []struct { + name string + args args + want *action.ListTargetsResponse + wantErr bool + }{ + { + name: "missing permission", + args: args{ + ctx: instance.WithAuthorizationToken(context.Background(), integration.UserTypeOrgOwner), + req: &action.ListTargetsRequest{}, + }, + wantErr: true, + }, + { + name: "list, not found", + args: args{ + ctx: isolatedIAMOwnerCTX, + req: &action.ListTargetsRequest{ + Filters: []*action.TargetSearchFilter{ + {Filter: &action.TargetSearchFilter_InTargetIdsFilter{ + InTargetIdsFilter: &action.InTargetIDsFilter{ + TargetIds: []string{"notfound"}, + }, + }, + }, + }, + }, + }, + want: &action.ListTargetsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 0, + AppliedLimit: 100, + }, + Targets: []*action.Target{}, + }, + }, + { + name: "list single id", + args: args{ + ctx: isolatedIAMOwnerCTX, + dep: func(ctx context.Context, request *action.ListTargetsRequest, response *action.ListTargetsResponse) { + name := gofakeit.Name() + resp := instance.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeWebhook, false) + request.Filters[0].Filter = &action.TargetSearchFilter_InTargetIdsFilter{ + InTargetIdsFilter: &action.InTargetIDsFilter{ + TargetIds: []string{resp.GetId()}, + }, + } + + response.Targets[0].Id = resp.GetId() + response.Targets[0].Name = name + response.Targets[0].CreationDate = resp.GetCreationDate() + response.Targets[0].ChangeDate = resp.GetCreationDate() + response.Targets[0].SigningKey = resp.GetSigningKey() + }, + req: &action.ListTargetsRequest{ + Filters: []*action.TargetSearchFilter{{}}, + }, + }, + want: &action.ListTargetsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Targets: []*action.Target{ + { + Endpoint: "https://example.com", + TargetType: &action.Target_RestWebhook{ + RestWebhook: &action.RESTWebhook{ + InterruptOnError: false, + }, + }, + Timeout: durationpb.New(5 * time.Second), + }, + }, + }, + }, { + name: "list single name", + args: args{ + ctx: isolatedIAMOwnerCTX, + dep: func(ctx context.Context, request *action.ListTargetsRequest, response *action.ListTargetsResponse) { + name := gofakeit.Name() + resp := instance.CreateTarget(ctx, t, name, "https://example.com", domain.TargetTypeWebhook, false) + request.Filters[0].Filter = &action.TargetSearchFilter_TargetNameFilter{ + TargetNameFilter: &action.TargetNameFilter{ + TargetName: name, + }, + } + + response.Targets[0].Id = resp.GetId() + response.Targets[0].Name = name + response.Targets[0].CreationDate = resp.GetCreationDate() + response.Targets[0].ChangeDate = resp.GetCreationDate() + response.Targets[0].SigningKey = resp.GetSigningKey() + }, + req: &action.ListTargetsRequest{ + Filters: []*action.TargetSearchFilter{{}}, + }, + }, + want: &action.ListTargetsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Targets: []*action.Target{ + { + Endpoint: "https://example.com", + TargetType: &action.Target_RestWebhook{ + RestWebhook: &action.RESTWebhook{ + InterruptOnError: false, + }, + }, + Timeout: durationpb.New(5 * time.Second), + }, + }, + }, + }, + { + name: "list multiple id", + args: args{ + ctx: isolatedIAMOwnerCTX, + dep: func(ctx context.Context, request *action.ListTargetsRequest, response *action.ListTargetsResponse) { + name1 := gofakeit.Name() + name2 := gofakeit.Name() + name3 := gofakeit.Name() + resp1 := instance.CreateTarget(ctx, t, name1, "https://example.com", domain.TargetTypeWebhook, false) + resp2 := instance.CreateTarget(ctx, t, name2, "https://example.com", domain.TargetTypeCall, true) + resp3 := instance.CreateTarget(ctx, t, name3, "https://example.com", domain.TargetTypeAsync, false) + request.Filters[0].Filter = &action.TargetSearchFilter_InTargetIdsFilter{ + InTargetIdsFilter: &action.InTargetIDsFilter{ + TargetIds: []string{resp1.GetId(), resp2.GetId(), resp3.GetId()}, + }, + } + + response.Targets[2].Id = resp1.GetId() + response.Targets[2].Name = name1 + response.Targets[2].CreationDate = resp1.GetCreationDate() + response.Targets[2].ChangeDate = resp1.GetCreationDate() + response.Targets[2].SigningKey = resp1.GetSigningKey() + + response.Targets[1].Id = resp2.GetId() + response.Targets[1].Name = name2 + response.Targets[1].CreationDate = resp2.GetCreationDate() + response.Targets[1].ChangeDate = resp2.GetCreationDate() + response.Targets[1].SigningKey = resp2.GetSigningKey() + + response.Targets[0].Id = resp3.GetId() + response.Targets[0].Name = name3 + response.Targets[0].CreationDate = resp3.GetCreationDate() + response.Targets[0].ChangeDate = resp3.GetCreationDate() + response.Targets[0].SigningKey = resp3.GetSigningKey() + }, + req: &action.ListTargetsRequest{ + Filters: []*action.TargetSearchFilter{{}}, + }, + }, + want: &action.ListTargetsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + Targets: []*action.Target{ + { + Endpoint: "https://example.com", + TargetType: &action.Target_RestAsync{ + RestAsync: &action.RESTAsync{}, + }, + Timeout: durationpb.New(5 * time.Second), + }, + { + Endpoint: "https://example.com", + TargetType: &action.Target_RestCall{ + RestCall: &action.RESTCall{ + InterruptOnError: true, + }, + }, + Timeout: durationpb.New(5 * time.Second), + }, + { + Endpoint: "https://example.com", + TargetType: &action.Target_RestWebhook{ + RestWebhook: &action.RESTWebhook{ + InterruptOnError: false, + }, + }, + Timeout: durationpb.New(5 * time.Second), + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.args.dep != nil { + tt.args.dep(tt.args.ctx, tt.args.req, tt.want) + } + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(isolatedIAMOwnerCTX, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, listErr := instance.Client.ActionV2.ListTargets(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(ttt, listErr, "Error: "+listErr.Error()) + return + } + require.NoError(ttt, listErr) + + // always first check length, otherwise its failed anyway + if assert.Len(ttt, got.Targets, len(tt.want.Targets)) { + for i := range tt.want.Targets { + assert.EqualExportedValues(ttt, tt.want.Targets[i], got.Targets[i]) + } + } + assertPaginationResponse(ttt, tt.want.Pagination, got.Pagination) + }, retryDuration, tick, "timeout waiting for expected execution Executions") + }) + } +} + +func assertPaginationResponse(t *assert.CollectT, expected *filter.PaginationResponse, actual *filter.PaginationResponse) { + assert.Equal(t, expected.AppliedLimit, actual.AppliedLimit) + assert.Equal(t, expected.TotalResult, actual.TotalResult) +} + +func TestServer_ListExecutions(t *testing.T) { + instance := integration.NewInstance(CTX) + isolatedIAMOwnerCTX := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false) + + type args struct { + ctx context.Context + dep func(context.Context, *action.ListExecutionsRequest, *action.ListExecutionsResponse) + req *action.ListExecutionsRequest + } + tests := []struct { + name string + args args + want *action.ListExecutionsResponse + wantErr bool + }{ + { + name: "missing permission", + args: args{ + ctx: instance.WithAuthorizationToken(context.Background(), integration.UserTypeOrgOwner), + req: &action.ListExecutionsRequest{}, + }, + wantErr: true, + }, + { + name: "list request single condition", + args: args{ + ctx: isolatedIAMOwnerCTX, + dep: func(ctx context.Context, request *action.ListExecutionsRequest, response *action.ListExecutionsResponse) { + cond := request.Filters[0].GetInConditionsFilter().GetConditions()[0] + resp := instance.SetExecution(ctx, t, cond, []string{targetResp.GetId()}) + + // Set expected response with used values for SetExecution + response.Executions[0].CreationDate = resp.GetSetDate() + response.Executions[0].ChangeDate = resp.GetSetDate() + response.Executions[0].Condition = cond + }, + req: &action.ListExecutionsRequest{ + Filters: []*action.ExecutionSearchFilter{{ + Filter: &action.ExecutionSearchFilter_InConditionsFilter{ + InConditionsFilter: &action.InConditionsFilter{ + Conditions: []*action.Condition{{ + ConditionType: &action.Condition_Request{ + Request: &action.RequestExecution{ + Condition: &action.RequestExecution_Method{ + Method: "/zitadel.session.v2.SessionService/GetSession", + }, + }, + }, + }}, + }, + }, + }}, + }, + }, + want: &action.ListExecutionsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Executions: []*action.Execution{ + { + Condition: &action.Condition{ + ConditionType: &action.Condition_Request{ + Request: &action.RequestExecution{ + Condition: &action.RequestExecution_Method{ + Method: "/zitadel.session.v2.SessionService/GetSession", + }, + }, + }, + }, + Targets: []string{targetResp.GetId()}, + }, + }, + }, + }, + { + name: "list request single target", + args: args{ + ctx: isolatedIAMOwnerCTX, + dep: func(ctx context.Context, request *action.ListExecutionsRequest, response *action.ListExecutionsResponse) { + target := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false) + // add target as Filter to the request + request.Filters[0] = &action.ExecutionSearchFilter{ + Filter: &action.ExecutionSearchFilter_TargetFilter{ + TargetFilter: &action.TargetFilter{ + TargetId: target.GetId(), + }, + }, + } + cond := &action.Condition{ + ConditionType: &action.Condition_Request{ + Request: &action.RequestExecution{ + Condition: &action.RequestExecution_Method{ + Method: "/zitadel.management.v1.ManagementService/UpdateAction", + }, + }, + }, + } + resp := instance.SetExecution(ctx, t, cond, []string{target.GetId()}) + + response.Executions[0].CreationDate = resp.GetSetDate() + response.Executions[0].ChangeDate = resp.GetSetDate() + response.Executions[0].Condition = cond + response.Executions[0].Targets = []string{target.GetId()} + }, + req: &action.ListExecutionsRequest{ + Filters: []*action.ExecutionSearchFilter{{}}, + }, + }, + want: &action.ListExecutionsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Executions: []*action.Execution{ + { + Condition: &action.Condition{}, + Targets: []string{""}, + }, + }, + }, + }, + { + name: "list multiple conditions", + args: args{ + ctx: isolatedIAMOwnerCTX, + dep: func(ctx context.Context, request *action.ListExecutionsRequest, response *action.ListExecutionsResponse) { + + request.Filters[0] = &action.ExecutionSearchFilter{ + Filter: &action.ExecutionSearchFilter_InConditionsFilter{ + InConditionsFilter: &action.InConditionsFilter{ + Conditions: []*action.Condition{ + {ConditionType: &action.Condition_Request{ + Request: &action.RequestExecution{ + Condition: &action.RequestExecution_Method{ + Method: "/zitadel.session.v2.SessionService/GetSession", + }, + }, + }}, + {ConditionType: &action.Condition_Request{ + Request: &action.RequestExecution{ + Condition: &action.RequestExecution_Method{ + Method: "/zitadel.session.v2.SessionService/CreateSession", + }, + }, + }}, + {ConditionType: &action.Condition_Request{ + Request: &action.RequestExecution{ + Condition: &action.RequestExecution_Method{ + Method: "/zitadel.session.v2.SessionService/SetSession", + }, + }, + }}, + }, + }, + }, + } + + cond1 := request.Filters[0].GetInConditionsFilter().GetConditions()[0] + resp1 := instance.SetExecution(ctx, t, cond1, []string{targetResp.GetId()}) + response.Executions[2] = &action.Execution{ + CreationDate: resp1.GetSetDate(), + ChangeDate: resp1.GetSetDate(), + Condition: cond1, + Targets: []string{targetResp.GetId()}, + } + + cond2 := request.Filters[0].GetInConditionsFilter().GetConditions()[1] + resp2 := instance.SetExecution(ctx, t, cond2, []string{targetResp.GetId()}) + response.Executions[1] = &action.Execution{ + CreationDate: resp2.GetSetDate(), + ChangeDate: resp2.GetSetDate(), + Condition: cond2, + Targets: []string{targetResp.GetId()}, + } + + cond3 := request.Filters[0].GetInConditionsFilter().GetConditions()[2] + resp3 := instance.SetExecution(ctx, t, cond3, []string{targetResp.GetId()}) + response.Executions[0] = &action.Execution{ + CreationDate: resp3.GetSetDate(), + ChangeDate: resp3.GetSetDate(), + Condition: cond3, + Targets: []string{targetResp.GetId()}, + } + }, + req: &action.ListExecutionsRequest{ + Filters: []*action.ExecutionSearchFilter{ + {}, + }, + }, + }, + want: &action.ListExecutionsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + Executions: []*action.Execution{ + {}, {}, {}, + }, + }, + }, + { + name: "list multiple conditions all types", + args: args{ + ctx: isolatedIAMOwnerCTX, + dep: func(ctx context.Context, request *action.ListExecutionsRequest, response *action.ListExecutionsResponse) { + conditions := request.Filters[0].GetInConditionsFilter().GetConditions() + for i, cond := range conditions { + resp := instance.SetExecution(ctx, t, cond, []string{targetResp.GetId()}) + response.Executions[(len(conditions)-1)-i] = &action.Execution{ + CreationDate: resp.GetSetDate(), + ChangeDate: resp.GetSetDate(), + Condition: cond, + Targets: []string{targetResp.GetId()}, + } + } + }, + req: &action.ListExecutionsRequest{ + Filters: []*action.ExecutionSearchFilter{{ + Filter: &action.ExecutionSearchFilter_InConditionsFilter{ + InConditionsFilter: &action.InConditionsFilter{ + Conditions: []*action.Condition{ + {ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Method{Method: "/zitadel.session.v2.SessionService/GetSession"}}}}, + {ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Service{Service: "zitadel.session.v2.SessionService"}}}}, + {ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_All{All: true}}}}, + {ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Method{Method: "/zitadel.session.v2.SessionService/GetSession"}}}}, + {ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Service{Service: "zitadel.session.v2.SessionService"}}}}, + {ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_All{All: true}}}}, + {ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_Event{Event: "user.added"}}}}, + {ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_Group{Group: "user"}}}}, + {ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_All{All: true}}}}, + {ConditionType: &action.Condition_Function{Function: &action.FunctionExecution{Name: "presamlresponse"}}}, + }, + }, + }, + }}, + }, + }, + want: &action.ListExecutionsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 10, + AppliedLimit: 100, + }, + Executions: []*action.Execution{ + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + }, + }, + }, + { + name: "list multiple conditions all types, sort id", + args: args{ + ctx: isolatedIAMOwnerCTX, + dep: func(ctx context.Context, request *action.ListExecutionsRequest, response *action.ListExecutionsResponse) { + conditions := request.Filters[0].GetInConditionsFilter().GetConditions() + for i, cond := range conditions { + resp := instance.SetExecution(ctx, t, cond, []string{targetResp.GetId()}) + response.Executions[i] = &action.Execution{ + CreationDate: resp.GetSetDate(), + ChangeDate: resp.GetSetDate(), + Condition: cond, + Targets: []string{targetResp.GetId()}, + } + } + }, + req: &action.ListExecutionsRequest{ + SortingColumn: gu.Ptr(action.ExecutionFieldName_EXECUTION_FIELD_NAME_ID), + Filters: []*action.ExecutionSearchFilter{{ + Filter: &action.ExecutionSearchFilter_InConditionsFilter{ + InConditionsFilter: &action.InConditionsFilter{ + Conditions: []*action.Condition{ + {ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Method{Method: "/zitadel.session.v2.SessionService/GetSession"}}}}, + {ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Service{Service: "zitadel.session.v2.SessionService"}}}}, + {ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_All{All: true}}}}, + {ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Method{Method: "/zitadel.session.v2.SessionService/GetSession"}}}}, + {ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Service{Service: "zitadel.session.v2.SessionService"}}}}, + {ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_All{All: true}}}}, + {ConditionType: &action.Condition_Function{Function: &action.FunctionExecution{Name: "presamlresponse"}}}, + {ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_Event{Event: "user.added"}}}}, + {ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_Group{Group: "user"}}}}, + {ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_All{All: true}}}}, + }, + }, + }, + }}, + }, + }, + want: &action.ListExecutionsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 10, + AppliedLimit: 100, + }, + Executions: []*action.Execution{ + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.args.dep != nil { + tt.args.dep(tt.args.ctx, tt.args.req, tt.want) + } + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(isolatedIAMOwnerCTX, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, listErr := instance.Client.ActionV2.ListExecutions(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(ttt, listErr, "Error: "+listErr.Error()) + return + } + require.NoError(ttt, listErr) + // always first check length, otherwise its failed anyway + if assert.Len(ttt, got.Executions, len(tt.want.Executions)) { + assert.EqualExportedValues(ttt, got.Executions, tt.want.Executions) + } + assertPaginationResponse(ttt, tt.want.Pagination, got.Pagination) + }, retryDuration, tick, "timeout waiting for expected execution Executions") + }) + } +} diff --git a/internal/api/grpc/action/v2/integration_test/server_test.go b/internal/api/grpc/action/v2/integration_test/server_test.go new file mode 100644 index 0000000000..07ee051c63 --- /dev/null +++ b/internal/api/grpc/action/v2/integration_test/server_test.go @@ -0,0 +1,23 @@ +//go:build integration + +package action_test + +import ( + "context" + "os" + "testing" + "time" +) + +var ( + CTX context.Context +) + +func TestMain(m *testing.M) { + os.Exit(func() int { + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) + defer cancel() + CTX = ctx + return m.Run() + }()) +} diff --git a/internal/api/grpc/action/v2/integration_test/target_test.go b/internal/api/grpc/action/v2/integration_test/target_test.go new file mode 100644 index 0000000000..5908a9d56e --- /dev/null +++ b/internal/api/grpc/action/v2/integration_test/target_test.go @@ -0,0 +1,549 @@ +//go:build integration + +package action_test + +import ( + "context" + "testing" + "time" + + "github.com/brianvoe/gofakeit/v6" + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "google.golang.org/protobuf/types/known/durationpb" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/integration" + "github.com/zitadel/zitadel/pkg/grpc/action/v2" +) + +func TestServer_CreateTarget(t *testing.T) { + instance := integration.NewInstance(CTX) + isolatedIAMOwnerCTX := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + type want struct { + id bool + creationDate bool + signingKey bool + } + alreadyExistingTargetName := gofakeit.AppName() + instance.CreateTarget(isolatedIAMOwnerCTX, t, alreadyExistingTargetName, "https://example.com", domain.TargetTypeAsync, false) + tests := []struct { + name string + ctx context.Context + req *action.CreateTargetRequest + want + wantErr bool + }{ + { + name: "missing permission", + ctx: instance.WithAuthorizationToken(context.Background(), integration.UserTypeOrgOwner), + req: &action.CreateTargetRequest{ + Name: gofakeit.Name(), + }, + wantErr: true, + }, + { + name: "empty name", + ctx: isolatedIAMOwnerCTX, + req: &action.CreateTargetRequest{ + Name: "", + }, + wantErr: true, + }, + { + name: "empty type", + ctx: isolatedIAMOwnerCTX, + req: &action.CreateTargetRequest{ + Name: gofakeit.Name(), + TargetType: nil, + }, + wantErr: true, + }, + { + name: "empty webhook url", + ctx: isolatedIAMOwnerCTX, + req: &action.CreateTargetRequest{ + Name: gofakeit.Name(), + TargetType: &action.CreateTargetRequest_RestWebhook{ + RestWebhook: &action.RESTWebhook{}, + }, + }, + wantErr: true, + }, + { + name: "empty request response url", + ctx: isolatedIAMOwnerCTX, + req: &action.CreateTargetRequest{ + Name: gofakeit.Name(), + TargetType: &action.CreateTargetRequest_RestCall{ + RestCall: &action.RESTCall{}, + }, + }, + wantErr: true, + }, + { + name: "empty timeout", + ctx: isolatedIAMOwnerCTX, + req: &action.CreateTargetRequest{ + Name: gofakeit.Name(), + Endpoint: "https://example.com", + TargetType: &action.CreateTargetRequest_RestWebhook{ + RestWebhook: &action.RESTWebhook{}, + }, + Timeout: nil, + }, + wantErr: true, + }, + { + name: "async, already existing, ok", + ctx: isolatedIAMOwnerCTX, + req: &action.CreateTargetRequest{ + Name: alreadyExistingTargetName, + Endpoint: "https://example.com", + TargetType: &action.CreateTargetRequest_RestAsync{ + RestAsync: &action.RESTAsync{}, + }, + Timeout: durationpb.New(10 * time.Second), + }, + wantErr: true, + }, + { + name: "async, ok", + ctx: isolatedIAMOwnerCTX, + req: &action.CreateTargetRequest{ + Name: gofakeit.Name(), + Endpoint: "https://example.com", + TargetType: &action.CreateTargetRequest_RestAsync{ + RestAsync: &action.RESTAsync{}, + }, + Timeout: durationpb.New(10 * time.Second), + }, + want: want{ + id: true, + creationDate: true, + signingKey: true, + }, + }, + { + name: "webhook, ok", + ctx: isolatedIAMOwnerCTX, + req: &action.CreateTargetRequest{ + Name: gofakeit.Name(), + Endpoint: "https://example.com", + TargetType: &action.CreateTargetRequest_RestWebhook{ + RestWebhook: &action.RESTWebhook{ + InterruptOnError: false, + }, + }, + Timeout: durationpb.New(10 * time.Second), + }, + want: want{ + id: true, + creationDate: true, + signingKey: true, + }, + }, + { + name: "webhook, interrupt on error, ok", + ctx: isolatedIAMOwnerCTX, + req: &action.CreateTargetRequest{ + Name: gofakeit.Name(), + Endpoint: "https://example.com", + TargetType: &action.CreateTargetRequest_RestWebhook{ + RestWebhook: &action.RESTWebhook{ + InterruptOnError: true, + }, + }, + Timeout: durationpb.New(10 * time.Second), + }, + want: want{ + id: true, + creationDate: true, + signingKey: true, + }, + }, + { + name: "call, ok", + ctx: isolatedIAMOwnerCTX, + req: &action.CreateTargetRequest{ + Name: gofakeit.Name(), + Endpoint: "https://example.com", + TargetType: &action.CreateTargetRequest_RestCall{ + RestCall: &action.RESTCall{ + InterruptOnError: false, + }, + }, + Timeout: durationpb.New(10 * time.Second), + }, + want: want{ + id: true, + creationDate: true, + signingKey: true, + }, + }, + + { + name: "call, interruptOnError, ok", + ctx: isolatedIAMOwnerCTX, + req: &action.CreateTargetRequest{ + Name: gofakeit.Name(), + Endpoint: "https://example.com", + TargetType: &action.CreateTargetRequest_RestCall{ + RestCall: &action.RESTCall{ + InterruptOnError: true, + }, + }, + Timeout: durationpb.New(10 * time.Second), + }, + want: want{ + id: true, + creationDate: true, + signingKey: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creationDate := time.Now().UTC() + got, err := instance.Client.ActionV2.CreateTarget(tt.ctx, tt.req) + changeDate := time.Now().UTC() + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertCreateTargetResponse(t, creationDate, changeDate, tt.want.creationDate, tt.want.id, tt.want.signingKey, got) + }) + } +} + +func assertCreateTargetResponse(t *testing.T, creationDate, changeDate time.Time, expectedCreationDate, expectedID, expectedSigningKey bool, actualResp *action.CreateTargetResponse) { + if expectedCreationDate { + if !changeDate.IsZero() { + assert.WithinRange(t, actualResp.GetCreationDate().AsTime(), creationDate, changeDate) + } else { + assert.WithinRange(t, actualResp.GetCreationDate().AsTime(), creationDate, time.Now().UTC()) + } + } else { + assert.Nil(t, actualResp.CreationDate) + } + + if expectedID { + assert.NotEmpty(t, actualResp.GetId()) + } else { + assert.Nil(t, actualResp.Id) + } + + if expectedSigningKey { + assert.NotEmpty(t, actualResp.GetSigningKey()) + } else { + assert.Nil(t, actualResp.SigningKey) + } +} + +func TestServer_UpdateTarget(t *testing.T) { + instance := integration.NewInstance(CTX) + isolatedIAMOwnerCTX := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + type args struct { + ctx context.Context + req *action.UpdateTargetRequest + } + type want struct { + change bool + changeDate bool + signingKey bool + } + tests := []struct { + name string + prepare func(request *action.UpdateTargetRequest) + args args + want want + wantErr bool + }{ + { + name: "missing permission", + prepare: func(request *action.UpdateTargetRequest) { + targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId() + request.Id = targetID + }, + args: args{ + ctx: instance.WithAuthorizationToken(context.Background(), integration.UserTypeOrgOwner), + req: &action.UpdateTargetRequest{ + Name: gu.Ptr(gofakeit.Name()), + }, + }, + wantErr: true, + }, + { + name: "not existing", + prepare: func(request *action.UpdateTargetRequest) { + request.Id = "notexisting" + }, + args: args{ + ctx: isolatedIAMOwnerCTX, + req: &action.UpdateTargetRequest{ + Name: gu.Ptr(gofakeit.Name()), + }, + }, + wantErr: true, + }, + { + name: "no change, ok", + prepare: func(request *action.UpdateTargetRequest) { + targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId() + request.Id = targetID + }, + args: args{ + ctx: isolatedIAMOwnerCTX, + req: &action.UpdateTargetRequest{ + Endpoint: gu.Ptr("https://example.com"), + }, + }, + want: want{ + change: false, + changeDate: true, + signingKey: false, + }, + }, + { + name: "change name, ok", + prepare: func(request *action.UpdateTargetRequest) { + targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId() + request.Id = targetID + }, + args: args{ + ctx: isolatedIAMOwnerCTX, + req: &action.UpdateTargetRequest{ + Name: gu.Ptr(gofakeit.Name()), + }, + }, + want: want{ + change: true, + changeDate: true, + signingKey: false, + }, + }, + { + name: "regenerate signingkey, ok", + prepare: func(request *action.UpdateTargetRequest) { + targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId() + request.Id = targetID + }, + args: args{ + ctx: isolatedIAMOwnerCTX, + req: &action.UpdateTargetRequest{ + ExpirationSigningKey: durationpb.New(0 * time.Second), + }, + }, + want: want{ + change: true, + changeDate: true, + signingKey: true, + }, + }, + { + name: "change type, ok", + prepare: func(request *action.UpdateTargetRequest) { + targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId() + request.Id = targetID + }, + args: args{ + ctx: isolatedIAMOwnerCTX, + req: &action.UpdateTargetRequest{ + TargetType: &action.UpdateTargetRequest_RestCall{ + RestCall: &action.RESTCall{ + InterruptOnError: true, + }, + }, + }, + }, + want: want{ + change: true, + changeDate: true, + signingKey: false, + }, + }, + { + name: "change url, ok", + prepare: func(request *action.UpdateTargetRequest) { + targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId() + request.Id = targetID + }, + args: args{ + ctx: isolatedIAMOwnerCTX, + req: &action.UpdateTargetRequest{ + Endpoint: gu.Ptr("https://example.com/hooks/new"), + }, + }, + want: want{ + change: true, + changeDate: true, + signingKey: false, + }, + }, + { + name: "change timeout, ok", + prepare: func(request *action.UpdateTargetRequest) { + targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId() + request.Id = targetID + }, + args: args{ + ctx: isolatedIAMOwnerCTX, + req: &action.UpdateTargetRequest{ + Timeout: durationpb.New(20 * time.Second), + }, + }, + want: want{ + change: true, + changeDate: true, + signingKey: false, + }, + }, + { + name: "change type async, ok", + prepare: func(request *action.UpdateTargetRequest) { + targetID := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeAsync, false).GetId() + request.Id = targetID + }, + args: args{ + ctx: isolatedIAMOwnerCTX, + req: &action.UpdateTargetRequest{ + TargetType: &action.UpdateTargetRequest_RestAsync{ + RestAsync: &action.RESTAsync{}, + }, + }, + }, + want: want{ + change: true, + changeDate: true, + signingKey: false, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creationDate := time.Now().UTC() + tt.prepare(tt.args.req) + + got, err := instance.Client.ActionV2.UpdateTarget(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(t, err) + return + } + changeDate := time.Time{} + if tt.want.change { + changeDate = time.Now().UTC() + } + assert.NoError(t, err) + assertUpdateTargetResponse(t, creationDate, changeDate, tt.want.changeDate, tt.want.signingKey, got) + }) + } +} + +func assertUpdateTargetResponse(t *testing.T, creationDate, changeDate time.Time, expectedChangeDate, expectedSigningKey bool, actualResp *action.UpdateTargetResponse) { + if expectedChangeDate { + if !changeDate.IsZero() { + assert.WithinRange(t, actualResp.GetChangeDate().AsTime(), creationDate, changeDate) + } else { + assert.WithinRange(t, actualResp.GetChangeDate().AsTime(), creationDate, time.Now().UTC()) + } + } else { + assert.Nil(t, actualResp.ChangeDate) + } + + if expectedSigningKey { + assert.NotEmpty(t, actualResp.GetSigningKey()) + } else { + assert.Nil(t, actualResp.SigningKey) + } +} + +func TestServer_DeleteTarget(t *testing.T) { + instance := integration.NewInstance(CTX) + iamOwnerCtx := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + tests := []struct { + name string + ctx context.Context + prepare func(request *action.DeleteTargetRequest) (time.Time, time.Time) + req *action.DeleteTargetRequest + wantDeletionDate bool + wantErr bool + }{ + { + name: "missing permission", + ctx: instance.WithAuthorizationToken(context.Background(), integration.UserTypeOrgOwner), + req: &action.DeleteTargetRequest{ + Id: "notexisting", + }, + wantErr: true, + }, + { + name: "empty id", + ctx: iamOwnerCtx, + req: &action.DeleteTargetRequest{ + Id: "", + }, + wantErr: true, + }, + { + name: "delete target, not existing", + ctx: iamOwnerCtx, + req: &action.DeleteTargetRequest{ + Id: "notexisting", + }, + wantDeletionDate: false, + }, + { + name: "delete target", + ctx: iamOwnerCtx, + prepare: func(request *action.DeleteTargetRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + targetID := instance.CreateTarget(iamOwnerCtx, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId() + request.Id = targetID + return creationDate, time.Time{} + }, + req: &action.DeleteTargetRequest{}, + wantDeletionDate: true, + }, + { + name: "delete target, already removed", + ctx: iamOwnerCtx, + prepare: func(request *action.DeleteTargetRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + targetID := instance.CreateTarget(iamOwnerCtx, t, "", "https://example.com", domain.TargetTypeWebhook, false).GetId() + request.Id = targetID + instance.DeleteTarget(iamOwnerCtx, t, targetID) + return creationDate, time.Now().UTC() + }, + req: &action.DeleteTargetRequest{}, + wantDeletionDate: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var creationDate, deletionDate time.Time + if tt.prepare != nil { + creationDate, deletionDate = tt.prepare(tt.req) + } + got, err := instance.Client.ActionV2.DeleteTarget(tt.ctx, tt.req) + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertDeleteTargetResponse(t, creationDate, deletionDate, tt.wantDeletionDate, got) + }) + } +} + +func assertDeleteTargetResponse(t *testing.T, creationDate, deletionDate time.Time, expectedDeletionDate bool, actualResp *action.DeleteTargetResponse) { + if expectedDeletionDate { + if !deletionDate.IsZero() { + assert.WithinRange(t, actualResp.GetDeletionDate().AsTime(), creationDate, deletionDate) + } else { + assert.WithinRange(t, actualResp.GetDeletionDate().AsTime(), creationDate, time.Now().UTC()) + } + } else { + assert.Nil(t, actualResp.DeletionDate) + } +} diff --git a/internal/api/grpc/action/v2/query.go b/internal/api/grpc/action/v2/query.go new file mode 100644 index 0000000000..c900d29d75 --- /dev/null +++ b/internal/api/grpc/action/v2/query.go @@ -0,0 +1,404 @@ +package action + +import ( + "context" + "strings" + + "connectrpc.com/connect" + "google.golang.org/protobuf/types/known/durationpb" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/api/grpc/filter/v2" + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/zerrors" + "github.com/zitadel/zitadel/pkg/grpc/action/v2" +) + +const ( + conditionIDAllSegmentCount = 0 + conditionIDRequestResponseServiceSegmentCount = 1 + conditionIDRequestResponseMethodSegmentCount = 2 + conditionIDEventGroupSegmentCount = 1 +) + +func (s *Server) GetTarget(ctx context.Context, req *connect.Request[action.GetTargetRequest]) (*connect.Response[action.GetTargetResponse], error) { + resp, err := s.query.GetTargetByID(ctx, req.Msg.GetId()) + if err != nil { + return nil, err + } + return connect.NewResponse(&action.GetTargetResponse{ + Target: targetToPb(resp), + }), nil +} + +type InstanceContext interface { + GetInstanceId() string + GetInstanceDomain() string +} + +type Context interface { + GetOwner() InstanceContext +} + +func (s *Server) ListTargets(ctx context.Context, req *connect.Request[action.ListTargetsRequest]) (*connect.Response[action.ListTargetsResponse], error) { + queries, err := s.ListTargetsRequestToModel(req.Msg) + if err != nil { + return nil, err + } + resp, err := s.query.SearchTargets(ctx, queries) + if err != nil { + return nil, err + } + return connect.NewResponse(&action.ListTargetsResponse{ + Targets: targetsToPb(resp.Targets), + Pagination: filter.QueryToPaginationPb(queries.SearchRequest, resp.SearchResponse), + }), nil +} + +func (s *Server) ListExecutions(ctx context.Context, req *connect.Request[action.ListExecutionsRequest]) (*connect.Response[action.ListExecutionsResponse], error) { + queries, err := s.ListExecutionsRequestToModel(req.Msg) + if err != nil { + return nil, err + } + resp, err := s.query.SearchExecutions(ctx, queries) + if err != nil { + return nil, err + } + return connect.NewResponse(&action.ListExecutionsResponse{ + Executions: executionsToPb(resp.Executions), + Pagination: filter.QueryToPaginationPb(queries.SearchRequest, resp.SearchResponse), + }), nil +} + +func targetsToPb(targets []*query.Target) []*action.Target { + t := make([]*action.Target, len(targets)) + for i, target := range targets { + t[i] = targetToPb(target) + } + return t +} + +func targetToPb(t *query.Target) *action.Target { + target := &action.Target{ + Id: t.ID, + Name: t.Name, + Timeout: durationpb.New(t.Timeout), + Endpoint: t.Endpoint, + SigningKey: t.SigningKey, + } + switch t.TargetType { + case domain.TargetTypeWebhook: + target.TargetType = &action.Target_RestWebhook{RestWebhook: &action.RESTWebhook{InterruptOnError: t.InterruptOnError}} + case domain.TargetTypeCall: + target.TargetType = &action.Target_RestCall{RestCall: &action.RESTCall{InterruptOnError: t.InterruptOnError}} + case domain.TargetTypeAsync: + target.TargetType = &action.Target_RestAsync{RestAsync: &action.RESTAsync{}} + default: + target.TargetType = nil + } + + if !t.EventDate.IsZero() { + target.ChangeDate = timestamppb.New(t.EventDate) + } + if !t.CreationDate.IsZero() { + target.CreationDate = timestamppb.New(t.CreationDate) + } + return target +} + +func (s *Server) ListTargetsRequestToModel(req *action.ListTargetsRequest) (*query.TargetSearchQueries, error) { + offset, limit, asc, err := filter.PaginationPbToQuery(s.systemDefaults, req.Pagination) + if err != nil { + return nil, err + } + queries, err := targetQueriesToQuery(req.Filters) + if err != nil { + return nil, err + } + return &query.TargetSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + SortingColumn: targetFieldNameToSortingColumn(req.SortingColumn), + }, + Queries: queries, + }, nil +} + +func targetQueriesToQuery(queries []*action.TargetSearchFilter) (_ []query.SearchQuery, err error) { + q := make([]query.SearchQuery, len(queries)) + for i, qry := range queries { + q[i], err = targetQueryToQuery(qry) + if err != nil { + return nil, err + } + } + return q, nil +} + +func targetQueryToQuery(filter *action.TargetSearchFilter) (query.SearchQuery, error) { + switch q := filter.Filter.(type) { + case *action.TargetSearchFilter_TargetNameFilter: + return targetNameQueryToQuery(q.TargetNameFilter) + case *action.TargetSearchFilter_InTargetIdsFilter: + return targetInTargetIdsQueryToQuery(q.InTargetIdsFilter) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "GRPC-vR9nC", "List.Query.Invalid") + } +} + +func targetNameQueryToQuery(q *action.TargetNameFilter) (query.SearchQuery, error) { + return query.NewTargetNameSearchQuery(filter.TextMethodPbToQuery(q.Method), q.GetTargetName()) +} + +func targetInTargetIdsQueryToQuery(q *action.InTargetIDsFilter) (query.SearchQuery, error) { + return query.NewTargetInIDsSearchQuery(q.GetTargetIds()) +} + +// targetFieldNameToSortingColumn defaults to the creation date because this ensures deterministic pagination +func targetFieldNameToSortingColumn(field *action.TargetFieldName) query.Column { + if field == nil { + return query.TargetColumnCreationDate + } + switch *field { + case action.TargetFieldName_TARGET_FIELD_NAME_UNSPECIFIED: + return query.TargetColumnCreationDate + case action.TargetFieldName_TARGET_FIELD_NAME_ID: + return query.TargetColumnID + case action.TargetFieldName_TARGET_FIELD_NAME_CREATED_DATE: + return query.TargetColumnCreationDate + case action.TargetFieldName_TARGET_FIELD_NAME_CHANGED_DATE: + return query.TargetColumnChangeDate + case action.TargetFieldName_TARGET_FIELD_NAME_NAME: + return query.TargetColumnName + case action.TargetFieldName_TARGET_FIELD_NAME_TARGET_TYPE: + return query.TargetColumnTargetType + case action.TargetFieldName_TARGET_FIELD_NAME_URL: + return query.TargetColumnURL + case action.TargetFieldName_TARGET_FIELD_NAME_TIMEOUT: + return query.TargetColumnTimeout + case action.TargetFieldName_TARGET_FIELD_NAME_INTERRUPT_ON_ERROR: + return query.TargetColumnInterruptOnError + default: + return query.TargetColumnCreationDate + } +} + +// executionFieldNameToSortingColumn defaults to the creation date because this ensures deterministic pagination +func executionFieldNameToSortingColumn(field *action.ExecutionFieldName) query.Column { + if field == nil { + return query.ExecutionColumnCreationDate + } + switch *field { + case action.ExecutionFieldName_EXECUTION_FIELD_NAME_UNSPECIFIED: + return query.ExecutionColumnCreationDate + case action.ExecutionFieldName_EXECUTION_FIELD_NAME_ID: + return query.ExecutionColumnID + case action.ExecutionFieldName_EXECUTION_FIELD_NAME_CREATED_DATE: + return query.ExecutionColumnCreationDate + case action.ExecutionFieldName_EXECUTION_FIELD_NAME_CHANGED_DATE: + return query.ExecutionColumnChangeDate + default: + return query.ExecutionColumnCreationDate + } +} + +func (s *Server) ListExecutionsRequestToModel(req *action.ListExecutionsRequest) (*query.ExecutionSearchQueries, error) { + offset, limit, asc, err := filter.PaginationPbToQuery(s.systemDefaults, req.Pagination) + if err != nil { + return nil, err + } + queries, err := executionQueriesToQuery(req.Filters) + if err != nil { + return nil, err + } + return &query.ExecutionSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + SortingColumn: executionFieldNameToSortingColumn(req.SortingColumn), + }, + Queries: queries, + }, nil +} + +func executionQueriesToQuery(queries []*action.ExecutionSearchFilter) (_ []query.SearchQuery, err error) { + q := make([]query.SearchQuery, len(queries)) + for i, query := range queries { + q[i], err = executionQueryToQuery(query) + if err != nil { + return nil, err + } + } + return q, nil +} + +func executionQueryToQuery(searchQuery *action.ExecutionSearchFilter) (query.SearchQuery, error) { + switch q := searchQuery.Filter.(type) { + case *action.ExecutionSearchFilter_InConditionsFilter: + return inConditionsQueryToQuery(q.InConditionsFilter) + case *action.ExecutionSearchFilter_ExecutionTypeFilter: + return executionTypeToQuery(q.ExecutionTypeFilter) + case *action.ExecutionSearchFilter_TargetFilter: + return query.NewTargetSearchQuery(q.TargetFilter.GetTargetId()) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "GRPC-vR9nC", "List.Query.Invalid") + } +} + +func executionTypeToQuery(q *action.ExecutionTypeFilter) (query.SearchQuery, error) { + switch q.ExecutionType { + case action.ExecutionType_EXECUTION_TYPE_UNSPECIFIED: + return query.NewExecutionTypeSearchQuery(domain.ExecutionTypeUnspecified) + case action.ExecutionType_EXECUTION_TYPE_REQUEST: + return query.NewExecutionTypeSearchQuery(domain.ExecutionTypeRequest) + case action.ExecutionType_EXECUTION_TYPE_RESPONSE: + return query.NewExecutionTypeSearchQuery(domain.ExecutionTypeResponse) + case action.ExecutionType_EXECUTION_TYPE_EVENT: + return query.NewExecutionTypeSearchQuery(domain.ExecutionTypeEvent) + case action.ExecutionType_EXECUTION_TYPE_FUNCTION: + return query.NewExecutionTypeSearchQuery(domain.ExecutionTypeFunction) + default: + return query.NewExecutionTypeSearchQuery(domain.ExecutionTypeUnspecified) + } +} + +func inConditionsQueryToQuery(q *action.InConditionsFilter) (query.SearchQuery, error) { + values := make([]string, len(q.GetConditions())) + for i, condition := range q.GetConditions() { + id, err := conditionToID(condition) + if err != nil { + return nil, err + } + values[i] = id + } + return query.NewExecutionInIDsSearchQuery(values) +} + +func conditionToID(q *action.Condition) (string, error) { + switch t := q.GetConditionType().(type) { + case *action.Condition_Request: + cond := &command.ExecutionAPICondition{ + Method: t.Request.GetMethod(), + Service: t.Request.GetService(), + All: t.Request.GetAll(), + } + return cond.ID(domain.ExecutionTypeRequest), nil + case *action.Condition_Response: + cond := &command.ExecutionAPICondition{ + Method: t.Response.GetMethod(), + Service: t.Response.GetService(), + All: t.Response.GetAll(), + } + return cond.ID(domain.ExecutionTypeResponse), nil + case *action.Condition_Event: + cond := &command.ExecutionEventCondition{ + Event: t.Event.GetEvent(), + Group: t.Event.GetGroup(), + All: t.Event.GetAll(), + } + return cond.ID(), nil + case *action.Condition_Function: + return command.ExecutionFunctionCondition(t.Function.GetName()).ID(), nil + default: + return "", zerrors.ThrowInvalidArgument(nil, "GRPC-vR9nC", "List.Query.Invalid") + } +} + +func executionsToPb(executions []*query.Execution) []*action.Execution { + e := make([]*action.Execution, len(executions)) + for i, execution := range executions { + e[i] = executionToPb(execution) + } + return e +} + +func executionToPb(e *query.Execution) *action.Execution { + targets := make([]string, len(e.Targets)) + for i := range e.Targets { + switch e.Targets[i].Type { + case domain.ExecutionTargetTypeTarget: + targets[i] = e.Targets[i].Target + case domain.ExecutionTargetTypeInclude, domain.ExecutionTargetTypeUnspecified: + continue + default: + continue + } + } + + exec := &action.Execution{ + Condition: executionIDToCondition(e.ID), + Targets: targets, + } + if !e.EventDate.IsZero() { + exec.ChangeDate = timestamppb.New(e.EventDate) + } + if !e.CreationDate.IsZero() { + exec.CreationDate = timestamppb.New(e.CreationDate) + } + return exec +} + +func executionIDToCondition(include string) *action.Condition { + if strings.HasPrefix(include, domain.ExecutionTypeRequest.String()) { + return includeRequestToCondition(strings.TrimPrefix(include, domain.ExecutionTypeRequest.String())) + } + if strings.HasPrefix(include, domain.ExecutionTypeResponse.String()) { + return includeResponseToCondition(strings.TrimPrefix(include, domain.ExecutionTypeResponse.String())) + } + if strings.HasPrefix(include, domain.ExecutionTypeEvent.String()) { + return includeEventToCondition(strings.TrimPrefix(include, domain.ExecutionTypeEvent.String())) + } + if strings.HasPrefix(include, domain.ExecutionTypeFunction.String()) { + return includeFunctionToCondition(strings.TrimPrefix(include, domain.ExecutionTypeFunction.String())) + } + return nil +} + +func includeRequestToCondition(id string) *action.Condition { + switch strings.Count(id, "/") { + case conditionIDRequestResponseMethodSegmentCount: + return &action.Condition{ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Method{Method: id}}}} + case conditionIDRequestResponseServiceSegmentCount: + return &action.Condition{ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_Service{Service: strings.TrimPrefix(id, "/")}}}} + case conditionIDAllSegmentCount: + return &action.Condition{ConditionType: &action.Condition_Request{Request: &action.RequestExecution{Condition: &action.RequestExecution_All{All: true}}}} + default: + return nil + } +} +func includeResponseToCondition(id string) *action.Condition { + switch strings.Count(id, "/") { + case conditionIDRequestResponseMethodSegmentCount: + return &action.Condition{ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Method{Method: id}}}} + case conditionIDRequestResponseServiceSegmentCount: + return &action.Condition{ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_Service{Service: strings.TrimPrefix(id, "/")}}}} + case conditionIDAllSegmentCount: + return &action.Condition{ConditionType: &action.Condition_Response{Response: &action.ResponseExecution{Condition: &action.ResponseExecution_All{All: true}}}} + default: + return nil + } +} + +func includeEventToCondition(id string) *action.Condition { + switch strings.Count(id, "/") { + case conditionIDEventGroupSegmentCount: + if strings.HasSuffix(id, command.EventGroupSuffix) { + return &action.Condition{ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_Group{Group: strings.TrimSuffix(strings.TrimPrefix(id, "/"), command.EventGroupSuffix)}}}} + } else { + return &action.Condition{ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_Event{Event: strings.TrimPrefix(id, "/")}}}} + } + case conditionIDAllSegmentCount: + return &action.Condition{ConditionType: &action.Condition_Event{Event: &action.EventExecution{Condition: &action.EventExecution_All{All: true}}}} + default: + return nil + } +} + +func includeFunctionToCondition(id string) *action.Condition { + return &action.Condition{ConditionType: &action.Condition_Function{Function: &action.FunctionExecution{Name: strings.TrimPrefix(id, "/")}}} +} diff --git a/internal/api/grpc/action/v2/server.go b/internal/api/grpc/action/v2/server.go new file mode 100644 index 0000000000..b94e43ef6b --- /dev/null +++ b/internal/api/grpc/action/v2/server.go @@ -0,0 +1,71 @@ +package action + +import ( + "net/http" + + "connectrpc.com/connect" + "google.golang.org/protobuf/reflect/protoreflect" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/api/grpc/server" + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/config/systemdefaults" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/pkg/grpc/action/v2" + "github.com/zitadel/zitadel/pkg/grpc/action/v2/actionconnect" +) + +var _ actionconnect.ActionServiceHandler = (*Server)(nil) + +type Server struct { + systemDefaults systemdefaults.SystemDefaults + command *command.Commands + query *query.Queries + ListActionFunctions func() []string + ListGRPCMethods func() []string + ListGRPCServices func() []string +} + +type Config struct{} + +func CreateServer( + systemDefaults systemdefaults.SystemDefaults, + command *command.Commands, + query *query.Queries, + listActionFunctions func() []string, + listGRPCMethods func() []string, + listGRPCServices func() []string, +) *Server { + return &Server{ + systemDefaults: systemDefaults, + command: command, + query: query, + ListActionFunctions: listActionFunctions, + ListGRPCMethods: listGRPCMethods, + ListGRPCServices: listGRPCServices, + } +} + +func (s *Server) RegisterConnectServer(interceptors ...connect.Interceptor) (string, http.Handler) { + return actionconnect.NewActionServiceHandler(s, connect.WithInterceptors(interceptors...)) +} + +func (s *Server) FileDescriptor() protoreflect.FileDescriptor { + return action.File_zitadel_action_v2_action_service_proto +} + +func (s *Server) AppName() string { + return action.ActionService_ServiceDesc.ServiceName +} + +func (s *Server) MethodPrefix() string { + return action.ActionService_ServiceDesc.ServiceName +} + +func (s *Server) AuthMethods() authz.MethodMapping { + return action.ActionService_AuthMethods +} + +func (s *Server) RegisterGateway() server.RegisterGatewayFunc { + return action.RegisterActionServiceHandler +} diff --git a/internal/api/grpc/action/v2/target.go b/internal/api/grpc/action/v2/target.go new file mode 100644 index 0000000000..971a6b871e --- /dev/null +++ b/internal/api/grpc/action/v2/target.go @@ -0,0 +1,123 @@ +package action + +import ( + "context" + + "connectrpc.com/connect" + "github.com/muhlemmer/gu" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore/v1/models" + "github.com/zitadel/zitadel/pkg/grpc/action/v2" +) + +func (s *Server) CreateTarget(ctx context.Context, req *connect.Request[action.CreateTargetRequest]) (*connect.Response[action.CreateTargetResponse], error) { + add := createTargetToCommand(req.Msg) + instanceID := authz.GetInstance(ctx).InstanceID() + createdAt, err := s.command.AddTarget(ctx, add, instanceID) + if err != nil { + return nil, err + } + var creationDate *timestamppb.Timestamp + if !createdAt.IsZero() { + creationDate = timestamppb.New(createdAt) + } + return connect.NewResponse(&action.CreateTargetResponse{ + Id: add.AggregateID, + CreationDate: creationDate, + SigningKey: add.SigningKey, + }), nil +} + +func (s *Server) UpdateTarget(ctx context.Context, req *connect.Request[action.UpdateTargetRequest]) (*connect.Response[action.UpdateTargetResponse], error) { + instanceID := authz.GetInstance(ctx).InstanceID() + update := updateTargetToCommand(req.Msg) + changedAt, err := s.command.ChangeTarget(ctx, update, instanceID) + if err != nil { + return nil, err + } + var changeDate *timestamppb.Timestamp + if !changedAt.IsZero() { + changeDate = timestamppb.New(changedAt) + } + return connect.NewResponse(&action.UpdateTargetResponse{ + ChangeDate: changeDate, + SigningKey: update.SigningKey, + }), nil +} + +func (s *Server) DeleteTarget(ctx context.Context, req *connect.Request[action.DeleteTargetRequest]) (*connect.Response[action.DeleteTargetResponse], error) { + instanceID := authz.GetInstance(ctx).InstanceID() + deletedAt, err := s.command.DeleteTarget(ctx, req.Msg.GetId(), instanceID) + if err != nil { + return nil, err + } + var deletionDate *timestamppb.Timestamp + if !deletedAt.IsZero() { + deletionDate = timestamppb.New(deletedAt) + } + return connect.NewResponse(&action.DeleteTargetResponse{ + DeletionDate: deletionDate, + }), nil +} + +func createTargetToCommand(req *action.CreateTargetRequest) *command.AddTarget { + var ( + targetType domain.TargetType + interruptOnError bool + ) + switch t := req.GetTargetType().(type) { + case *action.CreateTargetRequest_RestWebhook: + targetType = domain.TargetTypeWebhook + interruptOnError = t.RestWebhook.InterruptOnError + case *action.CreateTargetRequest_RestCall: + targetType = domain.TargetTypeCall + interruptOnError = t.RestCall.InterruptOnError + case *action.CreateTargetRequest_RestAsync: + targetType = domain.TargetTypeAsync + } + return &command.AddTarget{ + Name: req.GetName(), + TargetType: targetType, + Endpoint: req.GetEndpoint(), + Timeout: req.GetTimeout().AsDuration(), + InterruptOnError: interruptOnError, + } +} + +func updateTargetToCommand(req *action.UpdateTargetRequest) *command.ChangeTarget { + // TODO handle expiration, currently only immediate expiration is supported + expirationSigningKey := req.GetExpirationSigningKey() != nil + + if req == nil { + return nil + } + target := &command.ChangeTarget{ + ObjectRoot: models.ObjectRoot{ + AggregateID: req.GetId(), + }, + Name: req.Name, + Endpoint: req.Endpoint, + ExpirationSigningKey: expirationSigningKey, + } + if req.TargetType != nil { + switch t := req.GetTargetType().(type) { + case *action.UpdateTargetRequest_RestWebhook: + target.TargetType = gu.Ptr(domain.TargetTypeWebhook) + target.InterruptOnError = gu.Ptr(t.RestWebhook.InterruptOnError) + case *action.UpdateTargetRequest_RestCall: + target.TargetType = gu.Ptr(domain.TargetTypeCall) + target.InterruptOnError = gu.Ptr(t.RestCall.InterruptOnError) + case *action.UpdateTargetRequest_RestAsync: + target.TargetType = gu.Ptr(domain.TargetTypeAsync) + target.InterruptOnError = gu.Ptr(false) + } + } + if req.Timeout != nil { + target.Timeout = gu.Ptr(req.GetTimeout().AsDuration()) + } + return target +} diff --git a/internal/api/grpc/action/v2/target_test.go b/internal/api/grpc/action/v2/target_test.go new file mode 100644 index 0000000000..f41932933a --- /dev/null +++ b/internal/api/grpc/action/v2/target_test.go @@ -0,0 +1,229 @@ +package action + +import ( + "testing" + "time" + + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "google.golang.org/protobuf/types/known/durationpb" + + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/pkg/grpc/action/v2" +) + +func Test_createTargetToCommand(t *testing.T) { + type args struct { + req *action.CreateTargetRequest + } + tests := []struct { + name string + args args + want *command.AddTarget + }{ + { + name: "nil", + args: args{nil}, + want: &command.AddTarget{ + Name: "", + Endpoint: "", + Timeout: 0, + InterruptOnError: false, + }, + }, + { + name: "all fields (webhook)", + args: args{&action.CreateTargetRequest{ + Name: "target 1", + Endpoint: "https://example.com/hooks/1", + TargetType: &action.CreateTargetRequest_RestWebhook{ + RestWebhook: &action.RESTWebhook{}, + }, + Timeout: durationpb.New(10 * time.Second), + }}, + want: &command.AddTarget{ + Name: "target 1", + TargetType: domain.TargetTypeWebhook, + Endpoint: "https://example.com/hooks/1", + Timeout: 10 * time.Second, + InterruptOnError: false, + }, + }, + { + name: "all fields (async)", + args: args{&action.CreateTargetRequest{ + Name: "target 1", + Endpoint: "https://example.com/hooks/1", + TargetType: &action.CreateTargetRequest_RestAsync{ + RestAsync: &action.RESTAsync{}, + }, + Timeout: durationpb.New(10 * time.Second), + }}, + want: &command.AddTarget{ + Name: "target 1", + TargetType: domain.TargetTypeAsync, + Endpoint: "https://example.com/hooks/1", + Timeout: 10 * time.Second, + InterruptOnError: false, + }, + }, + { + name: "all fields (interrupting response)", + args: args{&action.CreateTargetRequest{ + Name: "target 1", + Endpoint: "https://example.com/hooks/1", + TargetType: &action.CreateTargetRequest_RestCall{ + RestCall: &action.RESTCall{ + InterruptOnError: true, + }, + }, + Timeout: durationpb.New(10 * time.Second), + }}, + want: &command.AddTarget{ + Name: "target 1", + TargetType: domain.TargetTypeCall, + Endpoint: "https://example.com/hooks/1", + Timeout: 10 * time.Second, + InterruptOnError: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := createTargetToCommand(tt.args.req) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_updateTargetToCommand(t *testing.T) { + type args struct { + req *action.UpdateTargetRequest + } + tests := []struct { + name string + args args + want *command.ChangeTarget + }{ + { + name: "nil", + args: args{nil}, + want: nil, + }, + { + name: "all fields nil", + args: args{&action.UpdateTargetRequest{ + Name: nil, + TargetType: nil, + Timeout: nil, + }}, + want: &command.ChangeTarget{ + Name: nil, + TargetType: nil, + Endpoint: nil, + Timeout: nil, + InterruptOnError: nil, + }, + }, + { + name: "all fields empty", + args: args{&action.UpdateTargetRequest{ + Name: gu.Ptr(""), + TargetType: nil, + Timeout: durationpb.New(0), + }}, + want: &command.ChangeTarget{ + Name: gu.Ptr(""), + TargetType: nil, + Endpoint: nil, + Timeout: gu.Ptr(0 * time.Second), + InterruptOnError: nil, + }, + }, + { + name: "all fields (webhook)", + args: args{&action.UpdateTargetRequest{ + Name: gu.Ptr("target 1"), + Endpoint: gu.Ptr("https://example.com/hooks/1"), + TargetType: &action.UpdateTargetRequest_RestWebhook{ + RestWebhook: &action.RESTWebhook{ + InterruptOnError: false, + }, + }, + Timeout: durationpb.New(10 * time.Second), + }}, + want: &command.ChangeTarget{ + Name: gu.Ptr("target 1"), + TargetType: gu.Ptr(domain.TargetTypeWebhook), + Endpoint: gu.Ptr("https://example.com/hooks/1"), + Timeout: gu.Ptr(10 * time.Second), + InterruptOnError: gu.Ptr(false), + }, + }, + { + name: "all fields (webhook interrupt)", + args: args{&action.UpdateTargetRequest{ + Name: gu.Ptr("target 1"), + Endpoint: gu.Ptr("https://example.com/hooks/1"), + TargetType: &action.UpdateTargetRequest_RestWebhook{ + RestWebhook: &action.RESTWebhook{ + InterruptOnError: true, + }, + }, + Timeout: durationpb.New(10 * time.Second), + }}, + want: &command.ChangeTarget{ + Name: gu.Ptr("target 1"), + TargetType: gu.Ptr(domain.TargetTypeWebhook), + Endpoint: gu.Ptr("https://example.com/hooks/1"), + Timeout: gu.Ptr(10 * time.Second), + InterruptOnError: gu.Ptr(true), + }, + }, + { + name: "all fields (async)", + args: args{&action.UpdateTargetRequest{ + Name: gu.Ptr("target 1"), + Endpoint: gu.Ptr("https://example.com/hooks/1"), + TargetType: &action.UpdateTargetRequest_RestAsync{ + RestAsync: &action.RESTAsync{}, + }, + Timeout: durationpb.New(10 * time.Second), + }}, + want: &command.ChangeTarget{ + Name: gu.Ptr("target 1"), + TargetType: gu.Ptr(domain.TargetTypeAsync), + Endpoint: gu.Ptr("https://example.com/hooks/1"), + Timeout: gu.Ptr(10 * time.Second), + InterruptOnError: gu.Ptr(false), + }, + }, + { + name: "all fields (interrupting response)", + args: args{&action.UpdateTargetRequest{ + Name: gu.Ptr("target 1"), + Endpoint: gu.Ptr("https://example.com/hooks/1"), + TargetType: &action.UpdateTargetRequest_RestCall{ + RestCall: &action.RESTCall{ + InterruptOnError: true, + }, + }, + Timeout: durationpb.New(10 * time.Second), + }}, + want: &command.ChangeTarget{ + Name: gu.Ptr("target 1"), + TargetType: gu.Ptr(domain.TargetTypeCall), + Endpoint: gu.Ptr("https://example.com/hooks/1"), + Timeout: gu.Ptr(10 * time.Second), + InterruptOnError: gu.Ptr(true), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := updateTargetToCommand(tt.args.req) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/internal/api/grpc/action/v2beta/execution.go b/internal/api/grpc/action/v2beta/execution.go index 5477a8128e..3b49ebb364 100644 --- a/internal/api/grpc/action/v2beta/execution.go +++ b/internal/api/grpc/action/v2beta/execution.go @@ -3,6 +3,7 @@ package action import ( "context" + "connectrpc.com/connect" "google.golang.org/protobuf/types/known/timestamppb" "github.com/zitadel/zitadel/internal/api/authz" @@ -13,8 +14,8 @@ import ( action "github.com/zitadel/zitadel/pkg/grpc/action/v2beta" ) -func (s *Server) SetExecution(ctx context.Context, req *action.SetExecutionRequest) (*action.SetExecutionResponse, error) { - reqTargets := req.GetTargets() +func (s *Server) SetExecution(ctx context.Context, req *connect.Request[action.SetExecutionRequest]) (*connect.Response[action.SetExecutionResponse], error) { + reqTargets := req.Msg.GetTargets() targets := make([]*execution.Target, len(reqTargets)) for i, target := range reqTargets { targets[i] = &execution.Target{Type: domain.ExecutionTargetTypeTarget, Target: target} @@ -25,7 +26,7 @@ func (s *Server) SetExecution(ctx context.Context, req *action.SetExecutionReque var err error var details *domain.ObjectDetails instanceID := authz.GetInstance(ctx).InstanceID() - switch t := req.GetCondition().GetConditionType().(type) { + switch t := req.Msg.GetCondition().GetConditionType().(type) { case *action.Condition_Request: cond := executionConditionFromRequest(t.Request) details, err = s.command.SetExecutionRequest(ctx, cond, set, instanceID) @@ -43,27 +44,27 @@ func (s *Server) SetExecution(ctx context.Context, req *action.SetExecutionReque if err != nil { return nil, err } - return &action.SetExecutionResponse{ + return connect.NewResponse(&action.SetExecutionResponse{ SetDate: timestamppb.New(details.EventDate), - }, nil + }), nil } -func (s *Server) ListExecutionFunctions(ctx context.Context, _ *action.ListExecutionFunctionsRequest) (*action.ListExecutionFunctionsResponse, error) { - return &action.ListExecutionFunctionsResponse{ +func (s *Server) ListExecutionFunctions(ctx context.Context, _ *connect.Request[action.ListExecutionFunctionsRequest]) (*connect.Response[action.ListExecutionFunctionsResponse], error) { + return connect.NewResponse(&action.ListExecutionFunctionsResponse{ Functions: s.ListActionFunctions(), - }, nil + }), nil } -func (s *Server) ListExecutionMethods(ctx context.Context, _ *action.ListExecutionMethodsRequest) (*action.ListExecutionMethodsResponse, error) { - return &action.ListExecutionMethodsResponse{ +func (s *Server) ListExecutionMethods(ctx context.Context, _ *connect.Request[action.ListExecutionMethodsRequest]) (*connect.Response[action.ListExecutionMethodsResponse], error) { + return connect.NewResponse(&action.ListExecutionMethodsResponse{ Methods: s.ListGRPCMethods(), - }, nil + }), nil } -func (s *Server) ListExecutionServices(ctx context.Context, _ *action.ListExecutionServicesRequest) (*action.ListExecutionServicesResponse, error) { - return &action.ListExecutionServicesResponse{ +func (s *Server) ListExecutionServices(ctx context.Context, _ *connect.Request[action.ListExecutionServicesRequest]) (*connect.Response[action.ListExecutionServicesResponse], error) { + return connect.NewResponse(&action.ListExecutionServicesResponse{ Services: s.ListGRPCServices(), - }, nil + }), nil } func executionConditionFromRequest(request *action.RequestExecution) *command.ExecutionAPICondition { diff --git a/internal/api/grpc/action/v2beta/integration_test/execution_target_test.go b/internal/api/grpc/action/v2beta/integration_test/execution_target_test.go index 0c5018dbb6..4db254fe30 100644 --- a/internal/api/grpc/action/v2beta/integration_test/execution_target_test.go +++ b/internal/api/grpc/action/v2beta/integration_test/execution_target_test.go @@ -48,7 +48,7 @@ var ( func TestServer_ExecutionTarget(t *testing.T) { instance := integration.NewInstance(CTX) - isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + isolatedIAMOwnerCTX := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) fullMethod := action.ActionService_GetTarget_FullMethodName tests := []struct { @@ -164,8 +164,8 @@ func TestServer_ExecutionTarget(t *testing.T) { } }, clean: func(ctx context.Context) { - instance.DeleteExecution(ctx, t, conditionRequestFullMethod(fullMethod)) - instance.DeleteExecution(ctx, t, conditionResponseFullMethod(fullMethod)) + deleteExecution(ctx, t, instance, conditionRequestFullMethod(fullMethod)) + deleteExecution(ctx, t, instance, conditionResponseFullMethod(fullMethod)) }, req: &action.GetTargetRequest{ Id: "something", @@ -197,7 +197,7 @@ func TestServer_ExecutionTarget(t *testing.T) { } }, clean: func(ctx context.Context) { - instance.DeleteExecution(ctx, t, conditionRequestFullMethod(fullMethod)) + deleteExecution(ctx, t, instance, conditionRequestFullMethod(fullMethod)) }, req: &action.GetTargetRequest{}, wantErr: true, @@ -259,7 +259,7 @@ func TestServer_ExecutionTarget(t *testing.T) { } }, clean: func(ctx context.Context) { - instance.DeleteExecution(ctx, t, conditionResponseFullMethod(fullMethod)) + deleteExecution(ctx, t, instance, conditionResponseFullMethod(fullMethod)) }, req: &action.GetTargetRequest{}, wantErr: true, @@ -290,9 +290,16 @@ func TestServer_ExecutionTarget(t *testing.T) { } } +func deleteExecution(ctx context.Context, t *testing.T, instance *integration.Instance, cond *action.Condition) { + _, err := instance.Client.ActionV2beta.SetExecution(ctx, &action.SetExecutionRequest{ + Condition: cond, + }) + require.NoError(t, err) +} + func TestServer_ExecutionTarget_Event(t *testing.T) { instance := integration.NewInstance(CTX) - isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + isolatedIAMOwnerCTX := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) event := "session.added" urlRequest, closeF, calledF, resetF := integration.TestServerCall(nil, 0, http.StatusOK, nil) @@ -349,7 +356,7 @@ func TestServer_ExecutionTarget_Event(t *testing.T) { func TestServer_ExecutionTarget_Event_LongerThanTargetTimeout(t *testing.T) { instance := integration.NewInstance(CTX) - isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + isolatedIAMOwnerCTX := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) event := "session.added" // call takes longer than timeout of target @@ -401,7 +408,7 @@ func TestServer_ExecutionTarget_Event_LongerThanTargetTimeout(t *testing.T) { func TestServer_ExecutionTarget_Event_LongerThanTransactionTimeout(t *testing.T) { instance := integration.NewInstance(CTX) - isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + isolatedIAMOwnerCTX := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) event := "session.added" urlRequest, closeF, calledF, resetF := integration.TestServerCall(nil, 1*time.Second, http.StatusOK, nil) @@ -463,7 +470,7 @@ func TestServer_ExecutionTarget_Event_LongerThanTransactionTimeout(t *testing.T) } func waitForExecutionOnCondition(ctx context.Context, t *testing.T, instance *integration.Instance, condition *action.Condition, targets []string) { - instance.SetExecution(ctx, t, condition, targets) + setExecution(ctx, t, instance, condition, targets) retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, time.Minute) require.EventuallyWithT(t, func(ttt *assert.CollectT) { @@ -477,10 +484,10 @@ func waitForExecutionOnCondition(ctx context.Context, t *testing.T, instance *in if !assert.NoError(ttt, err) { return } - if !assert.Len(ttt, got.GetResult(), 1) { + if !assert.Len(ttt, got.GetExecutions(), 1) { return } - gotTargets := got.GetResult()[0].GetTargets() + gotTargets := got.GetExecutions()[0].GetTargets() // always first check length, otherwise its failed anyway if assert.Len(ttt, gotTargets, len(targets)) { for i := range targets { @@ -488,11 +495,19 @@ func waitForExecutionOnCondition(ctx context.Context, t *testing.T, instance *in } } }, retryDuration, tick, "timeout waiting for expected execution result") - return +} + +func setExecution(ctx context.Context, t *testing.T, instance *integration.Instance, cond *action.Condition, targets []string) *action.SetExecutionResponse { + target, err := instance.Client.ActionV2beta.SetExecution(ctx, &action.SetExecutionRequest{ + Condition: cond, + Targets: targets, + }) + require.NoError(t, err) + return target } func waitForTarget(ctx context.Context, t *testing.T, instance *integration.Instance, endpoint string, ty domain.TargetType, interrupt bool) *action.CreateTargetResponse { - resp := instance.CreateTarget(ctx, t, "", endpoint, ty, interrupt) + resp := createTarget(ctx, t, instance, "", endpoint, ty, interrupt) retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, time.Minute) require.EventuallyWithT(t, func(ttt *assert.CollectT) { @@ -506,10 +521,10 @@ func waitForTarget(ctx context.Context, t *testing.T, instance *integration.Inst if !assert.NoError(ttt, err) { return } - if !assert.Len(ttt, got.GetResult(), 1) { + if !assert.Len(ttt, got.GetTargets(), 1) { return } - config := got.GetResult()[0] + config := got.GetTargets()[0] assert.Equal(ttt, config.GetEndpoint(), endpoint) switch ty { case domain.TargetTypeWebhook: @@ -529,6 +544,38 @@ func waitForTarget(ctx context.Context, t *testing.T, instance *integration.Inst return resp } +func createTarget(ctx context.Context, t *testing.T, instance *integration.Instance, name, endpoint string, ty domain.TargetType, interrupt bool) *action.CreateTargetResponse { + if name == "" { + name = gofakeit.Name() + } + req := &action.CreateTargetRequest{ + Name: name, + Endpoint: endpoint, + Timeout: durationpb.New(5 * time.Second), + } + switch ty { + case domain.TargetTypeWebhook: + req.TargetType = &action.CreateTargetRequest_RestWebhook{ + RestWebhook: &action.RESTWebhook{ + InterruptOnError: interrupt, + }, + } + case domain.TargetTypeCall: + req.TargetType = &action.CreateTargetRequest_RestCall{ + RestCall: &action.RESTCall{ + InterruptOnError: interrupt, + }, + } + case domain.TargetTypeAsync: + req.TargetType = &action.CreateTargetRequest_RestAsync{ + RestAsync: &action.RESTAsync{}, + } + } + target, err := instance.Client.ActionV2beta.CreateTarget(ctx, req) + require.NoError(t, err) + return target +} + func conditionRequestFullMethod(fullMethod string) *action.Condition { return &action.Condition{ ConditionType: &action.Condition_Request{ @@ -577,10 +624,10 @@ func conditionFunction(function string) *action.Condition { func TestServer_ExecutionTargetPreUserinfo(t *testing.T) { instance := integration.NewInstance(CTX) - isolatedIAMCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) - ctxLoginClient := instance.WithAuthorization(CTX, integration.UserTypeLogin) + isolatedIAMCtx := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + ctxLoginClient := instance.WithAuthorizationToken(CTX, integration.UserTypeLogin) - client, err := instance.CreateOIDCImplicitFlowClient(isolatedIAMCtx, redirectURIImplicit, loginV2) + client, err := instance.CreateOIDCImplicitFlowClient(isolatedIAMCtx, t, redirectURIImplicit, loginV2) require.NoError(t, err) type want struct { @@ -605,7 +652,7 @@ func TestServer_ExecutionTargetPreUserinfo(t *testing.T) { {Key: "added", Value: "value"}, }, } - return expectPreUserinfoExecution(ctx, t, instance, req, response) + return expectPreUserinfoExecution(ctx, t, instance, client.GetClientId(), req, response) }, req: &oidc_pb.CreateCallbackRequest{ AuthRequestId: func() string { @@ -630,7 +677,7 @@ func TestServer_ExecutionTargetPreUserinfo(t *testing.T) { "addedLog", }, } - return expectPreUserinfoExecution(ctx, t, instance, req, response) + return expectPreUserinfoExecution(ctx, t, instance, client.GetClientId(), req, response) }, req: &oidc_pb.CreateCallbackRequest{ AuthRequestId: func() string { @@ -655,7 +702,7 @@ func TestServer_ExecutionTargetPreUserinfo(t *testing.T) { {Key: "key", Value: []byte("value")}, }, } - return expectPreUserinfoExecution(ctx, t, instance, req, response) + return expectPreUserinfoExecution(ctx, t, instance, client.GetClientId(), req, response) }, req: &oidc_pb.CreateCallbackRequest{ AuthRequestId: func() string { @@ -692,7 +739,7 @@ func TestServer_ExecutionTargetPreUserinfo(t *testing.T) { {Key: "added3", Value: "value3"}, }, } - return expectPreUserinfoExecution(ctx, t, instance, req, response) + return expectPreUserinfoExecution(ctx, t, instance, client.GetClientId(), req, response) }, req: &oidc_pb.CreateCallbackRequest{ AuthRequestId: func() string { @@ -755,7 +802,7 @@ func TestServer_ExecutionTargetPreUserinfo(t *testing.T) { } } -func expectPreUserinfoExecution(ctx context.Context, t *testing.T, instance *integration.Instance, req *oidc_pb.CreateCallbackRequest, response *oidc_api.ContextInfoResponse) (string, func()) { +func expectPreUserinfoExecution(ctx context.Context, t *testing.T, instance *integration.Instance, clientID string, req *oidc_pb.CreateCallbackRequest, response *oidc_api.ContextInfoResponse) (string, func()) { userEmail := gofakeit.Email() userPhone := "+41" + gofakeit.Phone() userResp := instance.CreateHumanUserVerified(ctx, instance.DefaultOrg.Id, userEmail, userPhone) @@ -767,7 +814,7 @@ func expectPreUserinfoExecution(ctx context.Context, t *testing.T, instance *int SessionToken: sessionResp.GetSessionToken(), }, } - expectedContextInfo := contextInfoForUserOIDC(instance, "function/preuserinfo", userResp, userEmail, userPhone) + expectedContextInfo := contextInfoForUserOIDC(instance, "function/preuserinfo", clientID, userResp, userEmail, userPhone) targetURL, closeF, _, _ := integration.TestServerCall(expectedContextInfo, 0, http.StatusOK, response) @@ -845,7 +892,7 @@ func getAccessTokenClaims(ctx context.Context, t *testing.T, instance *integrati return claims } -func contextInfoForUserOIDC(instance *integration.Instance, function string, userResp *user.AddHumanUserResponse, email, phone string) *oidc_api.ContextInfo { +func contextInfoForUserOIDC(instance *integration.Instance, function string, clientID string, userResp *user.AddHumanUserResponse, email, phone string) *oidc_api.ContextInfo { return &oidc_api.ContextInfo{ Function: function, UserInfo: &oidc.UserInfo{ @@ -878,6 +925,9 @@ func contextInfoForUserOIDC(instance *integration.Instance, function string, use }, }, UserMetadata: nil, + Application: &oidc_api.ContextInfoApplication{ + ClientID: clientID, + }, Org: &query.UserInfoOrg{ ID: instance.DefaultOrg.GetId(), Name: instance.DefaultOrg.GetName(), @@ -890,10 +940,10 @@ func contextInfoForUserOIDC(instance *integration.Instance, function string, use func TestServer_ExecutionTargetPreAccessToken(t *testing.T) { instance := integration.NewInstance(CTX) - isolatedIAMCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) - ctxLoginClient := instance.WithAuthorization(CTX, integration.UserTypeLogin) + isolatedIAMCtx := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + ctxLoginClient := instance.WithAuthorizationToken(CTX, integration.UserTypeLogin) - client, err := instance.CreateOIDCImplicitFlowClient(isolatedIAMCtx, redirectURIImplicit, loginV2) + client, err := instance.CreateOIDCImplicitFlowClient(isolatedIAMCtx, t, redirectURIImplicit, loginV2) require.NoError(t, err) type want struct { @@ -918,7 +968,7 @@ func TestServer_ExecutionTargetPreAccessToken(t *testing.T) { {Key: "added1", Value: "value"}, }, } - return expectPreAccessTokenExecution(ctx, t, instance, req, response) + return expectPreAccessTokenExecution(ctx, t, instance, client.GetClientId(), req, response) }, req: &oidc_pb.CreateCallbackRequest{ AuthRequestId: func() string { @@ -943,7 +993,7 @@ func TestServer_ExecutionTargetPreAccessToken(t *testing.T) { "addedLog", }, } - return expectPreAccessTokenExecution(ctx, t, instance, req, response) + return expectPreAccessTokenExecution(ctx, t, instance, client.GetClientId(), req, response) }, req: &oidc_pb.CreateCallbackRequest{ AuthRequestId: func() string { @@ -968,7 +1018,7 @@ func TestServer_ExecutionTargetPreAccessToken(t *testing.T) { {Key: "key", Value: []byte("value")}, }, } - return expectPreAccessTokenExecution(ctx, t, instance, req, response) + return expectPreAccessTokenExecution(ctx, t, instance, client.GetClientId(), req, response) }, req: &oidc_pb.CreateCallbackRequest{ AuthRequestId: func() string { @@ -1005,7 +1055,7 @@ func TestServer_ExecutionTargetPreAccessToken(t *testing.T) { {Key: "added3", Value: "value3"}, }, } - return expectPreAccessTokenExecution(ctx, t, instance, req, response) + return expectPreAccessTokenExecution(ctx, t, instance, client.GetClientId(), req, response) }, req: &oidc_pb.CreateCallbackRequest{ AuthRequestId: func() string { @@ -1060,7 +1110,7 @@ func TestServer_ExecutionTargetPreAccessToken(t *testing.T) { } } -func expectPreAccessTokenExecution(ctx context.Context, t *testing.T, instance *integration.Instance, req *oidc_pb.CreateCallbackRequest, response *oidc_api.ContextInfoResponse) (string, func()) { +func expectPreAccessTokenExecution(ctx context.Context, t *testing.T, instance *integration.Instance, clientID string, req *oidc_pb.CreateCallbackRequest, response *oidc_api.ContextInfoResponse) (string, func()) { userEmail := gofakeit.Email() userPhone := "+41" + gofakeit.Phone() userResp := instance.CreateHumanUserVerified(ctx, instance.DefaultOrg.Id, userEmail, userPhone) @@ -1072,7 +1122,7 @@ func expectPreAccessTokenExecution(ctx context.Context, t *testing.T, instance * SessionToken: sessionResp.GetSessionToken(), }, } - expectedContextInfo := contextInfoForUserOIDC(instance, "function/preaccesstoken", userResp, userEmail, userPhone) + expectedContextInfo := contextInfoForUserOIDC(instance, "function/preaccesstoken", clientID, userResp, userEmail, userPhone) targetURL, closeF, _, _ := integration.TestServerCall(expectedContextInfo, 0, http.StatusOK, response) @@ -1083,8 +1133,8 @@ func expectPreAccessTokenExecution(ctx context.Context, t *testing.T, instance * func TestServer_ExecutionTargetPreSAMLResponse(t *testing.T) { instance := integration.NewInstance(CTX) - isolatedIAMCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) - ctxLoginClient := instance.WithAuthorization(CTX, integration.UserTypeLogin) + isolatedIAMCtx := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + ctxLoginClient := instance.WithAuthorizationToken(CTX, integration.UserTypeLogin) idpMetadata, err := instance.GetSAMLIDPMetadata() require.NoError(t, err) @@ -1255,10 +1305,9 @@ func createSAMLSP(t *testing.T, idpMetadata *saml.EntityDescriptor, binding stri } func createSAMLApplication(ctx context.Context, t *testing.T, instance *integration.Instance, idpMetadata *saml.EntityDescriptor, binding string, projectRoleCheck, hasProjectCheck bool) (string, string, *samlsp.Middleware) { - project, err := instance.CreateProjectWithPermissionCheck(ctx, projectRoleCheck, hasProjectCheck) - require.NoError(t, err) + project := instance.CreateProject(ctx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), projectRoleCheck, hasProjectCheck) rootURL, sp := createSAMLSP(t, idpMetadata, binding) - _, err = instance.CreateSAMLClient(ctx, project.GetId(), sp) + _, err := instance.CreateSAMLClient(ctx, project.GetId(), sp) require.NoError(t, err) return project.GetId(), rootURL, sp } diff --git a/internal/api/grpc/action/v2beta/integration_test/execution_test.go b/internal/api/grpc/action/v2beta/integration_test/execution_test.go index 2199b9f454..dee736991b 100644 --- a/internal/api/grpc/action/v2beta/integration_test/execution_test.go +++ b/internal/api/grpc/action/v2beta/integration_test/execution_test.go @@ -17,7 +17,7 @@ import ( func TestServer_SetExecution_Request(t *testing.T) { instance := integration.NewInstance(CTX) - isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + isolatedIAMOwnerCTX := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false) tests := []struct { @@ -29,7 +29,7 @@ func TestServer_SetExecution_Request(t *testing.T) { }{ { name: "missing permission", - ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), + ctx: instance.WithAuthorizationToken(context.Background(), integration.UserTypeOrgOwner), req: &action.SetExecutionRequest{ Condition: &action.Condition{ ConditionType: &action.Condition_Request{ @@ -155,7 +155,7 @@ func TestServer_SetExecution_Request(t *testing.T) { assertSetExecutionResponse(t, creationDate, setDate, tt.wantSetDate, got) // cleanup to not impact other requests - instance.DeleteExecution(tt.ctx, t, tt.req.GetCondition()) + deleteExecution(tt.ctx, t, instance, tt.req.GetCondition()) }) } } @@ -174,7 +174,7 @@ func assertSetExecutionResponse(t *testing.T, creationDate, setDate time.Time, e func TestServer_SetExecution_Response(t *testing.T) { instance := integration.NewInstance(CTX) - isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + isolatedIAMOwnerCTX := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false) tests := []struct { @@ -186,7 +186,7 @@ func TestServer_SetExecution_Response(t *testing.T) { }{ { name: "missing permission", - ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), + ctx: instance.WithAuthorizationToken(context.Background(), integration.UserTypeOrgOwner), req: &action.SetExecutionRequest{ Condition: &action.Condition{ ConditionType: &action.Condition_Response{ @@ -311,14 +311,14 @@ func TestServer_SetExecution_Response(t *testing.T) { assertSetExecutionResponse(t, creationDate, setDate, tt.wantSetDate, got) // cleanup to not impact other requests - instance.DeleteExecution(tt.ctx, t, tt.req.GetCondition()) + deleteExecution(tt.ctx, t, instance, tt.req.GetCondition()) }) } } func TestServer_SetExecution_Event(t *testing.T) { instance := integration.NewInstance(CTX) - isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + isolatedIAMOwnerCTX := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false) tests := []struct { @@ -330,7 +330,7 @@ func TestServer_SetExecution_Event(t *testing.T) { }{ { name: "missing permission", - ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), + ctx: instance.WithAuthorizationToken(context.Background(), integration.UserTypeOrgOwner), req: &action.SetExecutionRequest{ Condition: &action.Condition{ ConditionType: &action.Condition_Event{ @@ -474,14 +474,14 @@ func TestServer_SetExecution_Event(t *testing.T) { assertSetExecutionResponse(t, creationDate, setDate, tt.wantSetDate, got) // cleanup to not impact other requests - instance.DeleteExecution(tt.ctx, t, tt.req.GetCondition()) + deleteExecution(tt.ctx, t, instance, tt.req.GetCondition()) }) } } func TestServer_SetExecution_Function(t *testing.T) { instance := integration.NewInstance(CTX) - isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + isolatedIAMOwnerCTX := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://notexisting", domain.TargetTypeWebhook, false) tests := []struct { @@ -493,7 +493,7 @@ func TestServer_SetExecution_Function(t *testing.T) { }{ { name: "missing permission", - ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), + ctx: instance.WithAuthorizationToken(context.Background(), integration.UserTypeOrgOwner), req: &action.SetExecutionRequest{ Condition: &action.Condition{ ConditionType: &action.Condition_Response{ @@ -559,7 +559,7 @@ func TestServer_SetExecution_Function(t *testing.T) { assertSetExecutionResponse(t, creationDate, setDate, tt.wantSetDate, got) // cleanup to not impact other requests - instance.DeleteExecution(tt.ctx, t, tt.req.GetCondition()) + deleteExecution(tt.ctx, t, instance, tt.req.GetCondition()) }) } } diff --git a/internal/api/grpc/action/v2beta/integration_test/query_test.go b/internal/api/grpc/action/v2beta/integration_test/query_test.go index 5c59bee5d1..1118311bd2 100644 --- a/internal/api/grpc/action/v2beta/integration_test/query_test.go +++ b/internal/api/grpc/action/v2beta/integration_test/query_test.go @@ -21,7 +21,7 @@ import ( func TestServer_GetTarget(t *testing.T) { instance := integration.NewInstance(CTX) - isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + isolatedIAMOwnerCTX := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) type args struct { ctx context.Context dep func(context.Context, *action.GetTargetRequest, *action.GetTargetResponse) error @@ -36,7 +36,7 @@ func TestServer_GetTarget(t *testing.T) { { name: "missing permission", args: args{ - ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), + ctx: instance.WithAuthorizationToken(context.Background(), integration.UserTypeOrgOwner), req: &action.GetTargetRequest{}, }, wantErr: true, @@ -206,14 +206,14 @@ func TestServer_GetTarget(t *testing.T) { } assert.NoError(ttt, err) assert.EqualExportedValues(ttt, tt.want, got) - }, retryDuration, tick, "timeout waiting for expected target result") + }, retryDuration, tick, "timeout waiting for expected target Executions") }) } } func TestServer_ListTargets(t *testing.T) { instance := integration.NewInstance(CTX) - isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + isolatedIAMOwnerCTX := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) type args struct { ctx context.Context dep func(context.Context, *action.ListTargetsRequest, *action.ListTargetsResponse) @@ -228,7 +228,7 @@ func TestServer_ListTargets(t *testing.T) { { name: "missing permission", args: args{ - ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), + ctx: instance.WithAuthorizationToken(context.Background(), integration.UserTypeOrgOwner), req: &action.ListTargetsRequest{}, }, wantErr: true, @@ -253,7 +253,7 @@ func TestServer_ListTargets(t *testing.T) { TotalResult: 0, AppliedLimit: 100, }, - Result: []*action.Target{}, + Targets: []*action.Target{}, }, }, { @@ -269,11 +269,11 @@ func TestServer_ListTargets(t *testing.T) { }, } - response.Result[0].Id = resp.GetId() - response.Result[0].Name = name - response.Result[0].CreationDate = resp.GetCreationDate() - response.Result[0].ChangeDate = resp.GetCreationDate() - response.Result[0].SigningKey = resp.GetSigningKey() + response.Targets[0].Id = resp.GetId() + response.Targets[0].Name = name + response.Targets[0].CreationDate = resp.GetCreationDate() + response.Targets[0].ChangeDate = resp.GetCreationDate() + response.Targets[0].SigningKey = resp.GetSigningKey() }, req: &action.ListTargetsRequest{ Filters: []*action.TargetSearchFilter{{}}, @@ -284,7 +284,7 @@ func TestServer_ListTargets(t *testing.T) { TotalResult: 1, AppliedLimit: 100, }, - Result: []*action.Target{ + Targets: []*action.Target{ { Endpoint: "https://example.com", TargetType: &action.Target_RestWebhook{ @@ -309,11 +309,11 @@ func TestServer_ListTargets(t *testing.T) { }, } - response.Result[0].Id = resp.GetId() - response.Result[0].Name = name - response.Result[0].CreationDate = resp.GetCreationDate() - response.Result[0].ChangeDate = resp.GetCreationDate() - response.Result[0].SigningKey = resp.GetSigningKey() + response.Targets[0].Id = resp.GetId() + response.Targets[0].Name = name + response.Targets[0].CreationDate = resp.GetCreationDate() + response.Targets[0].ChangeDate = resp.GetCreationDate() + response.Targets[0].SigningKey = resp.GetSigningKey() }, req: &action.ListTargetsRequest{ Filters: []*action.TargetSearchFilter{{}}, @@ -324,7 +324,7 @@ func TestServer_ListTargets(t *testing.T) { TotalResult: 1, AppliedLimit: 100, }, - Result: []*action.Target{ + Targets: []*action.Target{ { Endpoint: "https://example.com", TargetType: &action.Target_RestWebhook{ @@ -354,23 +354,23 @@ func TestServer_ListTargets(t *testing.T) { }, } - response.Result[2].Id = resp1.GetId() - response.Result[2].Name = name1 - response.Result[2].CreationDate = resp1.GetCreationDate() - response.Result[2].ChangeDate = resp1.GetCreationDate() - response.Result[2].SigningKey = resp1.GetSigningKey() + response.Targets[2].Id = resp1.GetId() + response.Targets[2].Name = name1 + response.Targets[2].CreationDate = resp1.GetCreationDate() + response.Targets[2].ChangeDate = resp1.GetCreationDate() + response.Targets[2].SigningKey = resp1.GetSigningKey() - response.Result[1].Id = resp2.GetId() - response.Result[1].Name = name2 - response.Result[1].CreationDate = resp2.GetCreationDate() - response.Result[1].ChangeDate = resp2.GetCreationDate() - response.Result[1].SigningKey = resp2.GetSigningKey() + response.Targets[1].Id = resp2.GetId() + response.Targets[1].Name = name2 + response.Targets[1].CreationDate = resp2.GetCreationDate() + response.Targets[1].ChangeDate = resp2.GetCreationDate() + response.Targets[1].SigningKey = resp2.GetSigningKey() - response.Result[0].Id = resp3.GetId() - response.Result[0].Name = name3 - response.Result[0].CreationDate = resp3.GetCreationDate() - response.Result[0].ChangeDate = resp3.GetCreationDate() - response.Result[0].SigningKey = resp3.GetSigningKey() + response.Targets[0].Id = resp3.GetId() + response.Targets[0].Name = name3 + response.Targets[0].CreationDate = resp3.GetCreationDate() + response.Targets[0].ChangeDate = resp3.GetCreationDate() + response.Targets[0].SigningKey = resp3.GetSigningKey() }, req: &action.ListTargetsRequest{ Filters: []*action.TargetSearchFilter{{}}, @@ -381,7 +381,7 @@ func TestServer_ListTargets(t *testing.T) { TotalResult: 3, AppliedLimit: 100, }, - Result: []*action.Target{ + Targets: []*action.Target{ { Endpoint: "https://example.com", TargetType: &action.Target_RestAsync{ @@ -427,13 +427,13 @@ func TestServer_ListTargets(t *testing.T) { require.NoError(ttt, listErr) // always first check length, otherwise its failed anyway - if assert.Len(ttt, got.Result, len(tt.want.Result)) { - for i := range tt.want.Result { - assert.EqualExportedValues(ttt, tt.want.Result[i], got.Result[i]) + if assert.Len(ttt, got.Targets, len(tt.want.Targets)) { + for i := range tt.want.Targets { + assert.EqualExportedValues(ttt, tt.want.Targets[i], got.Targets[i]) } } assertPaginationResponse(ttt, tt.want.Pagination, got.Pagination) - }, retryDuration, tick, "timeout waiting for expected execution result") + }, retryDuration, tick, "timeout waiting for expected execution Executions") }) } } @@ -445,7 +445,7 @@ func assertPaginationResponse(t *assert.CollectT, expected *filter.PaginationRes func TestServer_ListExecutions(t *testing.T) { instance := integration.NewInstance(CTX) - isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + isolatedIAMOwnerCTX := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) targetResp := instance.CreateTarget(isolatedIAMOwnerCTX, t, "", "https://example.com", domain.TargetTypeWebhook, false) type args struct { @@ -462,7 +462,7 @@ func TestServer_ListExecutions(t *testing.T) { { name: "missing permission", args: args{ - ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), + ctx: instance.WithAuthorizationToken(context.Background(), integration.UserTypeOrgOwner), req: &action.ListExecutionsRequest{}, }, wantErr: true, @@ -473,12 +473,12 @@ func TestServer_ListExecutions(t *testing.T) { ctx: isolatedIAMOwnerCTX, dep: func(ctx context.Context, request *action.ListExecutionsRequest, response *action.ListExecutionsResponse) { cond := request.Filters[0].GetInConditionsFilter().GetConditions()[0] - resp := instance.SetExecution(ctx, t, cond, []string{targetResp.GetId()}) + resp := setExecution(ctx, t, instance, cond, []string{targetResp.GetId()}) // Set expected response with used values for SetExecution - response.Result[0].CreationDate = resp.GetSetDate() - response.Result[0].ChangeDate = resp.GetSetDate() - response.Result[0].Condition = cond + response.Executions[0].CreationDate = resp.GetSetDate() + response.Executions[0].ChangeDate = resp.GetSetDate() + response.Executions[0].Condition = cond }, req: &action.ListExecutionsRequest{ Filters: []*action.ExecutionSearchFilter{{ @@ -503,7 +503,7 @@ func TestServer_ListExecutions(t *testing.T) { TotalResult: 1, AppliedLimit: 100, }, - Result: []*action.Execution{ + Executions: []*action.Execution{ { Condition: &action.Condition{ ConditionType: &action.Condition_Request{ @@ -542,12 +542,12 @@ func TestServer_ListExecutions(t *testing.T) { }, }, } - resp := instance.SetExecution(ctx, t, cond, []string{target.GetId()}) + resp := setExecution(ctx, t, instance, cond, []string{target.GetId()}) - response.Result[0].CreationDate = resp.GetSetDate() - response.Result[0].ChangeDate = resp.GetSetDate() - response.Result[0].Condition = cond - response.Result[0].Targets = []string{target.GetId()} + response.Executions[0].CreationDate = resp.GetSetDate() + response.Executions[0].ChangeDate = resp.GetSetDate() + response.Executions[0].Condition = cond + response.Executions[0].Targets = []string{target.GetId()} }, req: &action.ListExecutionsRequest{ Filters: []*action.ExecutionSearchFilter{{}}, @@ -558,7 +558,7 @@ func TestServer_ListExecutions(t *testing.T) { TotalResult: 1, AppliedLimit: 100, }, - Result: []*action.Execution{ + Executions: []*action.Execution{ { Condition: &action.Condition{}, Targets: []string{""}, @@ -603,8 +603,8 @@ func TestServer_ListExecutions(t *testing.T) { } cond1 := request.Filters[0].GetInConditionsFilter().GetConditions()[0] - resp1 := instance.SetExecution(ctx, t, cond1, []string{targetResp.GetId()}) - response.Result[2] = &action.Execution{ + resp1 := setExecution(ctx, t, instance, cond1, []string{targetResp.GetId()}) + response.Executions[2] = &action.Execution{ CreationDate: resp1.GetSetDate(), ChangeDate: resp1.GetSetDate(), Condition: cond1, @@ -612,8 +612,8 @@ func TestServer_ListExecutions(t *testing.T) { } cond2 := request.Filters[0].GetInConditionsFilter().GetConditions()[1] - resp2 := instance.SetExecution(ctx, t, cond2, []string{targetResp.GetId()}) - response.Result[1] = &action.Execution{ + resp2 := setExecution(ctx, t, instance, cond2, []string{targetResp.GetId()}) + response.Executions[1] = &action.Execution{ CreationDate: resp2.GetSetDate(), ChangeDate: resp2.GetSetDate(), Condition: cond2, @@ -621,8 +621,8 @@ func TestServer_ListExecutions(t *testing.T) { } cond3 := request.Filters[0].GetInConditionsFilter().GetConditions()[2] - resp3 := instance.SetExecution(ctx, t, cond3, []string{targetResp.GetId()}) - response.Result[0] = &action.Execution{ + resp3 := setExecution(ctx, t, instance, cond3, []string{targetResp.GetId()}) + response.Executions[0] = &action.Execution{ CreationDate: resp3.GetSetDate(), ChangeDate: resp3.GetSetDate(), Condition: cond3, @@ -640,7 +640,7 @@ func TestServer_ListExecutions(t *testing.T) { TotalResult: 3, AppliedLimit: 100, }, - Result: []*action.Execution{ + Executions: []*action.Execution{ {}, {}, {}, }, }, @@ -652,8 +652,8 @@ func TestServer_ListExecutions(t *testing.T) { dep: func(ctx context.Context, request *action.ListExecutionsRequest, response *action.ListExecutionsResponse) { conditions := request.Filters[0].GetInConditionsFilter().GetConditions() for i, cond := range conditions { - resp := instance.SetExecution(ctx, t, cond, []string{targetResp.GetId()}) - response.Result[(len(conditions)-1)-i] = &action.Execution{ + resp := setExecution(ctx, t, instance, cond, []string{targetResp.GetId()}) + response.Executions[(len(conditions)-1)-i] = &action.Execution{ CreationDate: resp.GetSetDate(), ChangeDate: resp.GetSetDate(), Condition: cond, @@ -687,7 +687,7 @@ func TestServer_ListExecutions(t *testing.T) { TotalResult: 10, AppliedLimit: 100, }, - Result: []*action.Execution{ + Executions: []*action.Execution{ {}, {}, {}, @@ -708,8 +708,8 @@ func TestServer_ListExecutions(t *testing.T) { dep: func(ctx context.Context, request *action.ListExecutionsRequest, response *action.ListExecutionsResponse) { conditions := request.Filters[0].GetInConditionsFilter().GetConditions() for i, cond := range conditions { - resp := instance.SetExecution(ctx, t, cond, []string{targetResp.GetId()}) - response.Result[i] = &action.Execution{ + resp := setExecution(ctx, t, instance, cond, []string{targetResp.GetId()}) + response.Executions[i] = &action.Execution{ CreationDate: resp.GetSetDate(), ChangeDate: resp.GetSetDate(), Condition: cond, @@ -744,7 +744,7 @@ func TestServer_ListExecutions(t *testing.T) { TotalResult: 10, AppliedLimit: 100, }, - Result: []*action.Execution{ + Executions: []*action.Execution{ {}, {}, {}, @@ -774,20 +774,11 @@ func TestServer_ListExecutions(t *testing.T) { } require.NoError(ttt, listErr) // always first check length, otherwise its failed anyway - if assert.Len(ttt, got.Result, len(tt.want.Result)) { - assert.EqualExportedValues(ttt, got.Result, tt.want.Result) + if assert.Len(ttt, got.Executions, len(tt.want.Executions)) { + assert.EqualExportedValues(ttt, got.Executions, tt.want.Executions) } assertPaginationResponse(ttt, tt.want.Pagination, got.Pagination) - }, retryDuration, tick, "timeout waiting for expected execution result") + }, retryDuration, tick, "timeout waiting for expected execution Executions") }) } } - -func containExecution(t *assert.CollectT, executionList []*action.Execution, execution *action.Execution) bool { - for _, exec := range executionList { - if assert.EqualExportedValues(t, execution, exec) { - return true - } - } - return false -} diff --git a/internal/api/grpc/action/v2beta/integration_test/target_test.go b/internal/api/grpc/action/v2beta/integration_test/target_test.go index 8238d3146d..25a4e5f550 100644 --- a/internal/api/grpc/action/v2beta/integration_test/target_test.go +++ b/internal/api/grpc/action/v2beta/integration_test/target_test.go @@ -19,7 +19,7 @@ import ( func TestServer_CreateTarget(t *testing.T) { instance := integration.NewInstance(CTX) - isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + isolatedIAMOwnerCTX := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) type want struct { id bool creationDate bool @@ -36,7 +36,7 @@ func TestServer_CreateTarget(t *testing.T) { }{ { name: "missing permission", - ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), + ctx: instance.WithAuthorizationToken(context.Background(), integration.UserTypeOrgOwner), req: &action.CreateTargetRequest{ Name: gofakeit.Name(), }, @@ -243,7 +243,7 @@ func assertCreateTargetResponse(t *testing.T, creationDate, changeDate time.Time func TestServer_UpdateTarget(t *testing.T) { instance := integration.NewInstance(CTX) - isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + isolatedIAMOwnerCTX := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) type args struct { ctx context.Context req *action.UpdateTargetRequest @@ -267,7 +267,7 @@ func TestServer_UpdateTarget(t *testing.T) { request.Id = targetID }, args: args{ - ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), + ctx: instance.WithAuthorizationToken(context.Background(), integration.UserTypeOrgOwner), req: &action.UpdateTargetRequest{ Name: gu.Ptr(gofakeit.Name()), }, @@ -278,7 +278,6 @@ func TestServer_UpdateTarget(t *testing.T) { name: "not existing", prepare: func(request *action.UpdateTargetRequest) { request.Id = "notexisting" - return }, args: args{ ctx: isolatedIAMOwnerCTX, @@ -461,7 +460,7 @@ func assertUpdateTargetResponse(t *testing.T, creationDate, changeDate time.Time func TestServer_DeleteTarget(t *testing.T) { instance := integration.NewInstance(CTX) - iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + iamOwnerCtx := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) tests := []struct { name string ctx context.Context @@ -472,7 +471,7 @@ func TestServer_DeleteTarget(t *testing.T) { }{ { name: "missing permission", - ctx: instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), + ctx: instance.WithAuthorizationToken(context.Background(), integration.UserTypeOrgOwner), req: &action.DeleteTargetRequest{ Id: "notexisting", }, diff --git a/internal/api/grpc/action/v2beta/query.go b/internal/api/grpc/action/v2beta/query.go index 1dbe80a8f7..164283b890 100644 --- a/internal/api/grpc/action/v2beta/query.go +++ b/internal/api/grpc/action/v2beta/query.go @@ -4,6 +4,7 @@ import ( "context" "strings" + "connectrpc.com/connect" "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/timestamppb" @@ -22,14 +23,14 @@ const ( conditionIDEventGroupSegmentCount = 1 ) -func (s *Server) GetTarget(ctx context.Context, req *action.GetTargetRequest) (*action.GetTargetResponse, error) { - resp, err := s.query.GetTargetByID(ctx, req.GetId()) +func (s *Server) GetTarget(ctx context.Context, req *connect.Request[action.GetTargetRequest]) (*connect.Response[action.GetTargetResponse], error) { + resp, err := s.query.GetTargetByID(ctx, req.Msg.GetId()) if err != nil { return nil, err } - return &action.GetTargetResponse{ + return connect.NewResponse(&action.GetTargetResponse{ Target: targetToPb(resp), - }, nil + }), nil } type InstanceContext interface { @@ -41,8 +42,8 @@ type Context interface { GetOwner() InstanceContext } -func (s *Server) ListTargets(ctx context.Context, req *action.ListTargetsRequest) (*action.ListTargetsResponse, error) { - queries, err := s.ListTargetsRequestToModel(req) +func (s *Server) ListTargets(ctx context.Context, req *connect.Request[action.ListTargetsRequest]) (*connect.Response[action.ListTargetsResponse], error) { + queries, err := s.ListTargetsRequestToModel(req.Msg) if err != nil { return nil, err } @@ -50,14 +51,14 @@ func (s *Server) ListTargets(ctx context.Context, req *action.ListTargetsRequest if err != nil { return nil, err } - return &action.ListTargetsResponse{ - Result: targetsToPb(resp.Targets), + return connect.NewResponse(&action.ListTargetsResponse{ + Targets: targetsToPb(resp.Targets), Pagination: filter.QueryToPaginationPb(queries.SearchRequest, resp.SearchResponse), - }, nil + }), nil } -func (s *Server) ListExecutions(ctx context.Context, req *action.ListExecutionsRequest) (*action.ListExecutionsResponse, error) { - queries, err := s.ListExecutionsRequestToModel(req) +func (s *Server) ListExecutions(ctx context.Context, req *connect.Request[action.ListExecutionsRequest]) (*connect.Response[action.ListExecutionsResponse], error) { + queries, err := s.ListExecutionsRequestToModel(req.Msg) if err != nil { return nil, err } @@ -65,10 +66,10 @@ func (s *Server) ListExecutions(ctx context.Context, req *action.ListExecutionsR if err != nil { return nil, err } - return &action.ListExecutionsResponse{ - Result: executionsToPb(resp.Executions), + return connect.NewResponse(&action.ListExecutionsResponse{ + Executions: executionsToPb(resp.Executions), Pagination: filter.QueryToPaginationPb(queries.SearchRequest, resp.SearchResponse), - }, nil + }), nil } func targetsToPb(targets []*query.Target) []*action.Target { @@ -81,7 +82,7 @@ func targetsToPb(targets []*query.Target) []*action.Target { func targetToPb(t *query.Target) *action.Target { target := &action.Target{ - Id: t.ObjectDetails.ID, + Id: t.ID, Name: t.Name, Timeout: durationpb.New(t.Timeout), Endpoint: t.Endpoint, @@ -98,11 +99,11 @@ func targetToPb(t *query.Target) *action.Target { target.TargetType = nil } - if !t.ObjectDetails.EventDate.IsZero() { - target.ChangeDate = timestamppb.New(t.ObjectDetails.EventDate) + if !t.EventDate.IsZero() { + target.ChangeDate = timestamppb.New(t.EventDate) } - if !t.ObjectDetails.CreationDate.IsZero() { - target.CreationDate = timestamppb.New(t.ObjectDetails.CreationDate) + if !t.CreationDate.IsZero() { + target.CreationDate = timestamppb.New(t.CreationDate) } return target } @@ -333,11 +334,11 @@ func executionToPb(e *query.Execution) *action.Execution { Condition: executionIDToCondition(e.ID), Targets: targets, } - if !e.ObjectDetails.EventDate.IsZero() { - exec.ChangeDate = timestamppb.New(e.ObjectDetails.EventDate) + if !e.EventDate.IsZero() { + exec.ChangeDate = timestamppb.New(e.EventDate) } - if !e.ObjectDetails.CreationDate.IsZero() { - exec.CreationDate = timestamppb.New(e.ObjectDetails.CreationDate) + if !e.CreationDate.IsZero() { + exec.CreationDate = timestamppb.New(e.CreationDate) } return exec } diff --git a/internal/api/grpc/action/v2beta/server.go b/internal/api/grpc/action/v2beta/server.go index ef0d8eb2ba..440bf842ca 100644 --- a/internal/api/grpc/action/v2beta/server.go +++ b/internal/api/grpc/action/v2beta/server.go @@ -1,7 +1,10 @@ package action import ( - "google.golang.org/grpc" + "net/http" + + "connectrpc.com/connect" + "google.golang.org/protobuf/reflect/protoreflect" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/grpc/server" @@ -9,12 +12,12 @@ import ( "github.com/zitadel/zitadel/internal/config/systemdefaults" "github.com/zitadel/zitadel/internal/query" action "github.com/zitadel/zitadel/pkg/grpc/action/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/action/v2beta/actionconnect" ) -var _ action.ActionServiceServer = (*Server)(nil) +var _ actionconnect.ActionServiceHandler = (*Server)(nil) type Server struct { - action.UnimplementedActionServiceServer systemDefaults systemdefaults.SystemDefaults command *command.Commands query *query.Queries @@ -43,8 +46,12 @@ func CreateServer( } } -func (s *Server) RegisterServer(grpcServer *grpc.Server) { - action.RegisterActionServiceServer(grpcServer, s) +func (s *Server) RegisterConnectServer(interceptors ...connect.Interceptor) (string, http.Handler) { + return actionconnect.NewActionServiceHandler(s, connect.WithInterceptors(interceptors...)) +} + +func (s *Server) FileDescriptor() protoreflect.FileDescriptor { + return action.File_zitadel_action_v2beta_action_service_proto } func (s *Server) AppName() string { diff --git a/internal/api/grpc/action/v2beta/target.go b/internal/api/grpc/action/v2beta/target.go index 26c88b9683..b13f3461f0 100644 --- a/internal/api/grpc/action/v2beta/target.go +++ b/internal/api/grpc/action/v2beta/target.go @@ -3,6 +3,7 @@ package action import ( "context" + "connectrpc.com/connect" "github.com/muhlemmer/gu" "google.golang.org/protobuf/types/known/timestamppb" @@ -13,8 +14,8 @@ import ( action "github.com/zitadel/zitadel/pkg/grpc/action/v2beta" ) -func (s *Server) CreateTarget(ctx context.Context, req *action.CreateTargetRequest) (*action.CreateTargetResponse, error) { - add := createTargetToCommand(req) +func (s *Server) CreateTarget(ctx context.Context, req *connect.Request[action.CreateTargetRequest]) (*connect.Response[action.CreateTargetResponse], error) { + add := createTargetToCommand(req.Msg) instanceID := authz.GetInstance(ctx).InstanceID() createdAt, err := s.command.AddTarget(ctx, add, instanceID) if err != nil { @@ -24,16 +25,16 @@ func (s *Server) CreateTarget(ctx context.Context, req *action.CreateTargetReque if !createdAt.IsZero() { creationDate = timestamppb.New(createdAt) } - return &action.CreateTargetResponse{ + return connect.NewResponse(&action.CreateTargetResponse{ Id: add.AggregateID, CreationDate: creationDate, SigningKey: add.SigningKey, - }, nil + }), nil } -func (s *Server) UpdateTarget(ctx context.Context, req *action.UpdateTargetRequest) (*action.UpdateTargetResponse, error) { +func (s *Server) UpdateTarget(ctx context.Context, req *connect.Request[action.UpdateTargetRequest]) (*connect.Response[action.UpdateTargetResponse], error) { instanceID := authz.GetInstance(ctx).InstanceID() - update := updateTargetToCommand(req) + update := updateTargetToCommand(req.Msg) changedAt, err := s.command.ChangeTarget(ctx, update, instanceID) if err != nil { return nil, err @@ -42,15 +43,15 @@ func (s *Server) UpdateTarget(ctx context.Context, req *action.UpdateTargetReque if !changedAt.IsZero() { changeDate = timestamppb.New(changedAt) } - return &action.UpdateTargetResponse{ + return connect.NewResponse(&action.UpdateTargetResponse{ ChangeDate: changeDate, SigningKey: update.SigningKey, - }, nil + }), nil } -func (s *Server) DeleteTarget(ctx context.Context, req *action.DeleteTargetRequest) (*action.DeleteTargetResponse, error) { +func (s *Server) DeleteTarget(ctx context.Context, req *connect.Request[action.DeleteTargetRequest]) (*connect.Response[action.DeleteTargetResponse], error) { instanceID := authz.GetInstance(ctx).InstanceID() - deletedAt, err := s.command.DeleteTarget(ctx, req.GetId(), instanceID) + deletedAt, err := s.command.DeleteTarget(ctx, req.Msg.GetId(), instanceID) if err != nil { return nil, err } @@ -58,9 +59,9 @@ func (s *Server) DeleteTarget(ctx context.Context, req *action.DeleteTargetReque if !deletedAt.IsZero() { deletionDate = timestamppb.New(deletedAt) } - return &action.DeleteTargetResponse{ + return connect.NewResponse(&action.DeleteTargetResponse{ DeletionDate: deletionDate, - }, nil + }), nil } func createTargetToCommand(req *action.CreateTargetRequest) *command.AddTarget { diff --git a/internal/api/grpc/admin/export.go b/internal/api/grpc/admin/export.go index da364909cb..7ce2dbd7b5 100644 --- a/internal/api/grpc/admin/export.go +++ b/internal/api/grpc/admin/export.go @@ -8,7 +8,9 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" authn_grpc "github.com/zitadel/zitadel/internal/api/grpc/authn" + "github.com/zitadel/zitadel/internal/api/grpc/org" text_grpc "github.com/zitadel/zitadel/internal/api/grpc/text" + user_converter "github.com/zitadel/zitadel/internal/api/grpc/user" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -65,7 +67,7 @@ func (s *Server) ExportData(ctx context.Context, req *admin_pb.ExportDataRequest /****************************************************************************************************************** Organization ******************************************************************************************************************/ - org := &admin_pb.DataOrg{OrgId: queriedOrg.ID, Org: &management_pb.AddOrgRequest{Name: queriedOrg.Name}} + org := &admin_pb.DataOrg{OrgId: queriedOrg.ID, OrgState: org.OrgStateToPb(queriedOrg.State), Org: &management_pb.AddOrgRequest{Name: queriedOrg.Name}} orgs[i] = org } @@ -554,7 +556,7 @@ func (s *Server) getUsers(ctx context.Context, org string, withPasswords bool, w if err != nil { return nil, nil, nil, nil, err } - users, err := s.query.SearchUsers(ctx, &query.UserSearchQueries{Queries: []query.SearchQuery{orgSearch}}, org, nil) + users, err := s.query.SearchUsers(ctx, &query.UserSearchQueries{Queries: []query.SearchQuery{orgSearch}}, nil) if err != nil { return nil, nil, nil, nil, err } @@ -567,6 +569,7 @@ func (s *Server) getUsers(ctx context.Context, org string, withPasswords bool, w case domain.UserTypeHuman: dataUser := &v1_pb.DataHumanUser{ UserId: user.ID, + State: user_converter.UserStateToPb(user.State), User: &management_pb.ImportHumanUserRequest{ UserName: user.Username, Profile: &management_pb.ImportHumanUserRequest_Profile{ @@ -620,6 +623,7 @@ func (s *Server) getUsers(ctx context.Context, org string, withPasswords bool, w case domain.UserTypeMachine: machineUsers = append(machineUsers, &v1_pb.DataMachineUser{ UserId: user.ID, + State: user_converter.UserStateToPb(user.State), User: &management_pb.AddMachineUserRequest{ UserName: user.Username, Name: user.Machine.Name, @@ -647,7 +651,6 @@ func (s *Server) getUsers(ctx context.Context, org string, withPasswords bool, w ExpirationDate: timestamppb.New(key.Expiration), PublicKey: key.PublicKey, }) - } } @@ -656,7 +659,7 @@ func (s *Server) getUsers(ctx context.Context, org string, withPasswords bool, w if err != nil { return nil, nil, nil, nil, err } - metadataList, err := s.query.SearchUserMetadata(ctx, false, user.ID, &query.UserMetadataSearchQueries{Queries: []query.SearchQuery{metadataOrgSearch}}, false) + metadataList, err := s.query.SearchUserMetadata(ctx, false, user.ID, &query.UserMetadataSearchQueries{Queries: []query.SearchQuery{metadataOrgSearch}}, nil) metaspan.EndWithError(err) if err != nil { return nil, nil, nil, nil, err @@ -736,7 +739,7 @@ func (s *Server) getProjectsAndApps(ctx context.Context, org string) ([]*v1_pb.D if err != nil { return nil, nil, nil, nil, nil, err } - queriedProjects, err := s.query.SearchProjects(ctx, &query.ProjectSearchQueries{Queries: []query.SearchQuery{projectSearch}}) + queriedProjects, err := s.query.SearchProjects(ctx, &query.ProjectSearchQueries{Queries: []query.SearchQuery{projectSearch}}, nil) if err != nil { return nil, nil, nil, nil, nil, err } @@ -763,7 +766,7 @@ func (s *Server) getProjectsAndApps(ctx context.Context, org string) ([]*v1_pb.D return nil, nil, nil, nil, nil, err } - queriedProjectRoles, err := s.query.SearchProjectRoles(ctx, false, &query.ProjectRoleSearchQueries{Queries: []query.SearchQuery{projectRoleSearch}}) + queriedProjectRoles, err := s.query.SearchProjectRoles(ctx, false, &query.ProjectRoleSearchQueries{Queries: []query.SearchQuery{projectRoleSearch}}, nil) if err != nil { return nil, nil, nil, nil, nil, err } @@ -780,7 +783,7 @@ func (s *Server) getProjectsAndApps(ctx context.Context, org string) ([]*v1_pb.D if err != nil { return nil, nil, nil, nil, nil, err } - apps, err := s.query.SearchApps(ctx, &query.AppSearchQueries{Queries: []query.SearchQuery{appSearch}}, false) + apps, err := s.query.SearchApps(ctx, &query.AppSearchQueries{Queries: []query.SearchQuery{appSearch}}, nil) if err != nil { return nil, nil, nil, nil, nil, err } @@ -888,7 +891,6 @@ func (s *Server) getNecessaryProjectGrantMembersForOrg(ctx context.Context, org break } } - } } } @@ -940,12 +942,11 @@ func (s *Server) getNecessaryOrgMembersForOrg(ctx context.Context, org string, p } func (s *Server) getNecessaryProjectGrantsForOrg(ctx context.Context, org string, processedOrgs []string, processedProjects []string) ([]*v1_pb.DataProjectGrant, error) { - projectGrantSearchOrg, err := query.NewProjectGrantResourceOwnerSearchQuery(org) if err != nil { return nil, err } - queriedProjectGrants, err := s.query.SearchProjectGrants(ctx, &query.ProjectGrantSearchQueries{Queries: []query.SearchQuery{projectGrantSearchOrg}}) + queriedProjectGrants, err := s.query.SearchProjectGrants(ctx, &query.ProjectGrantSearchQueries{Queries: []query.SearchQuery{projectGrantSearchOrg}}, nil) if err != nil { return nil, err } @@ -983,7 +984,7 @@ func (s *Server) getNecessaryUserGrantsForOrg(ctx context.Context, org string, p return nil, err } - queriedUserGrants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{Queries: []query.SearchQuery{userGrantSearchOrg}}, true) + queriedUserGrants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{Queries: []query.SearchQuery{userGrantSearchOrg}}, true, nil) if err != nil { return nil, err } @@ -991,7 +992,7 @@ func (s *Server) getNecessaryUserGrantsForOrg(ctx context.Context, org string, p for _, userGrant := range queriedUserGrants.UserGrants { for _, projectID := range processedProjects { if projectID == userGrant.ProjectID { - //if usergrant is on a granted project + // if usergrant is on a granted project if userGrant.GrantID != "" { for _, grantID := range processedGrants { if grantID == userGrant.GrantID { @@ -1024,6 +1025,7 @@ func (s *Server) getNecessaryUserGrantsForOrg(ctx context.Context, org string, p } return userGrants, nil } + func (s *Server) getCustomLoginTexts(ctx context.Context, org string, languages []string) ([]*management_pb.SetCustomLoginTextsRequest, error) { customTexts := make([]*management_pb.SetCustomLoginTextsRequest, 0, len(languages)) for _, lang := range languages { diff --git a/internal/api/grpc/admin/iam_member.go b/internal/api/grpc/admin/iam_member.go index 8f9b11ce2a..301acec6d6 100644 --- a/internal/api/grpc/admin/iam_member.go +++ b/internal/api/grpc/admin/iam_member.go @@ -4,6 +4,7 @@ import ( "context" "time" + "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/grpc/member" "github.com/zitadel/zitadel/internal/api/grpc/object" admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin" @@ -33,35 +34,35 @@ func (s *Server) ListIAMMembers(ctx context.Context, req *admin_pb.ListIAMMember } func (s *Server) AddIAMMember(ctx context.Context, req *admin_pb.AddIAMMemberRequest) (*admin_pb.AddIAMMemberResponse, error) { - member, err := s.command.AddInstanceMember(ctx, req.UserId, req.Roles...) + member, err := s.command.AddInstanceMember(ctx, AddIAMMemberToCommand(req, authz.GetInstance(ctx).InstanceID())) if err != nil { return nil, err } return &admin_pb.AddIAMMemberResponse{ Details: object.AddToDetailsPb( member.Sequence, - member.ChangeDate, + member.EventDate, member.ResourceOwner, ), }, nil } func (s *Server) UpdateIAMMember(ctx context.Context, req *admin_pb.UpdateIAMMemberRequest) (*admin_pb.UpdateIAMMemberResponse, error) { - member, err := s.command.ChangeInstanceMember(ctx, UpdateIAMMemberToDomain(req)) + member, err := s.command.ChangeInstanceMember(ctx, UpdateIAMMemberToCommand(req, authz.GetInstance(ctx).InstanceID())) if err != nil { return nil, err } return &admin_pb.UpdateIAMMemberResponse{ Details: object.ChangeToDetailsPb( member.Sequence, - member.ChangeDate, + member.EventDate, member.ResourceOwner, ), }, nil } func (s *Server) RemoveIAMMember(ctx context.Context, req *admin_pb.RemoveIAMMemberRequest) (*admin_pb.RemoveIAMMemberResponse, error) { - objectDetails, err := s.command.RemoveInstanceMember(ctx, req.UserId) + objectDetails, err := s.command.RemoveInstanceMember(ctx, authz.GetInstance(ctx).InstanceID(), req.UserId) if err != nil { return nil, err } diff --git a/internal/api/grpc/admin/iam_member_converter.go b/internal/api/grpc/admin/iam_member_converter.go index 2fe75214fd..695711d9cc 100644 --- a/internal/api/grpc/admin/iam_member_converter.go +++ b/internal/api/grpc/admin/iam_member_converter.go @@ -3,23 +3,25 @@ package admin import ( member_grpc "github.com/zitadel/zitadel/internal/api/grpc/member" "github.com/zitadel/zitadel/internal/api/grpc/object" - "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/query" admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin" member_pb "github.com/zitadel/zitadel/pkg/grpc/member" ) -func AddIAMMemberToDomain(req *admin_pb.AddIAMMemberRequest) *domain.Member { - return &domain.Member{ - UserID: req.UserId, - Roles: req.Roles, +func AddIAMMemberToCommand(req *admin_pb.AddIAMMemberRequest, instanceID string) *command.AddInstanceMember { + return &command.AddInstanceMember{ + InstanceID: instanceID, + UserID: req.UserId, + Roles: req.Roles, } } -func UpdateIAMMemberToDomain(req *admin_pb.UpdateIAMMemberRequest) *domain.Member { - return &domain.Member{ - UserID: req.UserId, - Roles: req.Roles, +func UpdateIAMMemberToCommand(req *admin_pb.UpdateIAMMemberRequest, instanceID string) *command.ChangeInstanceMember { + return &command.ChangeInstanceMember{ + InstanceID: instanceID, + UserID: req.UserId, + Roles: req.Roles, } } diff --git a/internal/api/grpc/admin/iam_member_converter_test.go b/internal/api/grpc/admin/iam_member_converter_test.go index 74dd329ee1..70e282d621 100644 --- a/internal/api/grpc/admin/iam_member_converter_test.go +++ b/internal/api/grpc/admin/iam_member_converter_test.go @@ -27,7 +27,7 @@ func TestAddIAMMemberToDomain(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := AddIAMMemberToDomain(tt.args.req) + got := AddIAMMemberToCommand(tt.args.req, "INSTANCE") test.AssertFieldsMapped(t, got, "ObjectRoot") }) } @@ -53,7 +53,7 @@ func TestUpdateIAMMemberToDomain(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := UpdateIAMMemberToDomain(tt.args.req) + got := UpdateIAMMemberToCommand(tt.args.req, "INSTANCE") test.AssertFieldsMapped(t, got, "ObjectRoot") }) } diff --git a/internal/api/grpc/admin/import.go b/internal/api/grpc/admin/import.go index 5bbcab27cf..ac085c135b 100644 --- a/internal/api/grpc/admin/import.go +++ b/internal/api/grpc/admin/import.go @@ -22,6 +22,7 @@ import ( action_grpc "github.com/zitadel/zitadel/internal/api/grpc/action" "github.com/zitadel/zitadel/internal/api/grpc/authn" "github.com/zitadel/zitadel/internal/api/grpc/management" + org_converter "github.com/zitadel/zitadel/internal/api/grpc/org" "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" @@ -305,7 +306,8 @@ func importOrg1(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataEr ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - _, err = s.command.AddOrgWithID(ctx, org.GetOrg().GetName(), ctxData.UserID, ctxData.ResourceOwner, org.GetOrgId(), []string{}) + setOrgInactive := org_converter.OrgStateToDomain(org.OrgState) == domain.OrgStateInactive + _, err = s.command.AddOrgWithID(ctx, org.GetOrg().GetName(), ctxData.UserID, ctxData.ResourceOwner, org.GetOrgId(), setOrgInactive, []string{}) if err != nil { *errors = append(*errors, &admin_pb.ImportDataError{Type: "org", Id: org.GetOrgId(), Message: err.Error()}) if _, err := s.query.OrgByID(ctx, true, org.OrgId); err != nil { @@ -474,7 +476,10 @@ func importHumanUsers(ctx context.Context, s *Server, errors *[]*admin_pb.Import logging.Debugf("import user: %s", user.GetUserId()) human, passwordless, links := management.ImportHumanUserRequestToDomain(user.User) human.AggregateID = user.UserId - _, _, err := s.command.ImportHuman(ctx, org.GetOrgId(), human, passwordless, links, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessInitCode) + userState := user.State.ToDomain() + + //nolint:staticcheck + _, _, err := s.command.ImportHuman(ctx, org.GetOrgId(), human, passwordless, &userState, links, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessInitCode) if err != nil { *errors = append(*errors, &admin_pb.ImportDataError{Type: "human_user", Id: user.GetUserId(), Message: err.Error()}) if isCtxTimeout(ctx) { @@ -510,7 +515,8 @@ func importMachineUsers(ctx context.Context, s *Server, errors *[]*admin_pb.Impo } for _, user := range org.GetMachineUsers() { logging.Debugf("import user: %s", user.GetUserId()) - _, err := s.command.AddMachine(ctx, management.AddMachineUserRequestToCommand(user.GetUser(), org.GetOrgId())) + userState := user.State.ToDomain() + _, err := s.command.AddMachine(ctx, management.AddMachineUserRequestToCommand(user.GetUser(), org.GetOrgId()), &userState, nil) if err != nil { *errors = append(*errors, &admin_pb.ImportDataError{Type: "machine_user", Id: user.GetUserId(), Message: err.Error()}) if isCtxTimeout(ctx) { @@ -609,7 +615,6 @@ func importUserLinks(ctx context.Context, s *Server, errors *[]*admin_pb.ImportD successOrg.UserLinks = append(successOrg.UserLinks, &admin_pb.ImportDataSuccessUserLinks{UserId: userLinks.GetUserId(), IdpId: userLinks.GetIdpId(), ExternalUserId: userLinks.GetProvidedUserId(), DisplayName: userLinks.GetProvidedUserName()}) } return nil - } func importProjects(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, org *admin_pb.DataOrg, count *counts) (err error) { @@ -621,7 +626,7 @@ func importProjects(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDa } for _, project := range org.GetProjects() { logging.Debugf("import project: %s", project.GetProjectId()) - _, err := s.command.AddProjectWithID(ctx, management.ProjectCreateToDomain(project.GetProject()), org.GetOrgId(), project.GetProjectId()) + _, err := s.command.AddProject(ctx, management.ProjectCreateToCommand(project.GetProject(), project.GetProjectId(), org.GetOrgId())) if err != nil { *errors = append(*errors, &admin_pb.ImportDataError{Type: "project", Id: project.GetProjectId(), Message: err.Error()}) if isCtxTimeout(ctx) { @@ -750,6 +755,7 @@ func importActions(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDat } return nil } + func importProjectRoles(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataError, successOrg *admin_pb.ImportDataSuccessOrg, org *admin_pb.DataOrg, count *counts) (err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -761,7 +767,7 @@ func importProjectRoles(ctx context.Context, s *Server, errors *[]*admin_pb.Impo logging.Debugf("import projectroles: %s", role.ProjectId+"_"+role.RoleKey) // TBD: why not command.BulkAddProjectRole? - _, err := s.command.AddProjectRole(ctx, management.AddProjectRoleRequestToDomain(role), org.GetOrgId()) + _, err := s.command.AddProjectRole(ctx, management.AddProjectRoleRequestToCommand(role, org.GetOrgId())) if err != nil { *errors = append(*errors, &admin_pb.ImportDataError{Type: "project_role", Id: role.ProjectId + "_" + role.RoleKey, Message: err.Error()}) if isCtxTimeout(ctx) { @@ -805,6 +811,7 @@ func importResources(ctx context.Context, s *Server, errors *[]*admin_pb.ImportD importDomainClaimedMessageTexts(ctx, s, errors, org) importPasswordlessRegistrationMessageTexts(ctx, s, errors, org) importInviteUserMessageTexts(ctx, s, errors, org) + if err := importHumanUsers(ctx, s, errors, successOrg, org, count, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessInitCode); err != nil { return err } @@ -1023,7 +1030,7 @@ func importOrg2(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataEr if org.ProjectGrants != nil { for _, grant := range org.GetProjectGrants() { logging.Debugf("import projectgrant: %s", grant.GetGrantId()+"_"+grant.GetProjectGrant().GetProjectId()+"_"+grant.GetProjectGrant().GetGrantedOrgId()) - _, err := s.command.AddProjectGrantWithID(ctx, management.AddProjectGrantRequestToDomain(grant.GetProjectGrant()), grant.GetGrantId(), org.GetOrgId()) + _, err := s.command.AddProjectGrant(ctx, management.AddProjectGrantRequestToCommand(grant.GetProjectGrant(), grant.GetGrantId(), org.GetOrgId())) if err != nil { *errors = append(*errors, &admin_pb.ImportDataError{Type: "project_grant", Id: org.GetOrgId() + "_" + grant.GetProjectGrant().GetProjectId() + "_" + grant.GetProjectGrant().GetGrantedOrgId(), Message: err.Error()}) if isCtxTimeout(ctx) { @@ -1039,7 +1046,7 @@ func importOrg2(ctx context.Context, s *Server, errors *[]*admin_pb.ImportDataEr if org.UserGrants != nil { for _, grant := range org.GetUserGrants() { logging.Debugf("import usergrant: %s", grant.GetProjectId()+"_"+grant.GetUserId()) - _, err := s.command.AddUserGrant(ctx, management.AddUserGrantRequestToDomain(grant), org.GetOrgId()) + _, err := s.command.AddUserGrant(ctx, management.AddUserGrantRequestToDomain(grant, org.GetOrgId()), nil) if err != nil { *errors = append(*errors, &admin_pb.ImportDataError{Type: "user_grant", Id: org.GetOrgId() + "_" + grant.GetProjectId() + "_" + grant.GetUserId(), Message: err.Error()}) if isCtxTimeout(ctx) { @@ -1081,7 +1088,7 @@ func importOrgMembers(ctx context.Context, s *Server, errors *[]*admin_pb.Import } for _, member := range org.GetOrgMembers() { logging.Debugf("import orgmember: %s", member.GetUserId()) - _, err := s.command.AddOrgMember(ctx, org.GetOrgId(), member.GetUserId(), member.GetRoles()...) + _, err := s.command.AddOrgMember(ctx, management.AddOrgMemberRequestToCommand(member, org.GetOrgId())) if err != nil { *errors = append(*errors, &admin_pb.ImportDataError{Type: "org_member", Id: org.GetOrgId() + "_" + member.GetUserId(), Message: err.Error()}) if isCtxTimeout(ctx) { @@ -1105,7 +1112,7 @@ func importProjectGrantMembers(ctx context.Context, s *Server, errors *[]*admin_ } for _, member := range org.GetProjectGrantMembers() { logging.Debugf("import projectgrantmember: %s", member.GetProjectId()+"_"+member.GetGrantId()+"_"+member.GetUserId()) - _, err := s.command.AddProjectGrantMember(ctx, management.AddProjectGrantMemberRequestToDomain(member)) + _, err := s.command.AddProjectGrantMember(ctx, management.AddProjectGrantMemberRequestToCommand(member, org.GetOrgId())) if err != nil { *errors = append(*errors, &admin_pb.ImportDataError{Type: "project_grant_member", Id: org.GetOrgId() + "_" + member.GetProjectId() + "_" + member.GetGrantId() + "_" + member.GetUserId(), Message: err.Error()}) if isCtxTimeout(ctx) { @@ -1129,7 +1136,7 @@ func importProjectMembers(ctx context.Context, s *Server, errors *[]*admin_pb.Im } for _, member := range org.GetProjectMembers() { logging.Debugf("import orgmember: %s", member.GetProjectId()+"_"+member.GetUserId()) - _, err := s.command.AddProjectMember(ctx, management.AddProjectMemberRequestToDomain(member), org.GetOrgId()) + _, err := s.command.AddProjectMember(ctx, management.AddProjectMemberRequestToCommand(member, org.GetOrgId())) if err != nil { *errors = append(*errors, &admin_pb.ImportDataError{Type: "project_member", Id: org.GetOrgId() + "_" + member.GetProjectId() + "_" + member.GetUserId(), Message: err.Error()}) if isCtxTimeout(ctx) { diff --git a/internal/api/grpc/admin/instance.go b/internal/api/grpc/admin/instance.go index 74f1576ae1..38fc9ec4d8 100644 --- a/internal/api/grpc/admin/instance.go +++ b/internal/api/grpc/admin/instance.go @@ -29,7 +29,8 @@ func (s *Server) ListInstanceDomains(ctx context.Context, req *admin_pb.ListInst return nil, err } return &admin_pb.ListInstanceDomainsResponse{ - Result: instance_grpc.DomainsToPb(domains.Domains), + Result: instance_grpc.DomainsToPb(domains.Domains), + SortingColumn: req.SortingColumn, Details: object.ToListDetails( domains.Count, domains.Sequence, @@ -48,7 +49,8 @@ func (s *Server) ListInstanceTrustedDomains(ctx context.Context, req *admin_pb.L return nil, err } return &admin_pb.ListInstanceTrustedDomainsResponse{ - Result: instance_grpc.TrustedDomainsToPb(domains.Domains), + Result: instance_grpc.TrustedDomainsToPb(domains.Domains), + SortingColumn: req.SortingColumn, Details: object.ToListDetails( domains.Count, domains.Sequence, diff --git a/internal/api/grpc/admin/instance_converter.go b/internal/api/grpc/admin/instance_converter.go index 397845c9e3..603d544cb8 100644 --- a/internal/api/grpc/admin/instance_converter.go +++ b/internal/api/grpc/admin/instance_converter.go @@ -51,8 +51,23 @@ func ListInstanceTrustedDomainsRequestToModel(req *admin_pb.ListInstanceTrustedD Offset: offset, Limit: limit, Asc: asc, - SortingColumn: fieldNameToInstanceDomainColumn(req.SortingColumn), + SortingColumn: fieldNameToInstanceTrustedDomainColumn(req.SortingColumn), }, Queries: queries, }, nil } + +func fieldNameToInstanceTrustedDomainColumn(fieldName instance.DomainFieldName) query.Column { + switch fieldName { + case instance.DomainFieldName_DOMAIN_FIELD_NAME_DOMAIN: + return query.InstanceTrustedDomainDomainCol + case instance.DomainFieldName_DOMAIN_FIELD_NAME_CREATION_DATE: + return query.InstanceTrustedDomainCreationDateCol + case instance.DomainFieldName_DOMAIN_FIELD_NAME_UNSPECIFIED, + instance.DomainFieldName_DOMAIN_FIELD_NAME_PRIMARY, + instance.DomainFieldName_DOMAIN_FIELD_NAME_GENERATED: + return query.InstanceTrustedDomainCreationDateCol + default: + return query.Column{} + } +} diff --git a/internal/api/grpc/admin/integration_test/import_test.go b/internal/api/grpc/admin/integration_test/import_test.go index 7d323e5ab8..3f0d364aec 100644 --- a/internal/api/grpc/admin/integration_test/import_test.go +++ b/internal/api/grpc/admin/integration_test/import_test.go @@ -259,6 +259,12 @@ func TestServer_ImportData(t *testing.T) { Data: &admin.ImportDataRequest_DataOrgs{ DataOrgs: &admin.ImportDataOrg{ Orgs: []*admin.DataOrg{ + { + OrgId: orgIDs[4], + Org: &management.AddOrgRequest{ + Name: gofakeit.ProductName(), + }, + }, { OrgId: orgIDs[3], Org: &management.AddOrgRequest{ @@ -336,6 +342,9 @@ func TestServer_ImportData(t *testing.T) { }, Success: &admin.ImportDataSuccess{ Orgs: []*admin.ImportDataSuccessOrg{ + { + OrgId: orgIDs[4], + }, { OrgId: orgIDs[3], ProjectIds: projectIDs[2:4], @@ -363,6 +372,12 @@ func TestServer_ImportData(t *testing.T) { Data: &admin.ImportDataRequest_DataOrgs{ DataOrgs: &admin.ImportDataOrg{ Orgs: []*admin.DataOrg{ + { + OrgId: orgIDs[6], + Org: &management.AddOrgRequest{ + Name: gofakeit.ProductName(), + }, + }, { OrgId: orgIDs[5], Org: &management.AddOrgRequest{ @@ -383,6 +398,11 @@ func TestServer_ImportData(t *testing.T) { RoleKey: "role1", DisplayName: "role1", }, + { + ProjectId: projectIDs[4], + RoleKey: "role2", + DisplayName: "role2", + }, }, HumanUsers: []*v1.DataHumanUser{ { @@ -437,16 +457,20 @@ func TestServer_ImportData(t *testing.T) { { Type: "project_grant_member", Id: orgIDs[5] + "_" + projectIDs[4] + "_" + grantIDs[5] + "_" + userIDs[2], - Message: "ID=V3-DKcYh Message=Errors.Project.Member.AlreadyExists Parent=(ERROR: duplicate key value violates unique constraint \"unique_constraints_pkey\" (SQLSTATE 23505))", + Message: "ID=PROJECT-37fug Message=Errors.AlreadyExists", }, }, Success: &admin.ImportDataSuccess{ Orgs: []*admin.ImportDataSuccessOrg{ + { + OrgId: orgIDs[6], + }, { OrgId: orgIDs[5], ProjectIds: projectIDs[4:5], ProjectRoles: []string{ projectIDs[4] + "_role1", + projectIDs[4] + "_role2", }, HumanUserIds: userIDs[2:3], ProjectGrants: []*admin.ImportDataSuccessProjectGrant{ diff --git a/internal/api/grpc/admin/org.go b/internal/api/grpc/admin/org.go index 90b99ca208..a2b55cf21c 100644 --- a/internal/api/grpc/admin/org.go +++ b/internal/api/grpc/admin/org.go @@ -77,7 +77,7 @@ func (s *Server) SetUpOrg(ctx context.Context, req *admin_pb.SetUpOrgRequest) (* if err != nil { return nil, err } - human := setUpOrgHumanToCommand(req.User.(*admin_pb.SetUpOrgRequest_Human_).Human) //TODO: handle machine + human := setUpOrgHumanToCommand(req.User.(*admin_pb.SetUpOrgRequest_Human_).Human) // TODO: handle machine createdOrg, err := s.command.SetUpOrg(ctx, &command.OrgSetup{ Name: req.Org.Name, CustomDomain: req.Org.Domain, @@ -92,8 +92,8 @@ func (s *Server) SetUpOrg(ctx context.Context, req *admin_pb.SetUpOrgRequest) (* return nil, err } var userID string - if len(createdOrg.CreatedAdmins) == 1 { - userID = createdOrg.CreatedAdmins[0].ID + if len(createdOrg.OrgAdmins) == 1 { + userID = createdOrg.OrgAdmins[0].GetID() } return &admin_pb.SetUpOrgResponse{ Details: object.DomainToAddDetailsPb(createdOrg.ObjectDetails), diff --git a/internal/api/grpc/app/v2beta/app.go b/internal/api/grpc/app/v2beta/app.go new file mode 100644 index 0000000000..e751bf503f --- /dev/null +++ b/internal/api/grpc/app/v2beta/app.go @@ -0,0 +1,209 @@ +package app + +import ( + "context" + "strings" + "time" + + "connectrpc.com/connect" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/api/grpc/app/v2beta/convert" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/zerrors" + app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta" +) + +func (s *Server) CreateApplication(ctx context.Context, req *connect.Request[app.CreateApplicationRequest]) (*connect.Response[app.CreateApplicationResponse], error) { + switch t := req.Msg.GetCreationRequestType().(type) { + case *app.CreateApplicationRequest_ApiRequest: + apiApp, err := s.command.AddAPIApplication(ctx, convert.CreateAPIApplicationRequestToDomain(req.Msg.GetName(), req.Msg.GetProjectId(), req.Msg.GetId(), t.ApiRequest), "") + if err != nil { + return nil, err + } + + return connect.NewResponse(&app.CreateApplicationResponse{ + AppId: apiApp.AppID, + CreationDate: timestamppb.New(apiApp.ChangeDate), + CreationResponseType: &app.CreateApplicationResponse_ApiResponse{ + ApiResponse: &app.CreateAPIApplicationResponse{ + ClientId: apiApp.ClientID, + ClientSecret: apiApp.ClientSecretString, + }, + }, + }), nil + + case *app.CreateApplicationRequest_OidcRequest: + oidcAppRequest, err := convert.CreateOIDCAppRequestToDomain(req.Msg.GetName(), req.Msg.GetProjectId(), req.Msg.GetOidcRequest()) + if err != nil { + return nil, err + } + + oidcApp, err := s.command.AddOIDCApplication(ctx, oidcAppRequest, "") + if err != nil { + return nil, err + } + + return connect.NewResponse(&app.CreateApplicationResponse{ + AppId: oidcApp.AppID, + CreationDate: timestamppb.New(oidcApp.ChangeDate), + CreationResponseType: &app.CreateApplicationResponse_OidcResponse{ + OidcResponse: &app.CreateOIDCApplicationResponse{ + ClientId: oidcApp.ClientID, + ClientSecret: oidcApp.ClientSecretString, + NoneCompliant: oidcApp.Compliance.NoneCompliant, + ComplianceProblems: convert.ComplianceProblemsToLocalizedMessages(oidcApp.Compliance.Problems), + }, + }, + }), nil + + case *app.CreateApplicationRequest_SamlRequest: + samlAppRequest, err := convert.CreateSAMLAppRequestToDomain(req.Msg.GetName(), req.Msg.GetProjectId(), req.Msg.GetSamlRequest()) + if err != nil { + return nil, err + } + + samlApp, err := s.command.AddSAMLApplication(ctx, samlAppRequest, "") + if err != nil { + return nil, err + } + + return connect.NewResponse(&app.CreateApplicationResponse{ + AppId: samlApp.AppID, + CreationDate: timestamppb.New(samlApp.ChangeDate), + CreationResponseType: &app.CreateApplicationResponse_SamlResponse{ + SamlResponse: &app.CreateSAMLApplicationResponse{}, + }, + }), nil + default: + return nil, zerrors.ThrowInvalidArgument(nil, "APP-0iiN46", "unknown app type") + } +} + +func (s *Server) UpdateApplication(ctx context.Context, req *connect.Request[app.UpdateApplicationRequest]) (*connect.Response[app.UpdateApplicationResponse], error) { + var changedTime time.Time + + if name := strings.TrimSpace(req.Msg.GetName()); name != "" { + updatedDetails, err := s.command.UpdateApplicationName( + ctx, + req.Msg.GetProjectId(), + &domain.ChangeApp{ + AppID: req.Msg.GetId(), + AppName: name, + }, + "", + ) + if err != nil { + return nil, err + } + + changedTime = updatedDetails.EventDate + } + + switch t := req.Msg.GetUpdateRequestType().(type) { + case *app.UpdateApplicationRequest_ApiConfigurationRequest: + updatedAPIApp, err := s.command.UpdateAPIApplication(ctx, convert.UpdateAPIApplicationConfigurationRequestToDomain(req.Msg.GetId(), req.Msg.GetProjectId(), t.ApiConfigurationRequest), "") + if err != nil { + return nil, err + } + + changedTime = updatedAPIApp.ChangeDate + + case *app.UpdateApplicationRequest_OidcConfigurationRequest: + oidcApp, err := convert.UpdateOIDCAppConfigRequestToDomain(req.Msg.GetId(), req.Msg.GetProjectId(), t.OidcConfigurationRequest) + if err != nil { + return nil, err + } + + updatedOIDCApp, err := s.command.UpdateOIDCApplication(ctx, oidcApp, "") + if err != nil { + return nil, err + } + + changedTime = updatedOIDCApp.ChangeDate + + case *app.UpdateApplicationRequest_SamlConfigurationRequest: + samlApp, err := convert.UpdateSAMLAppConfigRequestToDomain(req.Msg.GetId(), req.Msg.GetProjectId(), t.SamlConfigurationRequest) + if err != nil { + return nil, err + } + + updatedSAMLApp, err := s.command.UpdateSAMLApplication(ctx, samlApp, "") + if err != nil { + return nil, err + } + + changedTime = updatedSAMLApp.ChangeDate + } + + return connect.NewResponse(&app.UpdateApplicationResponse{ + ChangeDate: timestamppb.New(changedTime), + }), nil +} + +func (s *Server) DeleteApplication(ctx context.Context, req *connect.Request[app.DeleteApplicationRequest]) (*connect.Response[app.DeleteApplicationResponse], error) { + details, err := s.command.RemoveApplication(ctx, req.Msg.GetProjectId(), req.Msg.GetId(), "") + if err != nil { + return nil, err + } + + return connect.NewResponse(&app.DeleteApplicationResponse{ + DeletionDate: timestamppb.New(details.EventDate), + }), nil +} + +func (s *Server) DeactivateApplication(ctx context.Context, req *connect.Request[app.DeactivateApplicationRequest]) (*connect.Response[app.DeactivateApplicationResponse], error) { + details, err := s.command.DeactivateApplication(ctx, req.Msg.GetProjectId(), req.Msg.GetId(), "") + if err != nil { + return nil, err + } + + return connect.NewResponse(&app.DeactivateApplicationResponse{ + DeactivationDate: timestamppb.New(details.EventDate), + }), nil + +} + +func (s *Server) ReactivateApplication(ctx context.Context, req *connect.Request[app.ReactivateApplicationRequest]) (*connect.Response[app.ReactivateApplicationResponse], error) { + details, err := s.command.ReactivateApplication(ctx, req.Msg.GetProjectId(), req.Msg.GetId(), "") + if err != nil { + return nil, err + } + + return connect.NewResponse(&app.ReactivateApplicationResponse{ + ReactivationDate: timestamppb.New(details.EventDate), + }), nil + +} + +func (s *Server) RegenerateClientSecret(ctx context.Context, req *connect.Request[app.RegenerateClientSecretRequest]) (*connect.Response[app.RegenerateClientSecretResponse], error) { + var secret string + var changeDate time.Time + + switch req.Msg.GetAppType().(type) { + case *app.RegenerateClientSecretRequest_IsApi: + config, err := s.command.ChangeAPIApplicationSecret(ctx, req.Msg.GetProjectId(), req.Msg.GetApplicationId(), "") + if err != nil { + return nil, err + } + secret = config.ClientSecretString + changeDate = config.ChangeDate + + case *app.RegenerateClientSecretRequest_IsOidc: + config, err := s.command.ChangeOIDCApplicationSecret(ctx, req.Msg.GetProjectId(), req.Msg.GetApplicationId(), "") + if err != nil { + return nil, err + } + + secret = config.ClientSecretString + changeDate = config.ChangeDate + + default: + return nil, zerrors.ThrowInvalidArgument(nil, "APP-aLWIzw", "unknown app type") + } + + return connect.NewResponse(&app.RegenerateClientSecretResponse{ + ClientSecret: secret, + CreationDate: timestamppb.New(changeDate), + }), nil +} diff --git a/internal/api/grpc/app/v2beta/app_key.go b/internal/api/grpc/app/v2beta/app_key.go new file mode 100644 index 0000000000..087ff90916 --- /dev/null +++ b/internal/api/grpc/app/v2beta/app_key.go @@ -0,0 +1,48 @@ +package app + +import ( + "context" + "strings" + + "connectrpc.com/connect" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/api/grpc/app/v2beta/convert" + app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta" +) + +func (s *Server) CreateApplicationKey(ctx context.Context, req *connect.Request[app.CreateApplicationKeyRequest]) (*connect.Response[app.CreateApplicationKeyResponse], error) { + domainReq := convert.CreateAPIClientKeyRequestToDomain(req.Msg) + + appKey, err := s.command.AddApplicationKey(ctx, domainReq, "") + if err != nil { + return nil, err + } + + keyDetails, err := appKey.Detail() + if err != nil { + return nil, err + } + + return connect.NewResponse(&app.CreateApplicationKeyResponse{ + Id: appKey.KeyID, + CreationDate: timestamppb.New(appKey.ChangeDate), + KeyDetails: keyDetails, + }), nil +} + +func (s *Server) DeleteApplicationKey(ctx context.Context, req *connect.Request[app.DeleteApplicationKeyRequest]) (*connect.Response[app.DeleteApplicationKeyResponse], error) { + deletionDetails, err := s.command.RemoveApplicationKey(ctx, + strings.TrimSpace(req.Msg.GetProjectId()), + strings.TrimSpace(req.Msg.GetApplicationId()), + strings.TrimSpace(req.Msg.GetId()), + strings.TrimSpace(req.Msg.GetOrganizationId()), + ) + if err != nil { + return nil, err + } + + return connect.NewResponse(&app.DeleteApplicationKeyResponse{ + DeletionDate: timestamppb.New(deletionDetails.EventDate), + }), nil +} diff --git a/internal/api/grpc/app/v2beta/convert/api_app.go b/internal/api/grpc/app/v2beta/convert/api_app.go new file mode 100644 index 0000000000..4900d534cb --- /dev/null +++ b/internal/api/grpc/app/v2beta/convert/api_app.go @@ -0,0 +1,98 @@ +package convert + +import ( + "strings" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore/v1/models" + "github.com/zitadel/zitadel/internal/query" + app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta" +) + +func CreateAPIApplicationRequestToDomain(name, projectID, appID string, app *app.CreateAPIApplicationRequest) *domain.APIApp { + return &domain.APIApp{ + ObjectRoot: models.ObjectRoot{ + AggregateID: projectID, + }, + AppName: name, + AppID: appID, + AuthMethodType: apiAuthMethodTypeToDomain(app.GetAuthMethodType()), + } +} + +func UpdateAPIApplicationConfigurationRequestToDomain(appID, projectID string, app *app.UpdateAPIApplicationConfigurationRequest) *domain.APIApp { + return &domain.APIApp{ + ObjectRoot: models.ObjectRoot{ + AggregateID: projectID, + }, + AppID: appID, + AuthMethodType: apiAuthMethodTypeToDomain(app.GetAuthMethodType()), + } +} + +func appAPIConfigToPb(apiApp *query.APIApp) app.ApplicationConfig { + return &app.Application_ApiConfig{ + ApiConfig: &app.APIConfig{ + ClientId: apiApp.ClientID, + AuthMethodType: apiAuthMethodTypeToPb(apiApp.AuthMethodType), + }, + } +} + +func apiAuthMethodTypeToDomain(authType app.APIAuthMethodType) domain.APIAuthMethodType { + switch authType { + case app.APIAuthMethodType_API_AUTH_METHOD_TYPE_BASIC: + return domain.APIAuthMethodTypeBasic + case app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT: + return domain.APIAuthMethodTypePrivateKeyJWT + default: + return domain.APIAuthMethodTypeBasic + } +} + +func apiAuthMethodTypeToPb(methodType domain.APIAuthMethodType) app.APIAuthMethodType { + switch methodType { + case domain.APIAuthMethodTypeBasic: + return app.APIAuthMethodType_API_AUTH_METHOD_TYPE_BASIC + case domain.APIAuthMethodTypePrivateKeyJWT: + return app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT + default: + return app.APIAuthMethodType_API_AUTH_METHOD_TYPE_BASIC + } +} + +func GetApplicationKeyQueriesRequestToDomain(orgID, projectID, appID string) ([]query.SearchQuery, error) { + var searchQueries []query.SearchQuery + + orgID, projectID, appID = strings.TrimSpace(orgID), strings.TrimSpace(projectID), strings.TrimSpace(appID) + + if orgID != "" { + resourceOwner, err := query.NewAuthNKeyResourceOwnerQuery(orgID) + if err != nil { + return nil, err + } + + searchQueries = append(searchQueries, resourceOwner) + } + + if projectID != "" { + aggregateID, err := query.NewAuthNKeyAggregateIDQuery(projectID) + if err != nil { + return nil, err + } + + searchQueries = append(searchQueries, aggregateID) + } + + if appID != "" { + objectID, err := query.NewAuthNKeyObjectIDQuery(appID) + + if err != nil { + return nil, err + } + + searchQueries = append(searchQueries, objectID) + } + + return searchQueries, nil +} diff --git a/internal/api/grpc/app/v2beta/convert/api_app_test.go b/internal/api/grpc/app/v2beta/convert/api_app_test.go new file mode 100644 index 0000000000..dcb87d712f --- /dev/null +++ b/internal/api/grpc/app/v2beta/convert/api_app_test.go @@ -0,0 +1,218 @@ +package convert + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore/v1/models" + app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta" +) + +func TestCreateAPIApplicationRequestToDomain(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + appName string + projectID string + appID string + req *app.CreateAPIApplicationRequest + want *domain.APIApp + }{ + { + name: "basic auth method", + appName: "my-app", + projectID: "proj-1", + appID: "someID", + req: &app.CreateAPIApplicationRequest{ + AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_BASIC, + }, + want: &domain.APIApp{ + ObjectRoot: models.ObjectRoot{AggregateID: "proj-1"}, + AppName: "my-app", + AuthMethodType: domain.APIAuthMethodTypeBasic, + AppID: "someID", + }, + }, + { + name: "private key jwt", + appName: "jwt-app", + projectID: "proj-2", + req: &app.CreateAPIApplicationRequest{ + AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT, + }, + want: &domain.APIApp{ + ObjectRoot: models.ObjectRoot{AggregateID: "proj-2"}, + AppName: "jwt-app", + AuthMethodType: domain.APIAuthMethodTypePrivateKeyJWT, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + // When + got := CreateAPIApplicationRequestToDomain(tt.appName, tt.projectID, tt.appID, tt.req) + + // Then + assert.Equal(t, tt.want, got) + }) + } +} + +func TestUpdateAPIApplicationConfigurationRequestToDomain(t *testing.T) { + t.Parallel() + tests := []struct { + name string + appID string + projectID string + req *app.UpdateAPIApplicationConfigurationRequest + want *domain.APIApp + }{ + { + name: "basic auth method", + appID: "app-1", + projectID: "proj-1", + req: &app.UpdateAPIApplicationConfigurationRequest{ + AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_BASIC, + }, + want: &domain.APIApp{ + ObjectRoot: models.ObjectRoot{AggregateID: "proj-1"}, + AppID: "app-1", + AuthMethodType: domain.APIAuthMethodTypeBasic, + }, + }, + { + name: "private key jwt", + appID: "app-2", + projectID: "proj-2", + req: &app.UpdateAPIApplicationConfigurationRequest{ + AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT, + }, + want: &domain.APIApp{ + ObjectRoot: models.ObjectRoot{AggregateID: "proj-2"}, + AppID: "app-2", + AuthMethodType: domain.APIAuthMethodTypePrivateKeyJWT, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + // When + got := UpdateAPIApplicationConfigurationRequestToDomain(tt.appID, tt.projectID, tt.req) + + // Then + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_apiAuthMethodTypeToPb(t *testing.T) { + t.Parallel() + + tt := []struct { + name string + methodType domain.APIAuthMethodType + expectedResult app.APIAuthMethodType + }{ + { + name: "basic auth method", + methodType: domain.APIAuthMethodTypeBasic, + expectedResult: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_BASIC, + }, + { + name: "private key jwt", + methodType: domain.APIAuthMethodTypePrivateKeyJWT, + expectedResult: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT, + }, + { + name: "unknown auth method defaults to basic", + expectedResult: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_BASIC, + }, + } + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + // When + res := apiAuthMethodTypeToPb(tc.methodType) + + // Then + assert.Equal(t, tc.expectedResult, res) + }) + } +} +func TestGetApplicationKeyQueriesRequestToDomain(t *testing.T) { + t.Parallel() + + tt := []struct { + testName string + inputOrgID string + inputProjectID string + inputAppID string + + expectedQueriesLength int + }{ + { + testName: "all IDs provided", + inputOrgID: "org-1", + inputProjectID: "proj-1", + inputAppID: "app-1", + expectedQueriesLength: 3, + }, + { + testName: "only org ID", + inputOrgID: "org-1", + inputProjectID: " ", + inputAppID: "", + expectedQueriesLength: 1, + }, + { + testName: "only project ID", + inputOrgID: "", + inputProjectID: "proj-1", + inputAppID: " ", + expectedQueriesLength: 1, + }, + { + testName: "only app ID", + inputOrgID: " ", + inputProjectID: "", + inputAppID: "app-1", + expectedQueriesLength: 1, + }, + { + testName: "empty IDs", + inputOrgID: " ", + inputProjectID: " ", + inputAppID: " ", + expectedQueriesLength: 0, + }, + { + testName: "with spaces", + inputOrgID: " org-1 ", + inputProjectID: " proj-1 ", + inputAppID: " app-1 ", + expectedQueriesLength: 3, + }, + } + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + + // When + got, err := GetApplicationKeyQueriesRequestToDomain(tc.inputOrgID, tc.inputProjectID, tc.inputAppID) + + // Then + require.NoError(t, err) + + assert.Len(t, got, tc.expectedQueriesLength) + }) + } +} diff --git a/internal/api/grpc/app/v2beta/convert/convert.go b/internal/api/grpc/app/v2beta/convert/convert.go new file mode 100644 index 0000000000..a0a1d5ef05 --- /dev/null +++ b/internal/api/grpc/app/v2beta/convert/convert.go @@ -0,0 +1,262 @@ +package convert + +import ( + "net/url" + "strings" + + "github.com/muhlemmer/gu" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/api/grpc/filter/v2" + "github.com/zitadel/zitadel/internal/config/systemdefaults" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore/v1/models" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/zerrors" + app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta" +) + +func AppToPb(query_app *query.App) *app.Application { + if query_app == nil { + return &app.Application{} + } + + return &app.Application{ + Id: query_app.ID, + CreationDate: timestamppb.New(query_app.CreationDate), + ChangeDate: timestamppb.New(query_app.ChangeDate), + State: appStateToPb(query_app.State), + Name: query_app.Name, + Config: appConfigToPb(query_app), + } +} + +func AppsToPb(queryApps []*query.App) []*app.Application { + pbApps := make([]*app.Application, len(queryApps)) + + for i, queryApp := range queryApps { + pbApps[i] = AppToPb(queryApp) + } + + return pbApps +} + +func ListApplicationsRequestToModel(sysDefaults systemdefaults.SystemDefaults, req *app.ListApplicationsRequest) (*query.AppSearchQueries, error) { + offset, limit, asc, err := filter.PaginationPbToQuery(sysDefaults, req.GetPagination()) + if err != nil { + return nil, err + } + + queries, err := appQueriesToModel(req.GetFilters()) + if err != nil { + return nil, err + } + projectQuery, err := query.NewAppProjectIDSearchQuery(req.GetProjectId()) + if err != nil { + return nil, err + } + + queries = append(queries, projectQuery) + return &query.AppSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + SortingColumn: appSortingToColumn(req.GetSortingColumn()), + }, + + Queries: queries, + }, nil +} + +func appSortingToColumn(sortingCriteria app.AppSorting) query.Column { + switch sortingCriteria { + case app.AppSorting_APP_SORT_BY_CHANGE_DATE: + return query.AppColumnChangeDate + case app.AppSorting_APP_SORT_BY_CREATION_DATE: + return query.AppColumnCreationDate + case app.AppSorting_APP_SORT_BY_NAME: + return query.AppColumnName + case app.AppSorting_APP_SORT_BY_STATE: + return query.AppColumnState + case app.AppSorting_APP_SORT_BY_ID: + fallthrough + default: + return query.AppColumnID + } +} + +func appStateToPb(state domain.AppState) app.AppState { + switch state { + case domain.AppStateActive: + return app.AppState_APP_STATE_ACTIVE + case domain.AppStateInactive: + return app.AppState_APP_STATE_INACTIVE + case domain.AppStateRemoved: + return app.AppState_APP_STATE_REMOVED + case domain.AppStateUnspecified: + fallthrough + default: + return app.AppState_APP_STATE_UNSPECIFIED + } +} + +func appConfigToPb(app *query.App) app.ApplicationConfig { + if app.OIDCConfig != nil { + return appOIDCConfigToPb(app.OIDCConfig) + } + if app.SAMLConfig != nil { + return appSAMLConfigToPb(app.SAMLConfig) + } + return appAPIConfigToPb(app.APIConfig) +} + +func loginVersionToDomain(version *app.LoginVersion) (*domain.LoginVersion, *string, error) { + switch v := version.GetVersion().(type) { + case nil: + return gu.Ptr(domain.LoginVersionUnspecified), gu.Ptr(""), nil + case *app.LoginVersion_LoginV1: + return gu.Ptr(domain.LoginVersion1), gu.Ptr(""), nil + case *app.LoginVersion_LoginV2: + _, err := url.Parse(v.LoginV2.GetBaseUri()) + return gu.Ptr(domain.LoginVersion2), gu.Ptr(v.LoginV2.GetBaseUri()), err + default: + return gu.Ptr(domain.LoginVersionUnspecified), gu.Ptr(""), nil + } +} + +func loginVersionToPb(version domain.LoginVersion, baseURI *string) *app.LoginVersion { + switch version { + case domain.LoginVersionUnspecified: + return nil + case domain.LoginVersion1: + return &app.LoginVersion{Version: &app.LoginVersion_LoginV1{LoginV1: &app.LoginV1{}}} + case domain.LoginVersion2: + return &app.LoginVersion{Version: &app.LoginVersion_LoginV2{LoginV2: &app.LoginV2{BaseUri: baseURI}}} + default: + return nil + } +} + +func appQueriesToModel(queries []*app.ApplicationSearchFilter) (toReturn []query.SearchQuery, err error) { + toReturn = make([]query.SearchQuery, len(queries)) + for i, query := range queries { + toReturn[i], err = appQueryToModel(query) + if err != nil { + return nil, err + } + } + return toReturn, nil +} + +func appQueryToModel(appQuery *app.ApplicationSearchFilter) (query.SearchQuery, error) { + switch q := appQuery.GetFilter().(type) { + case *app.ApplicationSearchFilter_NameFilter: + return query.NewAppNameSearchQuery(filter.TextMethodPbToQuery(q.NameFilter.GetMethod()), q.NameFilter.Name) + case *app.ApplicationSearchFilter_StateFilter: + return query.NewAppStateSearchQuery(domain.AppState(q.StateFilter)) + case *app.ApplicationSearchFilter_ApiAppOnly: + return query.NewNotNullQuery(query.AppAPIConfigColumnAppID) + case *app.ApplicationSearchFilter_OidcAppOnly: + return query.NewNotNullQuery(query.AppOIDCConfigColumnAppID) + case *app.ApplicationSearchFilter_SamlAppOnly: + return query.NewNotNullQuery(query.AppSAMLConfigColumnAppID) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "CONV-z2mAGy", "List.Query.Invalid") + } +} + +func CreateAPIClientKeyRequestToDomain(key *app.CreateApplicationKeyRequest) *domain.ApplicationKey { + return &domain.ApplicationKey{ + ObjectRoot: models.ObjectRoot{ + AggregateID: strings.TrimSpace(key.GetProjectId()), + }, + ExpirationDate: key.GetExpirationDate().AsTime(), + Type: domain.AuthNKeyTypeJSON, + ApplicationID: strings.TrimSpace(key.GetAppId()), + } +} + +func ListApplicationKeysRequestToDomain(sysDefaults systemdefaults.SystemDefaults, req *app.ListApplicationKeysRequest) (*query.AuthNKeySearchQueries, error) { + var queries []query.SearchQuery + + switch req.GetResourceId().(type) { + case *app.ListApplicationKeysRequest_ApplicationId: + object, err := query.NewAuthNKeyObjectIDQuery(strings.TrimSpace(req.GetApplicationId())) + if err != nil { + return nil, err + } + queries = append(queries, object) + case *app.ListApplicationKeysRequest_OrganizationId: + resourceOwner, err := query.NewAuthNKeyResourceOwnerQuery(strings.TrimSpace(req.GetOrganizationId())) + if err != nil { + return nil, err + } + queries = append(queries, resourceOwner) + case *app.ListApplicationKeysRequest_ProjectId: + aggregate, err := query.NewAuthNKeyAggregateIDQuery(strings.TrimSpace(req.GetProjectId())) + if err != nil { + return nil, err + } + queries = append(queries, aggregate) + case nil: + + default: + return nil, zerrors.ThrowInvalidArgument(nil, "CONV-t3ENme", "unexpected resource id") + } + + offset, limit, asc, err := filter.PaginationPbToQuery(sysDefaults, req.GetPagination()) + if err != nil { + return nil, err + } + + return &query.AuthNKeySearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + SortingColumn: appKeysSortingToColumn(req.GetSortingColumn()), + }, + + Queries: queries, + }, nil +} + +func appKeysSortingToColumn(sortingCriteria app.ApplicationKeysSorting) query.Column { + switch sortingCriteria { + case app.ApplicationKeysSorting_APPLICATION_KEYS_SORT_BY_PROJECT_ID: + return query.AuthNKeyColumnAggregateID + case app.ApplicationKeysSorting_APPLICATION_KEYS_SORT_BY_CREATION_DATE: + return query.AuthNKeyColumnCreationDate + case app.ApplicationKeysSorting_APPLICATION_KEYS_SORT_BY_EXPIRATION: + return query.AuthNKeyColumnExpiration + case app.ApplicationKeysSorting_APPLICATION_KEYS_SORT_BY_ORGANIZATION_ID: + return query.AuthNKeyColumnResourceOwner + case app.ApplicationKeysSorting_APPLICATION_KEYS_SORT_BY_TYPE: + return query.AuthNKeyColumnType + case app.ApplicationKeysSorting_APPLICATION_KEYS_SORT_BY_APPLICATION_ID: + return query.AuthNKeyColumnObjectID + case app.ApplicationKeysSorting_APPLICATION_KEYS_SORT_BY_ID: + fallthrough + default: + return query.AuthNKeyColumnID + } +} + +func ApplicationKeysToPb(keys []*query.AuthNKey) []*app.ApplicationKey { + pbAppKeys := make([]*app.ApplicationKey, len(keys)) + + for i, k := range keys { + pbKey := &app.ApplicationKey{ + Id: k.ID, + ApplicationId: k.ApplicationID, + ProjectId: k.AggregateID, + CreationDate: timestamppb.New(k.CreationDate), + OrganizationId: k.ResourceOwner, + ExpirationDate: timestamppb.New(k.Expiration), + } + pbAppKeys[i] = pbKey + } + + return pbAppKeys +} diff --git a/internal/api/grpc/app/v2beta/convert/convert_test.go b/internal/api/grpc/app/v2beta/convert/convert_test.go new file mode 100644 index 0000000000..8715d2a5dd --- /dev/null +++ b/internal/api/grpc/app/v2beta/convert/convert_test.go @@ -0,0 +1,704 @@ +package convert + +import ( + "errors" + "fmt" + "net/url" + "testing" + "time" + + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/durationpb" + "google.golang.org/protobuf/types/known/timestamppb" + + filter "github.com/zitadel/zitadel/internal/api/grpc/filter/v2beta" + "github.com/zitadel/zitadel/internal/config/systemdefaults" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/zerrors" + app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta" + filter_pb_v2 "github.com/zitadel/zitadel/pkg/grpc/filter/v2" + filter_pb_v2_beta "github.com/zitadel/zitadel/pkg/grpc/filter/v2beta" +) + +func TestAppToPb(t *testing.T) { + t.Parallel() + + now := time.Now() + + tt := []struct { + testName string + inputQueryApp *query.App + expectedPbApp *app.Application + }{ + { + testName: "full app conversion", + inputQueryApp: &query.App{ + ID: "id", + CreationDate: now, + ChangeDate: now, + State: domain.AppStateActive, + Name: "test-app", + APIConfig: &query.APIApp{}, + }, + expectedPbApp: &app.Application{ + Id: "id", + CreationDate: timestamppb.New(now), + ChangeDate: timestamppb.New(now), + State: app.AppState_APP_STATE_ACTIVE, + Name: "test-app", + Config: &app.Application_ApiConfig{ + ApiConfig: &app.APIConfig{}, + }, + }, + }, + { + testName: "nil app", + inputQueryApp: nil, + expectedPbApp: &app.Application{}, + }, + } + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + + // When + res := AppToPb(tc.inputQueryApp) + + // Then + assert.Equal(t, tc.expectedPbApp, res) + }) + } +} + +func TestListApplicationsRequestToModel(t *testing.T) { + t.Parallel() + + validSearchByNameQuery, err := query.NewAppNameSearchQuery(filter.TextMethodPbToQuery(filter_pb_v2_beta.TextFilterMethod_TEXT_FILTER_METHOD_EQUALS), "test") + require.NoError(t, err) + + validSearchByProjectQuery, err := query.NewAppProjectIDSearchQuery("project1") + require.NoError(t, err) + + sysDefaults := systemdefaults.SystemDefaults{DefaultQueryLimit: 100, MaxQueryLimit: 150} + + tt := []struct { + testName string + req *app.ListApplicationsRequest + + expectedResponse *query.AppSearchQueries + expectedError error + }{ + { + testName: "invalid pagination limit", + req: &app.ListApplicationsRequest{ + Pagination: &filter_pb_v2.PaginationRequest{Asc: true, Limit: uint32(sysDefaults.MaxQueryLimit + 1)}, + }, + expectedResponse: nil, + expectedError: zerrors.ThrowInvalidArgumentf(fmt.Errorf("given: %d, allowed: %d", sysDefaults.MaxQueryLimit+1, sysDefaults.MaxQueryLimit), "QUERY-4M0fs", "Errors.Query.LimitExceeded"), + }, + { + testName: "empty request", + req: &app.ListApplicationsRequest{ + ProjectId: "project1", + Pagination: &filter_pb_v2.PaginationRequest{Asc: true}, + }, + expectedResponse: &query.AppSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: 0, + Limit: 100, + Asc: true, + SortingColumn: query.AppColumnID, + }, + Queries: []query.SearchQuery{ + validSearchByProjectQuery, + }, + }, + }, + { + testName: "valid request", + req: &app.ListApplicationsRequest{ + ProjectId: "project1", + Filters: []*app.ApplicationSearchFilter{ + { + Filter: &app.ApplicationSearchFilter_NameFilter{NameFilter: &app.ApplicationNameQuery{Name: "test"}}, + }, + }, + SortingColumn: app.AppSorting_APP_SORT_BY_NAME, + Pagination: &filter_pb_v2.PaginationRequest{Asc: true}, + }, + + expectedResponse: &query.AppSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: 0, + Limit: 100, + Asc: true, + SortingColumn: query.AppColumnName, + }, + Queries: []query.SearchQuery{ + validSearchByNameQuery, + validSearchByProjectQuery, + }, + }, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + + // When + got, err := ListApplicationsRequestToModel(sysDefaults, tc.req) + + // Then + assert.Equal(t, tc.expectedError, err) + assert.Equal(t, tc.expectedResponse, got) + }) + } +} + +func TestAppSortingToColumn(t *testing.T) { + t.Parallel() + + tt := []struct { + name string + sorting app.AppSorting + expected query.Column + }{ + { + name: "sort by change date", + sorting: app.AppSorting_APP_SORT_BY_CHANGE_DATE, + expected: query.AppColumnChangeDate, + }, + { + name: "sort by creation date", + sorting: app.AppSorting_APP_SORT_BY_CREATION_DATE, + expected: query.AppColumnCreationDate, + }, + { + name: "sort by name", + sorting: app.AppSorting_APP_SORT_BY_NAME, + expected: query.AppColumnName, + }, + { + name: "sort by state", + sorting: app.AppSorting_APP_SORT_BY_STATE, + expected: query.AppColumnState, + }, + { + name: "sort by ID", + sorting: app.AppSorting_APP_SORT_BY_ID, + expected: query.AppColumnID, + }, + { + name: "unknown sorting defaults to ID", + sorting: app.AppSorting(99), + expected: query.AppColumnID, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + // When + result := appSortingToColumn(tc.sorting) + + // Then + assert.Equal(t, tc.expected, result) + }) + } +} + +func TestAppStateToPb(t *testing.T) { + t.Parallel() + + tt := []struct { + name string + state domain.AppState + expected app.AppState + }{ + { + name: "active state", + state: domain.AppStateActive, + expected: app.AppState_APP_STATE_ACTIVE, + }, + { + name: "inactive state", + state: domain.AppStateInactive, + expected: app.AppState_APP_STATE_INACTIVE, + }, + { + name: "removed state", + state: domain.AppStateRemoved, + expected: app.AppState_APP_STATE_REMOVED, + }, + { + name: "unspecified state", + state: domain.AppStateUnspecified, + expected: app.AppState_APP_STATE_UNSPECIFIED, + }, + { + name: "unknown state defaults to unspecified", + state: domain.AppState(99), + expected: app.AppState_APP_STATE_UNSPECIFIED, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + // When + result := appStateToPb(tc.state) + + // Then + assert.Equal(t, tc.expected, result) + }) + } +} + +func TestAppConfigToPb(t *testing.T) { + t.Parallel() + + tt := []struct { + name string + app *query.App + expected app.ApplicationConfig + }{ + { + name: "OIDC config", + app: &query.App{ + OIDCConfig: &query.OIDCApp{}, + }, + expected: &app.Application_OidcConfig{ + OidcConfig: &app.OIDCConfig{ + ResponseTypes: []app.OIDCResponseType{}, + GrantTypes: []app.OIDCGrantType{}, + ComplianceProblems: []*app.OIDCLocalizedMessage{}, + ClockSkew: &durationpb.Duration{}, + }, + }, + }, + { + name: "SAML config", + app: &query.App{ + SAMLConfig: &query.SAMLApp{}, + }, + expected: &app.Application_SamlConfig{ + SamlConfig: &app.SAMLConfig{ + Metadata: &app.SAMLConfig_MetadataXml{}, + }, + }, + }, + { + name: "API config", + app: &query.App{ + APIConfig: &query.APIApp{}, + }, + expected: &app.Application_ApiConfig{ + ApiConfig: &app.APIConfig{}, + }, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + // When + result := appConfigToPb(tc.app) + + // Then + assert.Equal(t, tc.expected, result) + }) + } +} + +func TestLoginVersionToDomain(t *testing.T) { + t.Parallel() + + tt := []struct { + name string + version *app.LoginVersion + expectedVer *domain.LoginVersion + expectedURI *string + expectedError error + }{ + { + name: "nil version", + version: nil, + expectedVer: gu.Ptr(domain.LoginVersionUnspecified), + expectedURI: gu.Ptr(""), + }, + { + name: "login v1", + version: &app.LoginVersion{Version: &app.LoginVersion_LoginV1{LoginV1: &app.LoginV1{}}}, + expectedVer: gu.Ptr(domain.LoginVersion1), + expectedURI: gu.Ptr(""), + }, + { + name: "login v2 valid URI", + version: &app.LoginVersion{Version: &app.LoginVersion_LoginV2{LoginV2: &app.LoginV2{BaseUri: gu.Ptr("https://valid.url")}}}, + expectedVer: gu.Ptr(domain.LoginVersion2), + expectedURI: gu.Ptr("https://valid.url"), + }, + { + name: "login v2 invalid URI", + version: &app.LoginVersion{Version: &app.LoginVersion_LoginV2{LoginV2: &app.LoginV2{BaseUri: gu.Ptr("://invalid")}}}, + expectedVer: gu.Ptr(domain.LoginVersion2), + expectedURI: gu.Ptr("://invalid"), + expectedError: &url.Error{Op: "parse", URL: "://invalid", Err: errors.New("missing protocol scheme")}, + }, + { + name: "unknown version type", + version: &app.LoginVersion{}, + expectedVer: gu.Ptr(domain.LoginVersionUnspecified), + expectedURI: gu.Ptr(""), + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + // When + version, uri, err := loginVersionToDomain(tc.version) + + // Then + assert.Equal(t, tc.expectedVer, version) + assert.Equal(t, tc.expectedURI, uri) + assert.Equal(t, tc.expectedError, err) + }) + } +} + +func TestLoginVersionToPb(t *testing.T) { + t.Parallel() + + tt := []struct { + name string + version domain.LoginVersion + baseURI *string + expected *app.LoginVersion + }{ + { + name: "unspecified version", + version: domain.LoginVersionUnspecified, + baseURI: nil, + expected: nil, + }, + { + name: "login v1", + version: domain.LoginVersion1, + baseURI: nil, + expected: &app.LoginVersion{ + Version: &app.LoginVersion_LoginV1{ + LoginV1: &app.LoginV1{}, + }, + }, + }, + { + name: "login v2", + version: domain.LoginVersion2, + baseURI: gu.Ptr("https://example.com"), + expected: &app.LoginVersion{ + Version: &app.LoginVersion_LoginV2{ + LoginV2: &app.LoginV2{ + BaseUri: gu.Ptr("https://example.com"), + }, + }, + }, + }, + { + name: "unknown version", + version: domain.LoginVersion(99), + baseURI: nil, + expected: nil, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + // When + result := loginVersionToPb(tc.version, tc.baseURI) + + // Then + assert.Equal(t, tc.expected, result) + }) + } +} + +func TestAppQueryToModel(t *testing.T) { + t.Parallel() + + validAppNameSearchQuery, err := query.NewAppNameSearchQuery(query.TextEquals, "test") + require.NoError(t, err) + + validAppStateSearchQuery, err := query.NewAppStateSearchQuery(domain.AppStateActive) + require.NoError(t, err) + + tt := []struct { + name string + query *app.ApplicationSearchFilter + + expectedQuery query.SearchQuery + expectedError error + }{ + { + name: "name query", + query: &app.ApplicationSearchFilter{ + Filter: &app.ApplicationSearchFilter_NameFilter{ + NameFilter: &app.ApplicationNameQuery{ + Name: "test", + Method: filter_pb_v2.TextFilterMethod_TEXT_FILTER_METHOD_EQUALS, + }, + }, + }, + expectedQuery: validAppNameSearchQuery, + }, + { + name: "state query", + query: &app.ApplicationSearchFilter{ + Filter: &app.ApplicationSearchFilter_StateFilter{ + StateFilter: app.AppState_APP_STATE_ACTIVE, + }, + }, + expectedQuery: validAppStateSearchQuery, + }, + { + name: "api app only query", + query: &app.ApplicationSearchFilter{ + Filter: &app.ApplicationSearchFilter_ApiAppOnly{}, + }, + expectedQuery: &query.NotNullQuery{ + Column: query.AppAPIConfigColumnAppID, + }, + }, + { + name: "oidc app only query", + query: &app.ApplicationSearchFilter{ + Filter: &app.ApplicationSearchFilter_OidcAppOnly{}, + }, + expectedQuery: &query.NotNullQuery{ + Column: query.AppOIDCConfigColumnAppID, + }, + }, + { + name: "saml app only query", + query: &app.ApplicationSearchFilter{ + Filter: &app.ApplicationSearchFilter_SamlAppOnly{}, + }, + expectedQuery: &query.NotNullQuery{ + Column: query.AppSAMLConfigColumnAppID, + }, + }, + { + name: "invalid query type", + query: &app.ApplicationSearchFilter{}, + expectedQuery: nil, + expectedError: zerrors.ThrowInvalidArgument(nil, "CONV-z2mAGy", "List.Query.Invalid"), + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + // When + result, err := appQueryToModel(tc.query) + + // Then + assert.Equal(t, tc.expectedError, err) + assert.Equal(t, tc.expectedQuery, result) + }) + } +} + +func TestListApplicationKeysRequestToDomain(t *testing.T) { + t.Parallel() + + resourceOwnerQuery, err := query.NewAuthNKeyResourceOwnerQuery("org1") + require.NoError(t, err) + + projectIDQuery, err := query.NewAuthNKeyAggregateIDQuery("project1") + require.NoError(t, err) + + appIDQuery, err := query.NewAuthNKeyObjectIDQuery("app1") + require.NoError(t, err) + + sysDefaults := systemdefaults.SystemDefaults{DefaultQueryLimit: 100, MaxQueryLimit: 150} + + tt := []struct { + name string + req *app.ListApplicationKeysRequest + + expectedResult *query.AuthNKeySearchQueries + expectedError error + }{ + { + name: "invalid pagination limit", + req: &app.ListApplicationKeysRequest{ + Pagination: &filter_pb_v2.PaginationRequest{Asc: true, Limit: uint32(sysDefaults.MaxQueryLimit + 1)}, + }, + expectedResult: nil, + expectedError: zerrors.ThrowInvalidArgumentf(fmt.Errorf("given: %d, allowed: %d", sysDefaults.MaxQueryLimit+1, sysDefaults.MaxQueryLimit), "QUERY-4M0fs", "Errors.Query.LimitExceeded"), + }, + { + name: "empty request", + req: &app.ListApplicationKeysRequest{ + Pagination: &filter_pb_v2.PaginationRequest{Asc: true}, + }, + expectedResult: &query.AuthNKeySearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: 0, + Limit: 100, + Asc: true, + SortingColumn: query.AuthNKeyColumnID, + }, + Queries: nil, + }, + }, + { + name: "only organization id", + req: &app.ListApplicationKeysRequest{ + ResourceId: &app.ListApplicationKeysRequest_OrganizationId{OrganizationId: "org1"}, + Pagination: &filter_pb_v2.PaginationRequest{Asc: true}, + }, + expectedResult: &query.AuthNKeySearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: 0, + Limit: 100, + Asc: true, + SortingColumn: query.AuthNKeyColumnID, + }, + Queries: []query.SearchQuery{ + resourceOwnerQuery, + }, + }, + }, + { + name: "only project id", + req: &app.ListApplicationKeysRequest{ + ResourceId: &app.ListApplicationKeysRequest_ProjectId{ProjectId: "project1"}, + Pagination: &filter_pb_v2.PaginationRequest{Asc: true}, + }, + expectedResult: &query.AuthNKeySearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: 0, + Limit: 100, + Asc: true, + SortingColumn: query.AuthNKeyColumnID, + }, + Queries: []query.SearchQuery{ + projectIDQuery, + }, + }, + }, + { + name: "only application id", + req: &app.ListApplicationKeysRequest{ + ResourceId: &app.ListApplicationKeysRequest_ApplicationId{ApplicationId: "app1"}, + Pagination: &filter_pb_v2.PaginationRequest{Asc: true}, + }, + expectedResult: &query.AuthNKeySearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: 0, + Limit: 100, + Asc: true, + SortingColumn: query.AuthNKeyColumnID, + }, + Queries: []query.SearchQuery{ + appIDQuery, + }, + }, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + result, err := ListApplicationKeysRequestToDomain(sysDefaults, tc.req) + + assert.Equal(t, tc.expectedError, err) + assert.Equal(t, tc.expectedResult, result) + }) + } +} + +func TestApplicationKeysToPb(t *testing.T) { + t.Parallel() + + now := time.Now() + + tt := []struct { + name string + input []*query.AuthNKey + expected []*app.ApplicationKey + }{ + { + name: "multiple keys", + input: []*query.AuthNKey{ + { + ID: "key1", + AggregateID: "project1", + ApplicationID: "app1", + CreationDate: now, + ResourceOwner: "org1", + Expiration: now.Add(24 * time.Hour), + Type: domain.AuthNKeyTypeJSON, + }, + { + ID: "key2", + AggregateID: "project2", + ApplicationID: "app1", + CreationDate: now.Add(-time.Hour), + ResourceOwner: "org2", + Expiration: now.Add(48 * time.Hour), + Type: domain.AuthNKeyTypeNONE, + }, + }, + expected: []*app.ApplicationKey{ + { + Id: "key1", + ApplicationId: "app1", + ProjectId: "project1", + CreationDate: timestamppb.New(now), + OrganizationId: "org1", + ExpirationDate: timestamppb.New(now.Add(24 * time.Hour)), + }, + { + Id: "key2", + ApplicationId: "app1", + ProjectId: "project2", + CreationDate: timestamppb.New(now.Add(-time.Hour)), + OrganizationId: "org2", + ExpirationDate: timestamppb.New(now.Add(48 * time.Hour)), + }, + }, + }, + { + name: "empty slice", + input: []*query.AuthNKey{}, + expected: []*app.ApplicationKey{}, + }, + { + name: "nil input", + input: nil, + expected: []*app.ApplicationKey{}, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + result := ApplicationKeysToPb(tc.input) + assert.Equal(t, tc.expected, result) + }) + } +} diff --git a/internal/api/grpc/app/v2beta/convert/oidc_app.go b/internal/api/grpc/app/v2beta/convert/oidc_app.go new file mode 100644 index 0000000000..223e43d166 --- /dev/null +++ b/internal/api/grpc/app/v2beta/convert/oidc_app.go @@ -0,0 +1,291 @@ +package convert + +import ( + "github.com/muhlemmer/gu" + "google.golang.org/protobuf/types/known/durationpb" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore/v1/models" + "github.com/zitadel/zitadel/internal/query" + app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta" +) + +func CreateOIDCAppRequestToDomain(name, projectID string, req *app.CreateOIDCApplicationRequest) (*domain.OIDCApp, error) { + loginVersion, loginBaseURI, err := loginVersionToDomain(req.GetLoginVersion()) + if err != nil { + return nil, err + } + return &domain.OIDCApp{ + ObjectRoot: models.ObjectRoot{ + AggregateID: projectID, + }, + AppName: name, + OIDCVersion: gu.Ptr(domain.OIDCVersionV1), + RedirectUris: req.GetRedirectUris(), + ResponseTypes: oidcResponseTypesToDomain(req.GetResponseTypes()), + GrantTypes: oidcGrantTypesToDomain(req.GetGrantTypes()), + ApplicationType: gu.Ptr(oidcApplicationTypeToDomain(req.GetAppType())), + AuthMethodType: gu.Ptr(oidcAuthMethodTypeToDomain(req.GetAuthMethodType())), + PostLogoutRedirectUris: req.GetPostLogoutRedirectUris(), + DevMode: &req.DevMode, + AccessTokenType: gu.Ptr(oidcTokenTypeToDomain(req.GetAccessTokenType())), + AccessTokenRoleAssertion: gu.Ptr(req.GetAccessTokenRoleAssertion()), + IDTokenRoleAssertion: gu.Ptr(req.GetIdTokenRoleAssertion()), + IDTokenUserinfoAssertion: gu.Ptr(req.GetIdTokenUserinfoAssertion()), + ClockSkew: gu.Ptr(req.GetClockSkew().AsDuration()), + AdditionalOrigins: req.GetAdditionalOrigins(), + SkipNativeAppSuccessPage: gu.Ptr(req.GetSkipNativeAppSuccessPage()), + BackChannelLogoutURI: gu.Ptr(req.GetBackChannelLogoutUri()), + LoginVersion: loginVersion, + LoginBaseURI: loginBaseURI, + }, nil +} + +func UpdateOIDCAppConfigRequestToDomain(appID, projectID string, app *app.UpdateOIDCApplicationConfigurationRequest) (*domain.OIDCApp, error) { + loginVersion, loginBaseURI, err := loginVersionToDomain(app.GetLoginVersion()) + if err != nil { + return nil, err + } + return &domain.OIDCApp{ + ObjectRoot: models.ObjectRoot{ + AggregateID: projectID, + }, + AppID: appID, + RedirectUris: app.RedirectUris, + ResponseTypes: oidcResponseTypesToDomain(app.ResponseTypes), + GrantTypes: oidcGrantTypesToDomain(app.GrantTypes), + ApplicationType: oidcApplicationTypeToDomainPtr(app.AppType), + AuthMethodType: oidcAuthMethodTypeToDomainPtr(app.AuthMethodType), + PostLogoutRedirectUris: app.PostLogoutRedirectUris, + DevMode: app.DevMode, + AccessTokenType: oidcTokenTypeToDomainPtr(app.AccessTokenType), + AccessTokenRoleAssertion: app.AccessTokenRoleAssertion, + IDTokenRoleAssertion: app.IdTokenRoleAssertion, + IDTokenUserinfoAssertion: app.IdTokenUserinfoAssertion, + ClockSkew: gu.Ptr(app.GetClockSkew().AsDuration()), + AdditionalOrigins: app.AdditionalOrigins, + SkipNativeAppSuccessPage: app.SkipNativeAppSuccessPage, + BackChannelLogoutURI: app.BackChannelLogoutUri, + LoginVersion: loginVersion, + LoginBaseURI: loginBaseURI, + }, nil +} + +func oidcResponseTypesToDomain(responseTypes []app.OIDCResponseType) []domain.OIDCResponseType { + if len(responseTypes) == 0 { + return []domain.OIDCResponseType{domain.OIDCResponseTypeCode} + } + oidcResponseTypes := make([]domain.OIDCResponseType, len(responseTypes)) + for i, responseType := range responseTypes { + switch responseType { + case app.OIDCResponseType_OIDC_RESPONSE_TYPE_UNSPECIFIED: + oidcResponseTypes[i] = domain.OIDCResponseTypeUnspecified + case app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE: + oidcResponseTypes[i] = domain.OIDCResponseTypeCode + case app.OIDCResponseType_OIDC_RESPONSE_TYPE_ID_TOKEN: + oidcResponseTypes[i] = domain.OIDCResponseTypeIDToken + case app.OIDCResponseType_OIDC_RESPONSE_TYPE_ID_TOKEN_TOKEN: + oidcResponseTypes[i] = domain.OIDCResponseTypeIDTokenToken + } + } + return oidcResponseTypes +} + +func oidcGrantTypesToDomain(grantTypes []app.OIDCGrantType) []domain.OIDCGrantType { + if len(grantTypes) == 0 { + return []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode} + } + oidcGrantTypes := make([]domain.OIDCGrantType, len(grantTypes)) + for i, grantType := range grantTypes { + switch grantType { + case app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE: + oidcGrantTypes[i] = domain.OIDCGrantTypeAuthorizationCode + case app.OIDCGrantType_OIDC_GRANT_TYPE_IMPLICIT: + oidcGrantTypes[i] = domain.OIDCGrantTypeImplicit + case app.OIDCGrantType_OIDC_GRANT_TYPE_REFRESH_TOKEN: + oidcGrantTypes[i] = domain.OIDCGrantTypeRefreshToken + case app.OIDCGrantType_OIDC_GRANT_TYPE_DEVICE_CODE: + oidcGrantTypes[i] = domain.OIDCGrantTypeDeviceCode + case app.OIDCGrantType_OIDC_GRANT_TYPE_TOKEN_EXCHANGE: + oidcGrantTypes[i] = domain.OIDCGrantTypeTokenExchange + } + } + return oidcGrantTypes +} + +func oidcApplicationTypeToDomainPtr(appType *app.OIDCAppType) *domain.OIDCApplicationType { + if appType == nil { + return nil + } + + res := oidcApplicationTypeToDomain(*appType) + return &res +} + +func oidcApplicationTypeToDomain(appType app.OIDCAppType) domain.OIDCApplicationType { + switch appType { + case app.OIDCAppType_OIDC_APP_TYPE_WEB: + return domain.OIDCApplicationTypeWeb + case app.OIDCAppType_OIDC_APP_TYPE_USER_AGENT: + return domain.OIDCApplicationTypeUserAgent + case app.OIDCAppType_OIDC_APP_TYPE_NATIVE: + return domain.OIDCApplicationTypeNative + } + return domain.OIDCApplicationTypeWeb +} + +func oidcAuthMethodTypeToDomainPtr(authType *app.OIDCAuthMethodType) *domain.OIDCAuthMethodType { + if authType == nil { + return nil + } + + res := oidcAuthMethodTypeToDomain(*authType) + return &res +} + +func oidcAuthMethodTypeToDomain(authType app.OIDCAuthMethodType) domain.OIDCAuthMethodType { + switch authType { + case app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC: + return domain.OIDCAuthMethodTypeBasic + case app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_POST: + return domain.OIDCAuthMethodTypePost + case app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE: + return domain.OIDCAuthMethodTypeNone + case app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT: + return domain.OIDCAuthMethodTypePrivateKeyJWT + default: + return domain.OIDCAuthMethodTypeBasic + } +} + +func oidcTokenTypeToDomainPtr(tokenType *app.OIDCTokenType) *domain.OIDCTokenType { + if tokenType == nil { + return nil + } + + res := oidcTokenTypeToDomain(*tokenType) + return &res +} + +func oidcTokenTypeToDomain(tokenType app.OIDCTokenType) domain.OIDCTokenType { + switch tokenType { + case app.OIDCTokenType_OIDC_TOKEN_TYPE_BEARER: + return domain.OIDCTokenTypeBearer + case app.OIDCTokenType_OIDC_TOKEN_TYPE_JWT: + return domain.OIDCTokenTypeJWT + default: + return domain.OIDCTokenTypeBearer + } +} + +func ComplianceProblemsToLocalizedMessages(complianceProblems []string) []*app.OIDCLocalizedMessage { + converted := make([]*app.OIDCLocalizedMessage, len(complianceProblems)) + for i, p := range complianceProblems { + converted[i] = &app.OIDCLocalizedMessage{Key: p} + } + + return converted +} + +func appOIDCConfigToPb(oidcApp *query.OIDCApp) *app.Application_OidcConfig { + return &app.Application_OidcConfig{ + OidcConfig: &app.OIDCConfig{ + RedirectUris: oidcApp.RedirectURIs, + ResponseTypes: oidcResponseTypesFromModel(oidcApp.ResponseTypes), + GrantTypes: oidcGrantTypesFromModel(oidcApp.GrantTypes), + AppType: oidcApplicationTypeToPb(oidcApp.AppType), + ClientId: oidcApp.ClientID, + AuthMethodType: oidcAuthMethodTypeToPb(oidcApp.AuthMethodType), + PostLogoutRedirectUris: oidcApp.PostLogoutRedirectURIs, + Version: app.OIDCVersion_OIDC_VERSION_1_0, + NoneCompliant: len(oidcApp.ComplianceProblems) != 0, + ComplianceProblems: ComplianceProblemsToLocalizedMessages(oidcApp.ComplianceProblems), + DevMode: oidcApp.IsDevMode, + AccessTokenType: oidcTokenTypeToPb(oidcApp.AccessTokenType), + AccessTokenRoleAssertion: oidcApp.AssertAccessTokenRole, + IdTokenRoleAssertion: oidcApp.AssertIDTokenRole, + IdTokenUserinfoAssertion: oidcApp.AssertIDTokenUserinfo, + ClockSkew: durationpb.New(oidcApp.ClockSkew), + AdditionalOrigins: oidcApp.AdditionalOrigins, + AllowedOrigins: oidcApp.AllowedOrigins, + SkipNativeAppSuccessPage: oidcApp.SkipNativeAppSuccessPage, + BackChannelLogoutUri: oidcApp.BackChannelLogoutURI, + LoginVersion: loginVersionToPb(oidcApp.LoginVersion, oidcApp.LoginBaseURI), + }, + } +} + +func oidcResponseTypesFromModel(responseTypes []domain.OIDCResponseType) []app.OIDCResponseType { + oidcResponseTypes := make([]app.OIDCResponseType, len(responseTypes)) + for i, responseType := range responseTypes { + switch responseType { + case domain.OIDCResponseTypeUnspecified: + oidcResponseTypes[i] = app.OIDCResponseType_OIDC_RESPONSE_TYPE_UNSPECIFIED + case domain.OIDCResponseTypeCode: + oidcResponseTypes[i] = app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE + case domain.OIDCResponseTypeIDToken: + oidcResponseTypes[i] = app.OIDCResponseType_OIDC_RESPONSE_TYPE_ID_TOKEN + case domain.OIDCResponseTypeIDTokenToken: + oidcResponseTypes[i] = app.OIDCResponseType_OIDC_RESPONSE_TYPE_ID_TOKEN_TOKEN + } + } + return oidcResponseTypes +} + +func oidcGrantTypesFromModel(grantTypes []domain.OIDCGrantType) []app.OIDCGrantType { + oidcGrantTypes := make([]app.OIDCGrantType, len(grantTypes)) + for i, grantType := range grantTypes { + switch grantType { + case domain.OIDCGrantTypeAuthorizationCode: + oidcGrantTypes[i] = app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE + case domain.OIDCGrantTypeImplicit: + oidcGrantTypes[i] = app.OIDCGrantType_OIDC_GRANT_TYPE_IMPLICIT + case domain.OIDCGrantTypeRefreshToken: + oidcGrantTypes[i] = app.OIDCGrantType_OIDC_GRANT_TYPE_REFRESH_TOKEN + case domain.OIDCGrantTypeDeviceCode: + oidcGrantTypes[i] = app.OIDCGrantType_OIDC_GRANT_TYPE_DEVICE_CODE + case domain.OIDCGrantTypeTokenExchange: + oidcGrantTypes[i] = app.OIDCGrantType_OIDC_GRANT_TYPE_TOKEN_EXCHANGE + } + } + return oidcGrantTypes +} + +func oidcApplicationTypeToPb(appType domain.OIDCApplicationType) app.OIDCAppType { + switch appType { + case domain.OIDCApplicationTypeWeb: + return app.OIDCAppType_OIDC_APP_TYPE_WEB + case domain.OIDCApplicationTypeUserAgent: + return app.OIDCAppType_OIDC_APP_TYPE_USER_AGENT + case domain.OIDCApplicationTypeNative: + return app.OIDCAppType_OIDC_APP_TYPE_NATIVE + default: + return app.OIDCAppType_OIDC_APP_TYPE_WEB + } +} + +func oidcAuthMethodTypeToPb(authType domain.OIDCAuthMethodType) app.OIDCAuthMethodType { + switch authType { + case domain.OIDCAuthMethodTypeBasic: + return app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC + case domain.OIDCAuthMethodTypePost: + return app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_POST + case domain.OIDCAuthMethodTypeNone: + return app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE + case domain.OIDCAuthMethodTypePrivateKeyJWT: + return app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT + default: + return app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC + } +} + +func oidcTokenTypeToPb(tokenType domain.OIDCTokenType) app.OIDCTokenType { + switch tokenType { + case domain.OIDCTokenTypeBearer: + return app.OIDCTokenType_OIDC_TOKEN_TYPE_BEARER + case domain.OIDCTokenTypeJWT: + return app.OIDCTokenType_OIDC_TOKEN_TYPE_JWT + default: + return app.OIDCTokenType_OIDC_TOKEN_TYPE_BEARER + } +} diff --git a/internal/api/grpc/app/v2beta/convert/oidc_app_test.go b/internal/api/grpc/app/v2beta/convert/oidc_app_test.go new file mode 100644 index 0000000000..a6b3f0b709 --- /dev/null +++ b/internal/api/grpc/app/v2beta/convert/oidc_app_test.go @@ -0,0 +1,755 @@ +package convert + +import ( + "net/url" + "testing" + "time" + + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "google.golang.org/protobuf/types/known/durationpb" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore/v1/models" + "github.com/zitadel/zitadel/internal/query" + app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta" +) + +func TestCreateOIDCAppRequestToDomain(t *testing.T) { + t.Parallel() + + tt := []struct { + testName string + projectID string + req *app.CreateOIDCApplicationRequest + + expectedModel *domain.OIDCApp + expectedError error + }{ + { + testName: "unparsable login version 2 URL", + projectID: "pid", + req: &app.CreateOIDCApplicationRequest{ + LoginVersion: &app.LoginVersion{Version: &app.LoginVersion_LoginV2{ + LoginV2: &app.LoginV2{BaseUri: gu.Ptr("%+o")}}, + }, + }, + expectedModel: nil, + expectedError: &url.Error{ + URL: "%+o", + Op: "parse", + Err: url.EscapeError("%+o"), + }, + }, + { + testName: "all fields set", + projectID: "project1", + req: &app.CreateOIDCApplicationRequest{ + RedirectUris: []string{"https://redirect"}, + ResponseTypes: []app.OIDCResponseType{app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE}, + GrantTypes: []app.OIDCGrantType{app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE}, + AppType: app.OIDCAppType_OIDC_APP_TYPE_WEB, + AuthMethodType: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC, + PostLogoutRedirectUris: []string{"https://logout"}, + DevMode: true, + AccessTokenType: app.OIDCTokenType_OIDC_TOKEN_TYPE_BEARER, + AccessTokenRoleAssertion: true, + IdTokenRoleAssertion: true, + IdTokenUserinfoAssertion: true, + ClockSkew: durationpb.New(5 * time.Second), + AdditionalOrigins: []string{"https://origin"}, + SkipNativeAppSuccessPage: true, + BackChannelLogoutUri: "https://backchannel", + LoginVersion: &app.LoginVersion{Version: &app.LoginVersion_LoginV2{LoginV2: &app.LoginV2{ + BaseUri: gu.Ptr("https://login"), + }}}, + }, + expectedModel: &domain.OIDCApp{ + ObjectRoot: models.ObjectRoot{AggregateID: "project1"}, + AppName: "all fields set", + OIDCVersion: gu.Ptr(domain.OIDCVersionV1), + RedirectUris: []string{"https://redirect"}, + ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, + GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}, + ApplicationType: gu.Ptr(domain.OIDCApplicationTypeWeb), + AuthMethodType: gu.Ptr(domain.OIDCAuthMethodTypeBasic), + PostLogoutRedirectUris: []string{"https://logout"}, + DevMode: gu.Ptr(true), + AccessTokenType: gu.Ptr(domain.OIDCTokenTypeBearer), + AccessTokenRoleAssertion: gu.Ptr(true), + IDTokenRoleAssertion: gu.Ptr(true), + IDTokenUserinfoAssertion: gu.Ptr(true), + ClockSkew: gu.Ptr(5 * time.Second), + AdditionalOrigins: []string{"https://origin"}, + SkipNativeAppSuccessPage: gu.Ptr(true), + BackChannelLogoutURI: gu.Ptr("https://backchannel"), + LoginVersion: gu.Ptr(domain.LoginVersion2), + LoginBaseURI: gu.Ptr("https://login"), + }, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + + // When + res, err := CreateOIDCAppRequestToDomain(tc.testName, tc.projectID, tc.req) + + // Then + assert.Equal(t, tc.expectedError, err) + assert.Equal(t, tc.expectedModel, res) + }) + } +} + +func TestUpdateOIDCAppConfigRequestToDomain(t *testing.T) { + t.Parallel() + + tt := []struct { + testName string + + appID string + projectID string + req *app.UpdateOIDCApplicationConfigurationRequest + + expectedModel *domain.OIDCApp + expectedError error + }{ + { + testName: "unparsable login version 2 URL", + appID: "app1", + projectID: "pid", + req: &app.UpdateOIDCApplicationConfigurationRequest{ + LoginVersion: &app.LoginVersion{Version: &app.LoginVersion_LoginV2{ + LoginV2: &app.LoginV2{BaseUri: gu.Ptr("%+o")}, + }}, + }, + expectedModel: nil, + expectedError: &url.Error{ + URL: "%+o", + Op: "parse", + Err: url.EscapeError("%+o"), + }, + }, + { + testName: "successful Update", + appID: "app1", + projectID: "proj1", + req: &app.UpdateOIDCApplicationConfigurationRequest{ + RedirectUris: []string{"https://redirect"}, + ResponseTypes: []app.OIDCResponseType{app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE}, + GrantTypes: []app.OIDCGrantType{app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE}, + AppType: gu.Ptr(app.OIDCAppType_OIDC_APP_TYPE_WEB), + AuthMethodType: gu.Ptr(app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC), + PostLogoutRedirectUris: []string{"https://logout"}, + DevMode: gu.Ptr(true), + AccessTokenType: gu.Ptr(app.OIDCTokenType_OIDC_TOKEN_TYPE_BEARER), + AccessTokenRoleAssertion: gu.Ptr(true), + IdTokenRoleAssertion: gu.Ptr(true), + IdTokenUserinfoAssertion: gu.Ptr(true), + ClockSkew: durationpb.New(5 * time.Second), + AdditionalOrigins: []string{"https://origin"}, + SkipNativeAppSuccessPage: gu.Ptr(true), + BackChannelLogoutUri: gu.Ptr("https://backchannel"), + LoginVersion: &app.LoginVersion{Version: &app.LoginVersion_LoginV2{ + LoginV2: &app.LoginV2{BaseUri: gu.Ptr("https://login")}, + }}, + }, + expectedModel: &domain.OIDCApp{ + ObjectRoot: models.ObjectRoot{AggregateID: "proj1"}, + AppID: "app1", + RedirectUris: []string{"https://redirect"}, + ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, + GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}, + ApplicationType: gu.Ptr(domain.OIDCApplicationTypeWeb), + AuthMethodType: gu.Ptr(domain.OIDCAuthMethodTypeBasic), + PostLogoutRedirectUris: []string{"https://logout"}, + DevMode: gu.Ptr(true), + AccessTokenType: gu.Ptr(domain.OIDCTokenTypeBearer), + AccessTokenRoleAssertion: gu.Ptr(true), + IDTokenRoleAssertion: gu.Ptr(true), + IDTokenUserinfoAssertion: gu.Ptr(true), + ClockSkew: gu.Ptr(5 * time.Second), + AdditionalOrigins: []string{"https://origin"}, + SkipNativeAppSuccessPage: gu.Ptr(true), + BackChannelLogoutURI: gu.Ptr("https://backchannel"), + LoginVersion: gu.Ptr(domain.LoginVersion2), + LoginBaseURI: gu.Ptr("https://login"), + }, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + + // When + got, err := UpdateOIDCAppConfigRequestToDomain(tc.appID, tc.projectID, tc.req) + + // Then + assert.Equal(t, tc.expectedError, err) + assert.Equal(t, tc.expectedModel, got) + }) + } +} + +func TestOIDCResponseTypesToDomain(t *testing.T) { + t.Parallel() + + tt := []struct { + testName string + inputResponseType []app.OIDCResponseType + expectedResponse []domain.OIDCResponseType + }{ + { + testName: "empty response types", + inputResponseType: []app.OIDCResponseType{}, + expectedResponse: []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, + }, + { + testName: "all response types", + inputResponseType: []app.OIDCResponseType{ + app.OIDCResponseType_OIDC_RESPONSE_TYPE_UNSPECIFIED, + app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE, + app.OIDCResponseType_OIDC_RESPONSE_TYPE_ID_TOKEN, + app.OIDCResponseType_OIDC_RESPONSE_TYPE_ID_TOKEN_TOKEN, + }, + expectedResponse: []domain.OIDCResponseType{ + domain.OIDCResponseTypeUnspecified, + domain.OIDCResponseTypeCode, + domain.OIDCResponseTypeIDToken, + domain.OIDCResponseTypeIDTokenToken, + }, + }, + { + testName: "single response type", + inputResponseType: []app.OIDCResponseType{ + app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE, + }, + expectedResponse: []domain.OIDCResponseType{ + domain.OIDCResponseTypeCode, + }, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + + // When + res := oidcResponseTypesToDomain(tc.inputResponseType) + + // Then + assert.Equal(t, tc.expectedResponse, res) + }) + } +} + +func TestOIDCGrantTypesToDomain(t *testing.T) { + t.Parallel() + + tt := []struct { + testName string + inputGrantType []app.OIDCGrantType + expectedGrants []domain.OIDCGrantType + }{ + { + testName: "empty grant types", + inputGrantType: []app.OIDCGrantType{}, + expectedGrants: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}, + }, + { + testName: "all grant types", + inputGrantType: []app.OIDCGrantType{ + app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE, + app.OIDCGrantType_OIDC_GRANT_TYPE_IMPLICIT, + app.OIDCGrantType_OIDC_GRANT_TYPE_REFRESH_TOKEN, + app.OIDCGrantType_OIDC_GRANT_TYPE_DEVICE_CODE, + app.OIDCGrantType_OIDC_GRANT_TYPE_TOKEN_EXCHANGE, + }, + expectedGrants: []domain.OIDCGrantType{ + domain.OIDCGrantTypeAuthorizationCode, + domain.OIDCGrantTypeImplicit, + domain.OIDCGrantTypeRefreshToken, + domain.OIDCGrantTypeDeviceCode, + domain.OIDCGrantTypeTokenExchange, + }, + }, + { + testName: "single grant type", + inputGrantType: []app.OIDCGrantType{ + app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE, + }, + expectedGrants: []domain.OIDCGrantType{ + domain.OIDCGrantTypeAuthorizationCode, + }, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + + // When + res := oidcGrantTypesToDomain(tc.inputGrantType) + + // Then + assert.Equal(t, tc.expectedGrants, res) + }) + } +} + +func TestOIDCApplicationTypeToDomain(t *testing.T) { + t.Parallel() + + tt := []struct { + name string + appType app.OIDCAppType + expected domain.OIDCApplicationType + }{ + { + name: "web type", + appType: app.OIDCAppType_OIDC_APP_TYPE_WEB, + expected: domain.OIDCApplicationTypeWeb, + }, + { + name: "user agent type", + appType: app.OIDCAppType_OIDC_APP_TYPE_USER_AGENT, + expected: domain.OIDCApplicationTypeUserAgent, + }, + { + name: "native type", + appType: app.OIDCAppType_OIDC_APP_TYPE_NATIVE, + expected: domain.OIDCApplicationTypeNative, + }, + { + name: "unspecified type defaults to web", + expected: domain.OIDCApplicationTypeWeb, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + // When + result := oidcApplicationTypeToDomain(tc.appType) + + // Then + assert.Equal(t, tc.expected, result) + }) + } +} + +func TestOIDCAuthMethodTypeToDomain(t *testing.T) { + t.Parallel() + + tt := []struct { + name string + authType app.OIDCAuthMethodType + expectedResponse domain.OIDCAuthMethodType + }{ + { + name: "basic auth type", + authType: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC, + expectedResponse: domain.OIDCAuthMethodTypeBasic, + }, + { + name: "post auth type", + authType: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_POST, + expectedResponse: domain.OIDCAuthMethodTypePost, + }, + { + name: "none auth type", + authType: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE, + expectedResponse: domain.OIDCAuthMethodTypeNone, + }, + { + name: "private key jwt auth type", + authType: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT, + expectedResponse: domain.OIDCAuthMethodTypePrivateKeyJWT, + }, + { + name: "unspecified auth type defaults to basic", + expectedResponse: domain.OIDCAuthMethodTypeBasic, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + // When + res := oidcAuthMethodTypeToDomain(tc.authType) + + // Then + assert.Equal(t, tc.expectedResponse, res) + }) + } +} + +func TestOIDCTokenTypeToDomain(t *testing.T) { + t.Parallel() + + tt := []struct { + name string + tokenType app.OIDCTokenType + expectedType domain.OIDCTokenType + }{ + { + name: "bearer token type", + tokenType: app.OIDCTokenType_OIDC_TOKEN_TYPE_BEARER, + expectedType: domain.OIDCTokenTypeBearer, + }, + { + name: "jwt token type", + tokenType: app.OIDCTokenType_OIDC_TOKEN_TYPE_JWT, + expectedType: domain.OIDCTokenTypeJWT, + }, + { + name: "unspecified defaults to bearer", + expectedType: domain.OIDCTokenTypeBearer, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + // When + result := oidcTokenTypeToDomain(tc.tokenType) + + // Then + assert.Equal(t, tc.expectedType, result) + }) + } +} +func TestAppOIDCConfigToPb(t *testing.T) { + t.Parallel() + + tt := []struct { + name string + input *query.OIDCApp + expected *app.Application_OidcConfig + }{ + { + name: "empty config", + input: &query.OIDCApp{}, + expected: &app.Application_OidcConfig{ + OidcConfig: &app.OIDCConfig{ + Version: app.OIDCVersion_OIDC_VERSION_1_0, + ComplianceProblems: []*app.OIDCLocalizedMessage{}, + ClockSkew: durationpb.New(0), + ResponseTypes: []app.OIDCResponseType{}, + GrantTypes: []app.OIDCGrantType{}, + }, + }, + }, + { + name: "full config", + input: &query.OIDCApp{ + RedirectURIs: []string{"https://example.com/callback"}, + ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, + GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}, + AppType: domain.OIDCApplicationTypeWeb, + ClientID: "client123", + AuthMethodType: domain.OIDCAuthMethodTypeBasic, + PostLogoutRedirectURIs: []string{"https://example.com/logout"}, + ComplianceProblems: []string{"problem1", "problem2"}, + IsDevMode: true, + AccessTokenType: domain.OIDCTokenTypeBearer, + AssertAccessTokenRole: true, + AssertIDTokenRole: true, + AssertIDTokenUserinfo: true, + ClockSkew: 5 * time.Second, + AdditionalOrigins: []string{"https://app.example.com"}, + AllowedOrigins: []string{"https://allowed.example.com"}, + SkipNativeAppSuccessPage: true, + BackChannelLogoutURI: "https://example.com/backchannel", + LoginVersion: domain.LoginVersion2, + LoginBaseURI: gu.Ptr("https://login.example.com"), + }, + expected: &app.Application_OidcConfig{ + OidcConfig: &app.OIDCConfig{ + RedirectUris: []string{"https://example.com/callback"}, + ResponseTypes: []app.OIDCResponseType{app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE}, + GrantTypes: []app.OIDCGrantType{app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE}, + AppType: app.OIDCAppType_OIDC_APP_TYPE_WEB, + ClientId: "client123", + AuthMethodType: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC, + PostLogoutRedirectUris: []string{"https://example.com/logout"}, + Version: app.OIDCVersion_OIDC_VERSION_1_0, + NoneCompliant: true, + ComplianceProblems: []*app.OIDCLocalizedMessage{ + {Key: "problem1"}, + {Key: "problem2"}, + }, + DevMode: true, + AccessTokenType: app.OIDCTokenType_OIDC_TOKEN_TYPE_BEARER, + AccessTokenRoleAssertion: true, + IdTokenRoleAssertion: true, + IdTokenUserinfoAssertion: true, + ClockSkew: durationpb.New(5 * time.Second), + AdditionalOrigins: []string{"https://app.example.com"}, + AllowedOrigins: []string{"https://allowed.example.com"}, + SkipNativeAppSuccessPage: true, + BackChannelLogoutUri: "https://example.com/backchannel", + LoginVersion: &app.LoginVersion{ + Version: &app.LoginVersion_LoginV2{ + LoginV2: &app.LoginV2{ + BaseUri: gu.Ptr("https://login.example.com"), + }, + }, + }, + }, + }, + }, + } + + for _, tt := range tt { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + // When + result := appOIDCConfigToPb(tt.input) + + // Then + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestOIDCResponseTypesFromModel(t *testing.T) { + t.Parallel() + + tt := []struct { + name string + responseTypes []domain.OIDCResponseType + expected []app.OIDCResponseType + }{ + { + name: "empty response types", + responseTypes: []domain.OIDCResponseType{}, + expected: []app.OIDCResponseType{}, + }, + { + name: "all response types", + responseTypes: []domain.OIDCResponseType{ + domain.OIDCResponseTypeUnspecified, + domain.OIDCResponseTypeCode, + domain.OIDCResponseTypeIDToken, + domain.OIDCResponseTypeIDTokenToken, + }, + expected: []app.OIDCResponseType{ + app.OIDCResponseType_OIDC_RESPONSE_TYPE_UNSPECIFIED, + app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE, + app.OIDCResponseType_OIDC_RESPONSE_TYPE_ID_TOKEN, + app.OIDCResponseType_OIDC_RESPONSE_TYPE_ID_TOKEN_TOKEN, + }, + }, + { + name: "single response type", + responseTypes: []domain.OIDCResponseType{ + domain.OIDCResponseTypeCode, + }, + expected: []app.OIDCResponseType{ + app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE, + }, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + // When + result := oidcResponseTypesFromModel(tc.responseTypes) + + // Then + assert.Equal(t, tc.expected, result) + }) + } +} +func TestOIDCGrantTypesFromModel(t *testing.T) { + t.Parallel() + + tt := []struct { + name string + grantTypes []domain.OIDCGrantType + expected []app.OIDCGrantType + }{ + { + name: "empty grant types", + grantTypes: []domain.OIDCGrantType{}, + expected: []app.OIDCGrantType{}, + }, + { + name: "all grant types", + grantTypes: []domain.OIDCGrantType{ + domain.OIDCGrantTypeAuthorizationCode, + domain.OIDCGrantTypeImplicit, + domain.OIDCGrantTypeRefreshToken, + domain.OIDCGrantTypeDeviceCode, + domain.OIDCGrantTypeTokenExchange, + }, + expected: []app.OIDCGrantType{ + app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE, + app.OIDCGrantType_OIDC_GRANT_TYPE_IMPLICIT, + app.OIDCGrantType_OIDC_GRANT_TYPE_REFRESH_TOKEN, + app.OIDCGrantType_OIDC_GRANT_TYPE_DEVICE_CODE, + app.OIDCGrantType_OIDC_GRANT_TYPE_TOKEN_EXCHANGE, + }, + }, + { + name: "single grant type", + grantTypes: []domain.OIDCGrantType{ + domain.OIDCGrantTypeAuthorizationCode, + }, + expected: []app.OIDCGrantType{ + app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE, + }, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + // When + result := oidcGrantTypesFromModel(tc.grantTypes) + + // Then + assert.Equal(t, tc.expected, result) + }) + } +} + +func TestOIDCApplicationTypeToPb(t *testing.T) { + t.Parallel() + + tt := []struct { + name string + appType domain.OIDCApplicationType + expected app.OIDCAppType + }{ + { + name: "web type", + appType: domain.OIDCApplicationTypeWeb, + expected: app.OIDCAppType_OIDC_APP_TYPE_WEB, + }, + { + name: "user agent type", + appType: domain.OIDCApplicationTypeUserAgent, + expected: app.OIDCAppType_OIDC_APP_TYPE_USER_AGENT, + }, + { + name: "native type", + appType: domain.OIDCApplicationTypeNative, + expected: app.OIDCAppType_OIDC_APP_TYPE_NATIVE, + }, + { + name: "unspecified type defaults to web", + appType: domain.OIDCApplicationType(999), // Invalid value to trigger default case + expected: app.OIDCAppType_OIDC_APP_TYPE_WEB, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + // When + result := oidcApplicationTypeToPb(tc.appType) + + // Then + assert.Equal(t, tc.expected, result) + }) + } +} + +func TestOIDCAuthMethodTypeToPb(t *testing.T) { + t.Parallel() + + tt := []struct { + name string + authType domain.OIDCAuthMethodType + expected app.OIDCAuthMethodType + }{ + { + name: "basic auth type", + authType: domain.OIDCAuthMethodTypeBasic, + expected: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC, + }, + { + name: "post auth type", + authType: domain.OIDCAuthMethodTypePost, + expected: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_POST, + }, + { + name: "none auth type", + authType: domain.OIDCAuthMethodTypeNone, + expected: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE, + }, + { + name: "private key jwt auth type", + authType: domain.OIDCAuthMethodTypePrivateKeyJWT, + expected: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT, + }, + { + name: "unknown auth type defaults to basic", + authType: domain.OIDCAuthMethodType(999), + expected: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + // When + result := oidcAuthMethodTypeToPb(tc.authType) + + // Then + assert.Equal(t, tc.expected, result) + }) + } +} + +func TestOIDCTokenTypeToPb(t *testing.T) { + t.Parallel() + + tt := []struct { + name string + tokenType domain.OIDCTokenType + expected app.OIDCTokenType + }{ + { + name: "bearer token type", + tokenType: domain.OIDCTokenTypeBearer, + expected: app.OIDCTokenType_OIDC_TOKEN_TYPE_BEARER, + }, + { + name: "jwt token type", + tokenType: domain.OIDCTokenTypeJWT, + expected: app.OIDCTokenType_OIDC_TOKEN_TYPE_JWT, + }, + { + name: "unknown token type defaults to bearer", + tokenType: domain.OIDCTokenType(999), // Invalid value to trigger default case + expected: app.OIDCTokenType_OIDC_TOKEN_TYPE_BEARER, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + // When + result := oidcTokenTypeToPb(tc.tokenType) + + // Then + assert.Equal(t, tc.expected, result) + }) + } +} diff --git a/internal/api/grpc/app/v2beta/convert/saml_app.go b/internal/api/grpc/app/v2beta/convert/saml_app.go new file mode 100644 index 0000000000..7f1bef082b --- /dev/null +++ b/internal/api/grpc/app/v2beta/convert/saml_app.go @@ -0,0 +1,77 @@ +package convert + +import ( + "github.com/muhlemmer/gu" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore/v1/models" + "github.com/zitadel/zitadel/internal/query" + app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta" +) + +func CreateSAMLAppRequestToDomain(name, projectID string, req *app.CreateSAMLApplicationRequest) (*domain.SAMLApp, error) { + loginVersion, loginBaseURI, err := loginVersionToDomain(req.GetLoginVersion()) + if err != nil { + return nil, err + } + return &domain.SAMLApp{ + ObjectRoot: models.ObjectRoot{ + AggregateID: projectID, + }, + AppName: name, + Metadata: req.GetMetadataXml(), + MetadataURL: gu.Ptr(req.GetMetadataUrl()), + LoginVersion: loginVersion, + LoginBaseURI: loginBaseURI, + }, nil +} + +func UpdateSAMLAppConfigRequestToDomain(appID, projectID string, app *app.UpdateSAMLApplicationConfigurationRequest) (*domain.SAMLApp, error) { + loginVersion, loginBaseURI, err := loginVersionToDomain(app.GetLoginVersion()) + if err != nil { + return nil, err + } + + metasXML, metasURL := metasToDomain(app.GetMetadata()) + return &domain.SAMLApp{ + ObjectRoot: models.ObjectRoot{ + AggregateID: projectID, + }, + AppID: appID, + Metadata: metasXML, + MetadataURL: metasURL, + LoginVersion: loginVersion, + LoginBaseURI: loginBaseURI, + }, nil +} + +func metasToDomain(metas app.MetaType) ([]byte, *string) { + switch t := metas.(type) { + case *app.UpdateSAMLApplicationConfigurationRequest_MetadataXml: + return t.MetadataXml, nil + case *app.UpdateSAMLApplicationConfigurationRequest_MetadataUrl: + return nil, &t.MetadataUrl + case nil: + return nil, nil + default: + return nil, nil + } +} + +func appSAMLConfigToPb(samlApp *query.SAMLApp) app.ApplicationConfig { + if samlApp == nil { + return &app.Application_SamlConfig{ + SamlConfig: &app.SAMLConfig{ + Metadata: &app.SAMLConfig_MetadataXml{}, + LoginVersion: &app.LoginVersion{}, + }, + } + } + + return &app.Application_SamlConfig{ + SamlConfig: &app.SAMLConfig{ + Metadata: &app.SAMLConfig_MetadataXml{MetadataXml: samlApp.Metadata}, + LoginVersion: loginVersionToPb(samlApp.LoginVersion, samlApp.LoginBaseURI), + }, + } +} diff --git a/internal/api/grpc/app/v2beta/convert/saml_app_test.go b/internal/api/grpc/app/v2beta/convert/saml_app_test.go new file mode 100644 index 0000000000..b41ec432b6 --- /dev/null +++ b/internal/api/grpc/app/v2beta/convert/saml_app_test.go @@ -0,0 +1,256 @@ +package convert + +import ( + "fmt" + "net/url" + "testing" + + "github.com/brianvoe/gofakeit/v6" + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore/v1/models" + "github.com/zitadel/zitadel/internal/query" + app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta" +) + +func samlMetadataGen(entityID string) []byte { + str := fmt.Sprintf(` + + + urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified + + + + +`, + entityID) + + return []byte(str) +} + +func TestCreateSAMLAppRequestToDomain(t *testing.T) { + t.Parallel() + + genMetaForValidRequest := samlMetadataGen(gofakeit.URL()) + + tt := []struct { + testName string + appName string + projectID string + req *app.CreateSAMLApplicationRequest + + expectedResponse *domain.SAMLApp + expectedError error + }{ + { + testName: "login version error", + appName: "test-app", + projectID: "proj-1", + req: &app.CreateSAMLApplicationRequest{ + Metadata: &app.CreateSAMLApplicationRequest_MetadataXml{ + MetadataXml: samlMetadataGen(gofakeit.URL()), + }, + LoginVersion: &app.LoginVersion{ + Version: &app.LoginVersion_LoginV2{ + LoginV2: &app.LoginV2{BaseUri: gu.Ptr("%+o")}, + }, + }, + }, + expectedError: &url.Error{ + URL: "%+o", + Op: "parse", + Err: url.EscapeError("%+o"), + }, + }, + { + testName: "valid request", + appName: "test-app", + projectID: "proj-1", + req: &app.CreateSAMLApplicationRequest{ + Metadata: &app.CreateSAMLApplicationRequest_MetadataXml{ + MetadataXml: genMetaForValidRequest, + }, + LoginVersion: nil, + }, + + expectedResponse: &domain.SAMLApp{ + ObjectRoot: models.ObjectRoot{AggregateID: "proj-1"}, + AppName: "test-app", + Metadata: genMetaForValidRequest, + MetadataURL: gu.Ptr(""), + LoginVersion: gu.Ptr(domain.LoginVersionUnspecified), + LoginBaseURI: gu.Ptr(""), + State: 0, + }, + }, + { + testName: "nil request", + appName: "test-app", + projectID: "proj-1", + req: nil, + + expectedResponse: &domain.SAMLApp{ + AppName: "test-app", + ObjectRoot: models.ObjectRoot{AggregateID: "proj-1"}, + MetadataURL: gu.Ptr(""), + LoginVersion: gu.Ptr(domain.LoginVersionUnspecified), + LoginBaseURI: gu.Ptr(""), + }, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + + // When + res, err := CreateSAMLAppRequestToDomain(tc.appName, tc.projectID, tc.req) + + // Then + assert.Equal(t, tc.expectedError, err) + assert.Equal(t, tc.expectedResponse, res) + }) + } +} +func TestUpdateSAMLAppConfigRequestToDomain(t *testing.T) { + t.Parallel() + + genMetaForValidRequest := samlMetadataGen(gofakeit.URL()) + + tt := []struct { + testName string + appID string + projectID string + req *app.UpdateSAMLApplicationConfigurationRequest + + expectedResponse *domain.SAMLApp + expectedError error + }{ + { + testName: "login version error", + appID: "app-1", + projectID: "proj-1", + req: &app.UpdateSAMLApplicationConfigurationRequest{ + Metadata: &app.UpdateSAMLApplicationConfigurationRequest_MetadataXml{ + MetadataXml: samlMetadataGen(gofakeit.URL()), + }, + LoginVersion: &app.LoginVersion{ + Version: &app.LoginVersion_LoginV2{ + LoginV2: &app.LoginV2{BaseUri: gu.Ptr("%+o")}, + }, + }, + }, + expectedError: &url.Error{ + URL: "%+o", + Op: "parse", + Err: url.EscapeError("%+o"), + }, + }, + { + testName: "valid request", + appID: "app-1", + projectID: "proj-1", + req: &app.UpdateSAMLApplicationConfigurationRequest{ + Metadata: &app.UpdateSAMLApplicationConfigurationRequest_MetadataXml{ + MetadataXml: genMetaForValidRequest, + }, + LoginVersion: nil, + }, + expectedResponse: &domain.SAMLApp{ + ObjectRoot: models.ObjectRoot{AggregateID: "proj-1"}, + AppID: "app-1", + Metadata: genMetaForValidRequest, + LoginVersion: gu.Ptr(domain.LoginVersionUnspecified), + LoginBaseURI: gu.Ptr(""), + }, + }, + { + testName: "nil request", + appID: "app-1", + projectID: "proj-1", + req: nil, + expectedResponse: &domain.SAMLApp{ + ObjectRoot: models.ObjectRoot{AggregateID: "proj-1"}, + AppID: "app-1", + LoginVersion: gu.Ptr(domain.LoginVersionUnspecified), + LoginBaseURI: gu.Ptr(""), + }, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + + // When + res, err := UpdateSAMLAppConfigRequestToDomain(tc.appID, tc.projectID, tc.req) + + // Then + assert.Equal(t, tc.expectedError, err) + assert.Equal(t, tc.expectedResponse, res) + }) + } +} + +func TestAppSAMLConfigToPb(t *testing.T) { + t.Parallel() + + metadata := samlMetadataGen(gofakeit.URL()) + + tt := []struct { + name string + inputSAMLApp *query.SAMLApp + + expectedPbApp app.ApplicationConfig + }{ + { + name: "valid conversion", + inputSAMLApp: &query.SAMLApp{ + Metadata: metadata, + LoginVersion: domain.LoginVersion2, + LoginBaseURI: gu.Ptr("https://example.com"), + }, + expectedPbApp: &app.Application_SamlConfig{ + SamlConfig: &app.SAMLConfig{ + Metadata: &app.SAMLConfig_MetadataXml{ + MetadataXml: metadata, + }, + LoginVersion: &app.LoginVersion{ + Version: &app.LoginVersion_LoginV2{ + LoginV2: &app.LoginV2{BaseUri: gu.Ptr("https://example.com")}, + }, + }, + }, + }, + }, + { + name: "nil saml app", + inputSAMLApp: nil, + expectedPbApp: &app.Application_SamlConfig{ + SamlConfig: &app.SAMLConfig{ + Metadata: &app.SAMLConfig_MetadataXml{}, + LoginVersion: &app.LoginVersion{}, + }, + }, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + // When + got := appSAMLConfigToPb(tc.inputSAMLApp) + + // Then + assert.Equal(t, tc.expectedPbApp, got) + }) + } +} diff --git a/internal/api/grpc/app/v2beta/integration_test/app_key_test.go b/internal/api/grpc/app/v2beta/integration_test/app_key_test.go new file mode 100644 index 0000000000..7c3c886cff --- /dev/null +++ b/internal/api/grpc/app/v2beta/integration_test/app_key_test.go @@ -0,0 +1,206 @@ +//go:build integration + +package app_test + +import ( + "context" + "testing" + "time" + + "github.com/brianvoe/gofakeit/v6" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/timestamppb" + + app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta" +) + +func TestCreateApplicationKey(t *testing.T) { + p, projectOwnerCtx := getProjectAndProjectContext(t, instance, IAMOwnerCtx) + createdApp := createAPIApp(t, IAMOwnerCtx, instance, p.GetId()) + + t.Parallel() + + tt := []struct { + testName string + creationRequest *app.CreateApplicationKeyRequest + inputCtx context.Context + + expectedErrorType codes.Code + }{ + { + testName: "when app id is not found should return failed precondition", + inputCtx: IAMOwnerCtx, + creationRequest: &app.CreateApplicationKeyRequest{ + ProjectId: p.GetId(), + AppId: gofakeit.UUID(), + ExpirationDate: timestamppb.New(time.Now().AddDate(0, 0, 1).UTC()), + }, + expectedErrorType: codes.FailedPrecondition, + }, + { + testName: "when CreateAPIApp request is valid should create app and return no error", + inputCtx: IAMOwnerCtx, + creationRequest: &app.CreateApplicationKeyRequest{ + ProjectId: p.GetId(), + AppId: createdApp.GetAppId(), + ExpirationDate: timestamppb.New(time.Now().AddDate(0, 0, 1).UTC()), + }, + }, + + // LoginUser + { + testName: "when user has no project.app.write permission for app key generation should return permission error", + inputCtx: LoginUserCtx, + creationRequest: &app.CreateApplicationKeyRequest{ + ProjectId: p.GetId(), + AppId: createdApp.GetAppId(), + ExpirationDate: timestamppb.New(time.Now().AddDate(0, 0, 1).UTC()), + }, + expectedErrorType: codes.PermissionDenied, + }, + + // OrgOwner + { + testName: "when user is OrgOwner app key request should succeed", + inputCtx: OrgOwnerCtx, + creationRequest: &app.CreateApplicationKeyRequest{ + ProjectId: p.GetId(), + AppId: createdApp.GetAppId(), + ExpirationDate: timestamppb.New(time.Now().AddDate(0, 0, 1).UTC()), + }, + }, + + // ProjectOwner + { + testName: "when user is ProjectOwner app key request should succeed", + inputCtx: projectOwnerCtx, + creationRequest: &app.CreateApplicationKeyRequest{ + ProjectId: p.GetId(), + AppId: createdApp.GetAppId(), + ExpirationDate: timestamppb.New(time.Now().AddDate(0, 0, 1).UTC()), + }, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + res, err := instance.Client.AppV2Beta.CreateApplicationKey(tc.inputCtx, tc.creationRequest) + + require.Equal(t, tc.expectedErrorType, status.Code(err)) + if tc.expectedErrorType == codes.OK { + assert.NotZero(t, res.GetId()) + assert.NotZero(t, res.GetCreationDate()) + assert.NotZero(t, res.GetKeyDetails()) + } + }) + } +} + +func TestDeleteApplicationKey(t *testing.T) { + p, projectOwnerCtx := getProjectAndProjectContext(t, instance, IAMOwnerCtx) + createdApp := createAPIApp(t, IAMOwnerCtx, instance, p.GetId()) + + t.Parallel() + + tt := []struct { + testName string + deletionRequest func(ttt *testing.T) *app.DeleteApplicationKeyRequest + inputCtx context.Context + + expectedErrorType codes.Code + }{ + { + testName: "when app key ID is not found should return not found error", + inputCtx: IAMOwnerCtx, + deletionRequest: func(ttt *testing.T) *app.DeleteApplicationKeyRequest { + return &app.DeleteApplicationKeyRequest{ + Id: gofakeit.UUID(), + ProjectId: p.GetId(), + ApplicationId: createdApp.GetAppId(), + } + }, + expectedErrorType: codes.NotFound, + }, + { + testName: "when valid app key ID should delete successfully", + inputCtx: IAMOwnerCtx, + deletionRequest: func(ttt *testing.T) *app.DeleteApplicationKeyRequest { + createdAppKey := createAppKey(ttt, IAMOwnerCtx, instance, p.GetId(), createdApp.GetAppId(), time.Now().AddDate(0, 0, 1)) + + return &app.DeleteApplicationKeyRequest{ + Id: createdAppKey.GetId(), + ProjectId: p.GetId(), + ApplicationId: createdApp.GetAppId(), + } + }, + }, + + // LoginUser + { + testName: "when user has no project.app.write permission for app key deletion should return permission error", + inputCtx: LoginUserCtx, + deletionRequest: func(ttt *testing.T) *app.DeleteApplicationKeyRequest { + createdAppKey := createAppKey(ttt, IAMOwnerCtx, instance, p.GetId(), createdApp.GetAppId(), time.Now().AddDate(0, 0, 1)) + + return &app.DeleteApplicationKeyRequest{ + Id: createdAppKey.GetId(), + ProjectId: p.GetId(), + ApplicationId: createdApp.GetAppId(), + } + }, + expectedErrorType: codes.PermissionDenied, + }, + + // ProjectOwner + { + testName: "when user is OrgOwner API request should succeed", + inputCtx: projectOwnerCtx, + deletionRequest: func(ttt *testing.T) *app.DeleteApplicationKeyRequest { + createdAppKey := createAppKey(ttt, IAMOwnerCtx, instance, p.GetId(), createdApp.GetAppId(), time.Now().AddDate(0, 0, 1)) + + return &app.DeleteApplicationKeyRequest{ + Id: createdAppKey.GetId(), + ProjectId: p.GetId(), + ApplicationId: createdApp.GetAppId(), + } + }, + }, + + // OrganizationOwner + { + testName: "when user is OrgOwner app key deletion request should succeed", + inputCtx: OrgOwnerCtx, + deletionRequest: func(ttt *testing.T) *app.DeleteApplicationKeyRequest { + createdAppKey := createAppKey(ttt, IAMOwnerCtx, instance, p.GetId(), createdApp.GetAppId(), time.Now().AddDate(0, 0, 1)) + + return &app.DeleteApplicationKeyRequest{ + Id: createdAppKey.GetId(), + ProjectId: p.GetId(), + ApplicationId: createdApp.GetAppId(), + } + }, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + + // Given + deletionReq := tc.deletionRequest(t) + + // When + res, err := instance.Client.AppV2Beta.DeleteApplicationKey(tc.inputCtx, deletionReq) + + // Then + require.Equal(t, tc.expectedErrorType, status.Code(err)) + if tc.expectedErrorType == codes.OK { + assert.NotEmpty(t, res.GetDeletionDate()) + } + }) + } +} diff --git a/internal/api/grpc/app/v2beta/integration_test/app_test.go b/internal/api/grpc/app/v2beta/integration_test/app_test.go new file mode 100644 index 0000000000..67e59aa91d --- /dev/null +++ b/internal/api/grpc/app/v2beta/integration_test/app_test.go @@ -0,0 +1,1446 @@ +//go:build integration + +package app_test + +import ( + "context" + "fmt" + "testing" + + "github.com/brianvoe/gofakeit/v6" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta" + org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" +) + +func TestCreateApplication(t *testing.T) { + p := instance.CreateProject(IAMOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.Name(), false, false) + + t.Parallel() + + notExistingProjectID := gofakeit.UUID() + + tt := []struct { + testName string + creationRequest *app.CreateApplicationRequest + inputCtx context.Context + + expectedResponseType string + expectedErrorType codes.Code + }{ + { + testName: "when project for API app creation is not found should return failed precondition error", + inputCtx: IAMOwnerCtx, + creationRequest: &app.CreateApplicationRequest{ + ProjectId: notExistingProjectID, + Name: "App Name", + CreationRequestType: &app.CreateApplicationRequest_ApiRequest{ + ApiRequest: &app.CreateAPIApplicationRequest{ + AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT, + }, + }, + }, + expectedErrorType: codes.FailedPrecondition, + }, + { + testName: "when CreateAPIApp request is valid should create app and return no error", + inputCtx: IAMOwnerCtx, + creationRequest: &app.CreateApplicationRequest{ + ProjectId: p.GetId(), + Name: "App Name", + CreationRequestType: &app.CreateApplicationRequest_ApiRequest{ + ApiRequest: &app.CreateAPIApplicationRequest{ + AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT, + }, + }, + }, + expectedResponseType: fmt.Sprintf("%T", &app.CreateApplicationResponse_ApiResponse{}), + }, + { + testName: "when project for OIDC app creation is not found should return failed precondition error", + inputCtx: IAMOwnerCtx, + creationRequest: &app.CreateApplicationRequest{ + ProjectId: notExistingProjectID, + Name: "App Name", + CreationRequestType: &app.CreateApplicationRequest_OidcRequest{ + OidcRequest: &app.CreateOIDCApplicationRequest{ + RedirectUris: []string{"http://example.com"}, + ResponseTypes: []app.OIDCResponseType{app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE}, + GrantTypes: []app.OIDCGrantType{app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE}, + AppType: app.OIDCAppType_OIDC_APP_TYPE_WEB, + AuthMethodType: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC, + PostLogoutRedirectUris: []string{"http://example.com/home"}, + Version: app.OIDCVersion_OIDC_VERSION_1_0, + AccessTokenType: app.OIDCTokenType_OIDC_TOKEN_TYPE_JWT, + BackChannelLogoutUri: "http://example.com/logout", + LoginVersion: &app.LoginVersion{ + Version: &app.LoginVersion_LoginV2{ + LoginV2: &app.LoginV2{ + BaseUri: &baseURI, + }, + }, + }, + }, + }, + }, + expectedErrorType: codes.FailedPrecondition, + }, + { + testName: "when CreateOIDCApp request is valid should create app and return no error", + inputCtx: IAMOwnerCtx, + creationRequest: &app.CreateApplicationRequest{ + ProjectId: p.GetId(), + Name: gofakeit.AppName(), + CreationRequestType: &app.CreateApplicationRequest_OidcRequest{ + OidcRequest: &app.CreateOIDCApplicationRequest{ + RedirectUris: []string{"http://example.com"}, + ResponseTypes: []app.OIDCResponseType{app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE}, + GrantTypes: []app.OIDCGrantType{app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE}, + AppType: app.OIDCAppType_OIDC_APP_TYPE_WEB, + AuthMethodType: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC, + PostLogoutRedirectUris: []string{"http://example.com/home"}, + Version: app.OIDCVersion_OIDC_VERSION_1_0, + AccessTokenType: app.OIDCTokenType_OIDC_TOKEN_TYPE_JWT, + BackChannelLogoutUri: "http://example.com/logout", + LoginVersion: &app.LoginVersion{ + Version: &app.LoginVersion_LoginV2{ + LoginV2: &app.LoginV2{ + BaseUri: &baseURI, + }, + }, + }, + }, + }, + }, + + expectedResponseType: fmt.Sprintf("%T", &app.CreateApplicationResponse_OidcResponse{}), + }, + { + testName: "when project for SAML app creation is not found should return failed precondition error", + inputCtx: IAMOwnerCtx, + creationRequest: &app.CreateApplicationRequest{ + ProjectId: notExistingProjectID, + Name: gofakeit.AppName(), + CreationRequestType: &app.CreateApplicationRequest_SamlRequest{ + SamlRequest: &app.CreateSAMLApplicationRequest{ + Metadata: &app.CreateSAMLApplicationRequest_MetadataUrl{ + MetadataUrl: "http://example.com/metas", + }, + LoginVersion: &app.LoginVersion{ + Version: &app.LoginVersion_LoginV2{ + LoginV2: &app.LoginV2{ + BaseUri: &baseURI, + }, + }, + }, + }, + }, + }, + expectedErrorType: codes.FailedPrecondition, + }, + { + testName: "when CreateSAMLApp request is valid should create app and return no error", + inputCtx: IAMOwnerCtx, + creationRequest: &app.CreateApplicationRequest{ + ProjectId: p.GetId(), + Name: gofakeit.AppName(), + CreationRequestType: &app.CreateApplicationRequest_SamlRequest{ + SamlRequest: &app.CreateSAMLApplicationRequest{ + Metadata: &app.CreateSAMLApplicationRequest_MetadataXml{ + MetadataXml: samlMetadataGen(gofakeit.URL()), + }, + LoginVersion: &app.LoginVersion{ + Version: &app.LoginVersion_LoginV2{ + LoginV2: &app.LoginV2{ + BaseUri: &baseURI, + }, + }, + }, + }, + }, + }, + expectedResponseType: fmt.Sprintf("%T", &app.CreateApplicationResponse_SamlResponse{}), + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + res, err := instance.Client.AppV2Beta.CreateApplication(tc.inputCtx, tc.creationRequest) + + require.Equal(t, tc.expectedErrorType, status.Code(err)) + if tc.expectedErrorType == codes.OK { + resType := fmt.Sprintf("%T", res.GetCreationResponseType()) + assert.Equal(t, tc.expectedResponseType, resType) + assert.NotZero(t, res.GetAppId()) + assert.NotZero(t, res.GetCreationDate()) + } + }) + } +} + +func TestCreateApplication_WithDifferentPermissions(t *testing.T) { + p, projectOwnerCtx := getProjectAndProjectContext(t, instance, IAMOwnerCtx) + + t.Parallel() + + tt := []struct { + testName string + creationRequest *app.CreateApplicationRequest + inputCtx context.Context + + expectedResponseType string + expectedErrorType codes.Code + }{ + // Login User with no project.app.write + { + testName: "when user has no project.app.write permission for API request should return permission error", + inputCtx: LoginUserCtx, + creationRequest: &app.CreateApplicationRequest{ + ProjectId: p.GetId(), + Name: gofakeit.Name(), + CreationRequestType: &app.CreateApplicationRequest_ApiRequest{ + ApiRequest: &app.CreateAPIApplicationRequest{ + AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT, + }, + }, + }, + expectedErrorType: codes.PermissionDenied, + }, + { + testName: "when user has no project.app.write permission for OIDC request should return permission error", + inputCtx: LoginUserCtx, + creationRequest: &app.CreateApplicationRequest{ + ProjectId: p.GetId(), + Name: gofakeit.AppName(), + CreationRequestType: &app.CreateApplicationRequest_OidcRequest{ + OidcRequest: &app.CreateOIDCApplicationRequest{ + RedirectUris: []string{"http://example.com"}, + ResponseTypes: []app.OIDCResponseType{app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE}, + GrantTypes: []app.OIDCGrantType{app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE}, + AppType: app.OIDCAppType_OIDC_APP_TYPE_WEB, + AuthMethodType: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC, + PostLogoutRedirectUris: []string{"http://example.com/home"}, + Version: app.OIDCVersion_OIDC_VERSION_1_0, + AccessTokenType: app.OIDCTokenType_OIDC_TOKEN_TYPE_JWT, + BackChannelLogoutUri: "http://example.com/logout", + LoginVersion: &app.LoginVersion{ + Version: &app.LoginVersion_LoginV2{ + LoginV2: &app.LoginV2{ + BaseUri: &baseURI, + }, + }, + }, + }, + }, + }, + + expectedErrorType: codes.PermissionDenied, + }, + { + testName: "when user has no project.app.write permission for SAML request should return permission error", + inputCtx: LoginUserCtx, + creationRequest: &app.CreateApplicationRequest{ + ProjectId: p.GetId(), + Name: gofakeit.AppName(), + CreationRequestType: &app.CreateApplicationRequest_SamlRequest{ + SamlRequest: &app.CreateSAMLApplicationRequest{ + Metadata: &app.CreateSAMLApplicationRequest_MetadataXml{ + MetadataXml: samlMetadataGen(gofakeit.URL()), + }, + LoginVersion: &app.LoginVersion{ + Version: &app.LoginVersion_LoginV2{ + LoginV2: &app.LoginV2{ + BaseUri: &baseURI, + }, + }, + }, + }, + }, + }, + expectedErrorType: codes.PermissionDenied, + }, + + // OrgOwner with project.app.write permission + { + testName: "when user is OrgOwner API request should succeed", + inputCtx: OrgOwnerCtx, + creationRequest: &app.CreateApplicationRequest{ + ProjectId: p.GetId(), + Name: gofakeit.Name(), + CreationRequestType: &app.CreateApplicationRequest_ApiRequest{ + ApiRequest: &app.CreateAPIApplicationRequest{ + AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT, + }, + }, + }, + expectedResponseType: fmt.Sprintf("%T", &app.CreateApplicationResponse_ApiResponse{}), + }, + { + testName: "when user is OrgOwner OIDC request should succeed", + inputCtx: OrgOwnerCtx, + creationRequest: &app.CreateApplicationRequest{ + ProjectId: p.GetId(), + Name: gofakeit.AppName(), + CreationRequestType: &app.CreateApplicationRequest_OidcRequest{ + OidcRequest: &app.CreateOIDCApplicationRequest{ + RedirectUris: []string{"http://example.com"}, + ResponseTypes: []app.OIDCResponseType{app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE}, + GrantTypes: []app.OIDCGrantType{app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE}, + AppType: app.OIDCAppType_OIDC_APP_TYPE_WEB, + AuthMethodType: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC, + PostLogoutRedirectUris: []string{"http://example.com/home"}, + Version: app.OIDCVersion_OIDC_VERSION_1_0, + AccessTokenType: app.OIDCTokenType_OIDC_TOKEN_TYPE_JWT, + BackChannelLogoutUri: "http://example.com/logout", + LoginVersion: &app.LoginVersion{ + Version: &app.LoginVersion_LoginV2{ + LoginV2: &app.LoginV2{ + BaseUri: &baseURI, + }, + }, + }, + }, + }, + }, + + expectedResponseType: fmt.Sprintf("%T", &app.CreateApplicationResponse_OidcResponse{}), + }, + { + testName: "when user is OrgOwner SAML request should succeed", + inputCtx: OrgOwnerCtx, + creationRequest: &app.CreateApplicationRequest{ + ProjectId: p.GetId(), + Name: gofakeit.AppName(), + CreationRequestType: &app.CreateApplicationRequest_SamlRequest{ + SamlRequest: &app.CreateSAMLApplicationRequest{ + Metadata: &app.CreateSAMLApplicationRequest_MetadataXml{ + MetadataXml: samlMetadataGen(gofakeit.URL()), + }, + LoginVersion: &app.LoginVersion{ + Version: &app.LoginVersion_LoginV2{ + LoginV2: &app.LoginV2{ + BaseUri: &baseURI, + }, + }, + }, + }, + }, + }, + expectedResponseType: fmt.Sprintf("%T", &app.CreateApplicationResponse_SamlResponse{}), + }, + + // Project owner with project.app.write permission + { + testName: "when user is ProjectOwner API request should succeed", + inputCtx: projectOwnerCtx, + creationRequest: &app.CreateApplicationRequest{ + ProjectId: p.GetId(), + Name: gofakeit.Name(), + CreationRequestType: &app.CreateApplicationRequest_ApiRequest{ + ApiRequest: &app.CreateAPIApplicationRequest{ + AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT, + }, + }, + }, + expectedResponseType: fmt.Sprintf("%T", &app.CreateApplicationResponse_ApiResponse{}), + }, + { + testName: "when user is ProjectOwner OIDC request should succeed", + inputCtx: projectOwnerCtx, + creationRequest: &app.CreateApplicationRequest{ + ProjectId: p.GetId(), + Name: gofakeit.AppName(), + CreationRequestType: &app.CreateApplicationRequest_OidcRequest{ + OidcRequest: &app.CreateOIDCApplicationRequest{ + RedirectUris: []string{"http://example.com"}, + ResponseTypes: []app.OIDCResponseType{app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE}, + GrantTypes: []app.OIDCGrantType{app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE}, + AppType: app.OIDCAppType_OIDC_APP_TYPE_WEB, + AuthMethodType: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC, + PostLogoutRedirectUris: []string{"http://example.com/home"}, + Version: app.OIDCVersion_OIDC_VERSION_1_0, + AccessTokenType: app.OIDCTokenType_OIDC_TOKEN_TYPE_JWT, + BackChannelLogoutUri: "http://example.com/logout", + LoginVersion: &app.LoginVersion{ + Version: &app.LoginVersion_LoginV2{ + LoginV2: &app.LoginV2{ + BaseUri: &baseURI, + }, + }, + }, + }, + }, + }, + + expectedResponseType: fmt.Sprintf("%T", &app.CreateApplicationResponse_OidcResponse{}), + }, + { + testName: "when user is ProjectOwner SAML request should succeed", + inputCtx: projectOwnerCtx, + creationRequest: &app.CreateApplicationRequest{ + ProjectId: p.GetId(), + Name: gofakeit.AppName(), + CreationRequestType: &app.CreateApplicationRequest_SamlRequest{ + SamlRequest: &app.CreateSAMLApplicationRequest{ + Metadata: &app.CreateSAMLApplicationRequest_MetadataXml{ + MetadataXml: samlMetadataGen(gofakeit.URL()), + }, + LoginVersion: &app.LoginVersion{ + Version: &app.LoginVersion_LoginV2{ + LoginV2: &app.LoginV2{ + BaseUri: &baseURI, + }, + }, + }, + }, + }, + }, + expectedResponseType: fmt.Sprintf("%T", &app.CreateApplicationResponse_SamlResponse{}), + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + res, err := instance.Client.AppV2Beta.CreateApplication(tc.inputCtx, tc.creationRequest) + + require.Equal(t, tc.expectedErrorType, status.Code(err)) + if tc.expectedErrorType == codes.OK { + resType := fmt.Sprintf("%T", res.GetCreationResponseType()) + assert.Equal(t, tc.expectedResponseType, resType) + assert.NotZero(t, res.GetAppId()) + assert.NotZero(t, res.GetCreationDate()) + } + }) + } +} + +func TestUpdateApplication(t *testing.T) { + orgNotInCtx := instance.CreateOrganization(IAMOwnerCtx, gofakeit.Name(), gofakeit.Email()) + pNotInCtx := instance.CreateProject(IAMOwnerCtx, t, orgNotInCtx.GetOrganizationId(), gofakeit.AppName(), false, false) + + p := instance.CreateProject(IAMOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.Name(), false, false) + + baseURI := "http://example.com" + + t.Cleanup(func() { + instance.Client.OrgV2beta.DeleteOrganization(IAMOwnerCtx, &org.DeleteOrganizationRequest{ + Id: orgNotInCtx.GetOrganizationId(), + }) + }) + + reqForAppNameCreation := &app.CreateApplicationRequest_ApiRequest{ + ApiRequest: &app.CreateAPIApplicationRequest{AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT}, + } + reqForAPIAppCreation := reqForAppNameCreation + + reqForOIDCAppCreation := &app.CreateApplicationRequest_OidcRequest{ + OidcRequest: &app.CreateOIDCApplicationRequest{ + RedirectUris: []string{"http://example.com"}, + ResponseTypes: []app.OIDCResponseType{app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE}, + GrantTypes: []app.OIDCGrantType{app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE}, + AppType: app.OIDCAppType_OIDC_APP_TYPE_WEB, + AuthMethodType: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC, + PostLogoutRedirectUris: []string{"http://example.com/home"}, + Version: app.OIDCVersion_OIDC_VERSION_1_0, + AccessTokenType: app.OIDCTokenType_OIDC_TOKEN_TYPE_JWT, + BackChannelLogoutUri: "http://example.com/logout", + LoginVersion: &app.LoginVersion{ + Version: &app.LoginVersion_LoginV2{ + LoginV2: &app.LoginV2{ + BaseUri: &baseURI, + }, + }, + }, + }, + } + + samlMetas := samlMetadataGen(gofakeit.URL()) + reqForSAMLAppCreation := &app.CreateApplicationRequest_SamlRequest{ + SamlRequest: &app.CreateSAMLApplicationRequest{ + Metadata: &app.CreateSAMLApplicationRequest_MetadataXml{ + MetadataXml: samlMetas, + }, + LoginVersion: &app.LoginVersion{ + Version: &app.LoginVersion_LoginV2{ + LoginV2: &app.LoginV2{ + BaseUri: &baseURI, + }, + }, + }, + }, + } + + appForNameChange, appNameChangeErr := instance.Client.AppV2Beta.CreateApplication(IAMOwnerCtx, &app.CreateApplicationRequest{ + ProjectId: p.GetId(), + Name: gofakeit.AppName(), + CreationRequestType: reqForAppNameCreation, + }) + require.Nil(t, appNameChangeErr) + + appForAPIConfigChange, appAPIConfigChangeErr := instance.Client.AppV2Beta.CreateApplication(IAMOwnerCtx, &app.CreateApplicationRequest{ + ProjectId: p.GetId(), + Name: gofakeit.AppName(), + CreationRequestType: reqForAPIAppCreation, + }) + require.Nil(t, appAPIConfigChangeErr) + + appForOIDCConfigChange, appOIDCConfigChangeErr := instance.Client.AppV2Beta.CreateApplication(IAMOwnerCtx, &app.CreateApplicationRequest{ + ProjectId: p.GetId(), + Name: gofakeit.AppName(), + CreationRequestType: reqForOIDCAppCreation, + }) + require.Nil(t, appOIDCConfigChangeErr) + + appForSAMLConfigChange, appSAMLConfigChangeErr := instance.Client.AppV2Beta.CreateApplication(IAMOwnerCtx, &app.CreateApplicationRequest{ + ProjectId: p.GetId(), + Name: gofakeit.AppName(), + CreationRequestType: reqForSAMLAppCreation, + }) + require.Nil(t, appSAMLConfigChangeErr) + + t.Parallel() + + tt := []struct { + testName string + inputCtx context.Context + updateRequest *app.UpdateApplicationRequest + + expectedErrorType codes.Code + }{ + { + testName: "when app for app name change request is not found should return not found error", + inputCtx: IAMOwnerCtx, + updateRequest: &app.UpdateApplicationRequest{ + ProjectId: pNotInCtx.GetId(), + Id: appForNameChange.GetAppId(), + Name: "New name", + }, + expectedErrorType: codes.NotFound, + }, + { + testName: "when request for app name change is valid should return updated timestamp", + inputCtx: IAMOwnerCtx, + updateRequest: &app.UpdateApplicationRequest{ + ProjectId: p.GetId(), + Id: appForNameChange.GetAppId(), + + Name: "New name", + }, + }, + + { + testName: "when app for API config change request is not found should return not found error", + inputCtx: IAMOwnerCtx, + updateRequest: &app.UpdateApplicationRequest{ + ProjectId: pNotInCtx.GetId(), + Id: appForAPIConfigChange.GetAppId(), + UpdateRequestType: &app.UpdateApplicationRequest_ApiConfigurationRequest{ + ApiConfigurationRequest: &app.UpdateAPIApplicationConfigurationRequest{ + AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT, + }, + }, + }, + expectedErrorType: codes.NotFound, + }, + { + testName: "when request for API config change is valid should return updated timestamp", + inputCtx: IAMOwnerCtx, + updateRequest: &app.UpdateApplicationRequest{ + ProjectId: p.GetId(), + Id: appForAPIConfigChange.GetAppId(), + UpdateRequestType: &app.UpdateApplicationRequest_ApiConfigurationRequest{ + ApiConfigurationRequest: &app.UpdateAPIApplicationConfigurationRequest{ + AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_BASIC, + }, + }, + }, + }, + { + testName: "when app for OIDC config change request is not found should return not found error", + inputCtx: IAMOwnerCtx, + updateRequest: &app.UpdateApplicationRequest{ + ProjectId: pNotInCtx.GetId(), + Id: appForOIDCConfigChange.GetAppId(), + UpdateRequestType: &app.UpdateApplicationRequest_OidcConfigurationRequest{ + OidcConfigurationRequest: &app.UpdateOIDCApplicationConfigurationRequest{ + PostLogoutRedirectUris: []string{"http://example.com/home2"}, + }, + }, + }, + expectedErrorType: codes.NotFound, + }, + { + testName: "when request for OIDC config change is valid should return updated timestamp", + inputCtx: IAMOwnerCtx, + updateRequest: &app.UpdateApplicationRequest{ + ProjectId: p.GetId(), + Id: appForOIDCConfigChange.GetAppId(), + UpdateRequestType: &app.UpdateApplicationRequest_OidcConfigurationRequest{ + OidcConfigurationRequest: &app.UpdateOIDCApplicationConfigurationRequest{ + PostLogoutRedirectUris: []string{"http://example.com/home2"}, + }, + }, + }, + }, + + { + testName: "when app for SAML config change request is not found should return not found error", + inputCtx: IAMOwnerCtx, + updateRequest: &app.UpdateApplicationRequest{ + ProjectId: pNotInCtx.GetId(), + Id: appForSAMLConfigChange.GetAppId(), + UpdateRequestType: &app.UpdateApplicationRequest_SamlConfigurationRequest{ + SamlConfigurationRequest: &app.UpdateSAMLApplicationConfigurationRequest{ + Metadata: &app.UpdateSAMLApplicationConfigurationRequest_MetadataXml{ + MetadataXml: samlMetas, + }, + LoginVersion: &app.LoginVersion{Version: &app.LoginVersion_LoginV1{LoginV1: &app.LoginV1{}}}, + }, + }, + }, + expectedErrorType: codes.NotFound, + }, + { + testName: "when request for SAML config change is valid should return updated timestamp", + inputCtx: IAMOwnerCtx, + updateRequest: &app.UpdateApplicationRequest{ + ProjectId: p.GetId(), + Id: appForSAMLConfigChange.GetAppId(), + UpdateRequestType: &app.UpdateApplicationRequest_SamlConfigurationRequest{ + SamlConfigurationRequest: &app.UpdateSAMLApplicationConfigurationRequest{ + Metadata: &app.UpdateSAMLApplicationConfigurationRequest_MetadataXml{ + MetadataXml: samlMetas, + }, + LoginVersion: &app.LoginVersion{Version: &app.LoginVersion_LoginV1{LoginV1: &app.LoginV1{}}}, + }, + }, + }, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + res, err := instance.Client.AppV2Beta.UpdateApplication(tc.inputCtx, tc.updateRequest) + + require.Equal(t, tc.expectedErrorType, status.Code(err)) + if tc.expectedErrorType == codes.OK { + assert.NotZero(t, res.GetChangeDate()) + } + }) + } +} + +func TestUpdateApplication_WithDifferentPermissions(t *testing.T) { + baseURI := "http://example.com" + + p, projectOwnerCtx := getProjectAndProjectContext(t, instance, IAMOwnerCtx) + + reqForAppNameCreation := &app.CreateApplicationRequest_ApiRequest{ + ApiRequest: &app.CreateAPIApplicationRequest{AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT}, + } + + appForNameChange, appNameChangeErr := instance.Client.AppV2Beta.CreateApplication(IAMOwnerCtx, &app.CreateApplicationRequest{ + ProjectId: p.GetId(), + Name: gofakeit.AppName(), + CreationRequestType: reqForAppNameCreation, + }) + require.Nil(t, appNameChangeErr) + + appForAPIConfigChangeForProjectOwner := createAPIApp(t, IAMOwnerCtx, instance, p.GetId()) + appForAPIConfigChangeForOrgOwner := createAPIApp(t, IAMOwnerCtx, instance, p.GetId()) + appForAPIConfigChangeForLoginUser := createAPIApp(t, IAMOwnerCtx, instance, p.GetId()) + + appForOIDCConfigChangeForProjectOwner := createOIDCApp(t, baseURI, p.GetId()) + appForOIDCConfigChangeForOrgOwner := createOIDCApp(t, baseURI, p.GetId()) + appForOIDCConfigChangeForLoginUser := createOIDCApp(t, baseURI, p.GetId()) + + samlMetasForProjectOwner, appForSAMLConfigChangeForProjectOwner := createSAMLApp(t, baseURI, p.GetId()) + samlMetasForOrgOwner, appForSAMLConfigChangeForOrgOwner := createSAMLApp(t, baseURI, p.GetId()) + samlMetasForLoginUser, appForSAMLConfigChangeForLoginUser := createSAMLApp(t, baseURI, p.GetId()) + + t.Parallel() + + tt := []struct { + testName string + inputCtx context.Context + updateRequest *app.UpdateApplicationRequest + + expectedErrorType codes.Code + }{ + // ProjectOwner + { + testName: "when user is ProjectOwner app name request should succeed", + inputCtx: projectOwnerCtx, + updateRequest: &app.UpdateApplicationRequest{ + ProjectId: p.GetId(), + Id: appForNameChange.GetAppId(), + + Name: gofakeit.AppName(), + }, + }, + { + testName: "when user is ProjectOwner API app request should succeed", + inputCtx: projectOwnerCtx, + updateRequest: &app.UpdateApplicationRequest{ + ProjectId: p.GetId(), + Id: appForAPIConfigChangeForProjectOwner.GetAppId(), + UpdateRequestType: &app.UpdateApplicationRequest_ApiConfigurationRequest{ + ApiConfigurationRequest: &app.UpdateAPIApplicationConfigurationRequest{ + AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_BASIC, + }, + }, + }, + }, + { + testName: "when user is ProjectOwner OIDC app request should succeed", + inputCtx: projectOwnerCtx, + updateRequest: &app.UpdateApplicationRequest{ + ProjectId: p.GetId(), + Id: appForOIDCConfigChangeForProjectOwner.GetAppId(), + UpdateRequestType: &app.UpdateApplicationRequest_OidcConfigurationRequest{ + OidcConfigurationRequest: &app.UpdateOIDCApplicationConfigurationRequest{ + PostLogoutRedirectUris: []string{"http://example.com/home2"}, + }, + }, + }, + }, + { + testName: "when user is ProjectOwner SAML request should succeed", + inputCtx: projectOwnerCtx, + updateRequest: &app.UpdateApplicationRequest{ + ProjectId: p.GetId(), + Id: appForSAMLConfigChangeForProjectOwner.GetAppId(), + UpdateRequestType: &app.UpdateApplicationRequest_SamlConfigurationRequest{ + SamlConfigurationRequest: &app.UpdateSAMLApplicationConfigurationRequest{ + Metadata: &app.UpdateSAMLApplicationConfigurationRequest_MetadataXml{ + MetadataXml: samlMetasForProjectOwner, + }, + LoginVersion: &app.LoginVersion{Version: &app.LoginVersion_LoginV1{LoginV1: &app.LoginV1{}}}, + }, + }, + }, + }, + + // OrgOwner context + { + testName: "when user is OrgOwner app name request should succeed", + inputCtx: OrgOwnerCtx, + updateRequest: &app.UpdateApplicationRequest{ + ProjectId: p.GetId(), + Id: appForNameChange.GetAppId(), + + Name: gofakeit.AppName(), + }, + }, + { + testName: "when user is OrgOwner API app request should succeed", + inputCtx: OrgOwnerCtx, + updateRequest: &app.UpdateApplicationRequest{ + ProjectId: p.GetId(), + Id: appForAPIConfigChangeForOrgOwner.GetAppId(), + UpdateRequestType: &app.UpdateApplicationRequest_ApiConfigurationRequest{ + ApiConfigurationRequest: &app.UpdateAPIApplicationConfigurationRequest{ + AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_BASIC, + }, + }, + }, + }, + { + testName: "when user is OrgOwner OIDC app request should succeed", + inputCtx: OrgOwnerCtx, + updateRequest: &app.UpdateApplicationRequest{ + ProjectId: p.GetId(), + Id: appForOIDCConfigChangeForOrgOwner.GetAppId(), + UpdateRequestType: &app.UpdateApplicationRequest_OidcConfigurationRequest{ + OidcConfigurationRequest: &app.UpdateOIDCApplicationConfigurationRequest{ + PostLogoutRedirectUris: []string{"http://example.com/home2"}, + }, + }, + }, + }, + { + testName: "when user is OrgOwner SAML request should succeed", + inputCtx: OrgOwnerCtx, + updateRequest: &app.UpdateApplicationRequest{ + ProjectId: p.GetId(), + Id: appForSAMLConfigChangeForOrgOwner.GetAppId(), + UpdateRequestType: &app.UpdateApplicationRequest_SamlConfigurationRequest{ + SamlConfigurationRequest: &app.UpdateSAMLApplicationConfigurationRequest{ + Metadata: &app.UpdateSAMLApplicationConfigurationRequest_MetadataXml{ + MetadataXml: samlMetasForOrgOwner, + }, + LoginVersion: &app.LoginVersion{Version: &app.LoginVersion_LoginV1{LoginV1: &app.LoginV1{}}}, + }, + }, + }, + }, + + // LoginUser + { + testName: "when user has no project.app.write permission for app name change request should return permission error", + inputCtx: LoginUserCtx, + updateRequest: &app.UpdateApplicationRequest{ + ProjectId: p.GetId(), + Id: appForNameChange.GetAppId(), + + Name: gofakeit.AppName(), + }, + expectedErrorType: codes.PermissionDenied, + }, + { + testName: "when user has no project.app.write permission for API request should return permission error", + inputCtx: LoginUserCtx, + updateRequest: &app.UpdateApplicationRequest{ + ProjectId: p.GetId(), + Id: appForAPIConfigChangeForLoginUser.GetAppId(), + UpdateRequestType: &app.UpdateApplicationRequest_ApiConfigurationRequest{ + ApiConfigurationRequest: &app.UpdateAPIApplicationConfigurationRequest{ + AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_BASIC, + }, + }, + }, + expectedErrorType: codes.PermissionDenied, + }, + { + testName: "when user has no project.app.write permission for OIDC request should return permission error", + inputCtx: LoginUserCtx, + updateRequest: &app.UpdateApplicationRequest{ + ProjectId: p.GetId(), + Id: appForOIDCConfigChangeForLoginUser.GetAppId(), + UpdateRequestType: &app.UpdateApplicationRequest_OidcConfigurationRequest{ + OidcConfigurationRequest: &app.UpdateOIDCApplicationConfigurationRequest{ + PostLogoutRedirectUris: []string{"http://example.com/home2"}, + }, + }, + }, + expectedErrorType: codes.PermissionDenied, + }, + { + testName: "when user has no project.app.write permission for SAML request should return permission error", + inputCtx: LoginUserCtx, + updateRequest: &app.UpdateApplicationRequest{ + ProjectId: p.GetId(), + Id: appForSAMLConfigChangeForLoginUser.GetAppId(), + UpdateRequestType: &app.UpdateApplicationRequest_SamlConfigurationRequest{ + SamlConfigurationRequest: &app.UpdateSAMLApplicationConfigurationRequest{ + Metadata: &app.UpdateSAMLApplicationConfigurationRequest_MetadataXml{ + MetadataXml: samlMetasForLoginUser, + }, + LoginVersion: &app.LoginVersion{Version: &app.LoginVersion_LoginV1{LoginV1: &app.LoginV1{}}}, + }, + }, + }, + expectedErrorType: codes.PermissionDenied, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + res, err := instance.Client.AppV2Beta.UpdateApplication(tc.inputCtx, tc.updateRequest) + + require.Equal(t, tc.expectedErrorType, status.Code(err)) + if tc.expectedErrorType == codes.OK { + assert.NotZero(t, res.GetChangeDate()) + } + }) + } +} + +func TestDeleteApplication(t *testing.T) { + p := instance.CreateProject(IAMOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.Name(), false, false) + + reqForAppNameCreation := &app.CreateApplicationRequest_ApiRequest{ + ApiRequest: &app.CreateAPIApplicationRequest{AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT}, + } + + appToDelete, appNameChangeErr := instance.Client.AppV2Beta.CreateApplication(IAMOwnerCtx, &app.CreateApplicationRequest{ + ProjectId: p.GetId(), + Name: gofakeit.AppName(), + CreationRequestType: reqForAppNameCreation, + }) + require.Nil(t, appNameChangeErr) + + t.Parallel() + tt := []struct { + testName string + deleteRequest *app.DeleteApplicationRequest + inputCtx context.Context + + expectedErrorType codes.Code + }{ + { + testName: "when app to delete is not found should return not found error", + inputCtx: IAMOwnerCtx, + deleteRequest: &app.DeleteApplicationRequest{ + ProjectId: p.GetId(), + Id: gofakeit.Sentence(2), + }, + expectedErrorType: codes.NotFound, + }, + { + testName: "when app to delete is found should return deletion time", + inputCtx: IAMOwnerCtx, + deleteRequest: &app.DeleteApplicationRequest{ + ProjectId: p.GetId(), + Id: appToDelete.GetAppId(), + }, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + + // When + res, err := instance.Client.AppV2Beta.DeleteApplication(tc.inputCtx, tc.deleteRequest) + + // Then + require.Equal(t, tc.expectedErrorType, status.Code(err)) + if tc.expectedErrorType == codes.OK { + assert.NotZero(t, res.GetDeletionDate()) + } + }) + } +} + +func TestDeleteApplication_WithDifferentPermissions(t *testing.T) { + p, projectOwnerCtx := getProjectAndProjectContext(t, instance, IAMOwnerCtx) + + appToDeleteForLoginUser := createAPIApp(t, IAMOwnerCtx, instance, p.GetId()) + appToDeleteForProjectOwner := createAPIApp(t, IAMOwnerCtx, instance, p.GetId()) + appToDeleteForOrgOwner := createAPIApp(t, IAMOwnerCtx, instance, p.GetId()) + + t.Parallel() + tt := []struct { + testName string + deleteRequest *app.DeleteApplicationRequest + inputCtx context.Context + + expectedErrorType codes.Code + }{ + // Login User + { + testName: "when user has no project.app.delete permission for app delete request should return permission error", + inputCtx: LoginUserCtx, + deleteRequest: &app.DeleteApplicationRequest{ + ProjectId: p.GetId(), + Id: appToDeleteForLoginUser.GetAppId(), + }, + expectedErrorType: codes.PermissionDenied, + }, + + // Project Owner + { + testName: "when user is ProjectOwner delete app request should succeed", + inputCtx: projectOwnerCtx, + deleteRequest: &app.DeleteApplicationRequest{ + ProjectId: p.GetId(), + Id: appToDeleteForProjectOwner.GetAppId(), + }, + }, + + // Org Owner + { + testName: "when user is OrgOwner delete app request should succeed", + inputCtx: projectOwnerCtx, + deleteRequest: &app.DeleteApplicationRequest{ + ProjectId: p.GetId(), + Id: appToDeleteForOrgOwner.GetAppId(), + }, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + + // When + res, err := instance.Client.AppV2Beta.DeleteApplication(tc.inputCtx, tc.deleteRequest) + + // Then + require.Equal(t, tc.expectedErrorType, status.Code(err)) + if tc.expectedErrorType == codes.OK { + assert.NotZero(t, res.GetDeletionDate()) + } + }) + } +} + +func TestDeactivateApplication(t *testing.T) { + p := instance.CreateProject(IAMOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.Name(), false, false) + + reqForAppNameCreation := &app.CreateApplicationRequest_ApiRequest{ + ApiRequest: &app.CreateAPIApplicationRequest{AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT}, + } + + appToDeactivate, appCreateErr := instance.Client.AppV2Beta.CreateApplication(IAMOwnerCtx, &app.CreateApplicationRequest{ + ProjectId: p.GetId(), + Name: gofakeit.AppName(), + CreationRequestType: reqForAppNameCreation, + }) + require.NoError(t, appCreateErr) + + t.Parallel() + + tt := []struct { + testName string + inputCtx context.Context + deleteRequest *app.DeactivateApplicationRequest + + expectedErrorType codes.Code + }{ + { + testName: "when app to deactivate is not found should return not found error", + inputCtx: IAMOwnerCtx, + deleteRequest: &app.DeactivateApplicationRequest{ + ProjectId: p.GetId(), + Id: gofakeit.Sentence(2), + }, + expectedErrorType: codes.NotFound, + }, + { + testName: "when app to deactivate is found should return deactivation time", + inputCtx: IAMOwnerCtx, + deleteRequest: &app.DeactivateApplicationRequest{ + ProjectId: p.GetId(), + Id: appToDeactivate.GetAppId(), + }, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + + // When + res, err := instance.Client.AppV2Beta.DeactivateApplication(tc.inputCtx, tc.deleteRequest) + + // Then + require.Equal(t, tc.expectedErrorType, status.Code(err)) + if tc.expectedErrorType == codes.OK { + assert.NotZero(t, res.GetDeactivationDate()) + } + }) + } +} + +func TestDeactivateApplication_WithDifferentPermissions(t *testing.T) { + p, projectOwnerCtx := getProjectAndProjectContext(t, instance, IAMOwnerCtx) + + appToDeactivateForLoginUser := createAPIApp(t, IAMOwnerCtx, instance, p.GetId()) + appToDeactivateForPrjectOwner := createAPIApp(t, IAMOwnerCtx, instance, p.GetId()) + appToDeactivateForOrgOwner := createAPIApp(t, IAMOwnerCtx, instance, p.GetId()) + + t.Parallel() + + tt := []struct { + testName string + inputCtx context.Context + deleteRequest *app.DeactivateApplicationRequest + + expectedErrorType codes.Code + }{ + // Login User + { + testName: "when user has no project.app.write permission for app deactivate request should return permission error", + inputCtx: IAMOwnerCtx, + deleteRequest: &app.DeactivateApplicationRequest{ + ProjectId: p.GetId(), + Id: appToDeactivateForLoginUser.GetAppId(), + }, + }, + + // Project Owner + { + testName: "when user is ProjectOwner deactivate app request should succeed", + inputCtx: projectOwnerCtx, + deleteRequest: &app.DeactivateApplicationRequest{ + ProjectId: p.GetId(), + Id: appToDeactivateForPrjectOwner.GetAppId(), + }, + }, + + // Org Owner + { + testName: "when user is OrgOwner deactivate app request should succeed", + inputCtx: OrgOwnerCtx, + deleteRequest: &app.DeactivateApplicationRequest{ + ProjectId: p.GetId(), + Id: appToDeactivateForOrgOwner.GetAppId(), + }, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + + // When + res, err := instance.Client.AppV2Beta.DeactivateApplication(tc.inputCtx, tc.deleteRequest) + + // Then + require.Equal(t, tc.expectedErrorType, status.Code(err)) + if tc.expectedErrorType == codes.OK { + assert.NotZero(t, res.GetDeactivationDate()) + } + }) + } +} + +func TestReactivateApplication(t *testing.T) { + p := instance.CreateProject(IAMOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.Name(), false, false) + + reqForAppNameCreation := &app.CreateApplicationRequest_ApiRequest{ + ApiRequest: &app.CreateAPIApplicationRequest{AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT}, + } + + appToReactivate, appCreateErr := instance.Client.AppV2Beta.CreateApplication(IAMOwnerCtx, &app.CreateApplicationRequest{ + ProjectId: p.GetId(), + Name: gofakeit.AppName(), + CreationRequestType: reqForAppNameCreation, + }) + require.Nil(t, appCreateErr) + + _, appDeactivateErr := instance.Client.AppV2Beta.DeactivateApplication(IAMOwnerCtx, &app.DeactivateApplicationRequest{ + ProjectId: p.GetId(), + Id: appToReactivate.GetAppId(), + }) + require.Nil(t, appDeactivateErr) + + t.Parallel() + + tt := []struct { + testName string + inputCtx context.Context + reactivateRequest *app.ReactivateApplicationRequest + + expectedErrorType codes.Code + }{ + { + testName: "when app to reactivate is not found should return not found error", + inputCtx: IAMOwnerCtx, + reactivateRequest: &app.ReactivateApplicationRequest{ + ProjectId: p.GetId(), + Id: gofakeit.Sentence(2), + }, + expectedErrorType: codes.NotFound, + }, + { + testName: "when app to reactivate is found should return deactivation time", + inputCtx: IAMOwnerCtx, + reactivateRequest: &app.ReactivateApplicationRequest{ + ProjectId: p.GetId(), + Id: appToReactivate.GetAppId(), + }, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + + // When + res, err := instance.Client.AppV2Beta.ReactivateApplication(tc.inputCtx, tc.reactivateRequest) + + // Then + require.Equal(t, tc.expectedErrorType, status.Code(err)) + if tc.expectedErrorType == codes.OK { + assert.NotZero(t, res.GetReactivationDate()) + } + }) + } +} + +func TestReactivateApplication_WithDifferentPermissions(t *testing.T) { + p, projectOwnerCtx := getProjectAndProjectContext(t, instance, IAMOwnerCtx) + + appToReactivateForLoginUser := createAPIApp(t, IAMOwnerCtx, instance, p.GetId()) + deactivateApp(t, appToReactivateForLoginUser, p.GetId()) + + appToReactivateForProjectOwner := createAPIApp(t, IAMOwnerCtx, instance, p.GetId()) + deactivateApp(t, appToReactivateForProjectOwner, p.GetId()) + + appToReactivateForOrgOwner := createAPIApp(t, IAMOwnerCtx, instance, p.GetId()) + deactivateApp(t, appToReactivateForOrgOwner, p.GetId()) + + t.Parallel() + + tt := []struct { + testName string + inputCtx context.Context + reactivateRequest *app.ReactivateApplicationRequest + + expectedErrorType codes.Code + }{ + // Login User + { + testName: "when user has no project.app.write permission for app reactivate request should return permission error", + inputCtx: LoginUserCtx, + reactivateRequest: &app.ReactivateApplicationRequest{ + ProjectId: p.GetId(), + Id: appToReactivateForLoginUser.GetAppId(), + }, + expectedErrorType: codes.PermissionDenied, + }, + + // Project Owner + { + testName: "when user is ProjectOwner reactivate app request should succeed", + inputCtx: projectOwnerCtx, + reactivateRequest: &app.ReactivateApplicationRequest{ + ProjectId: p.GetId(), + Id: appToReactivateForProjectOwner.GetAppId(), + }, + }, + + // Org Owner + { + testName: "when user is OrgOwner reactivate app request should succeed", + inputCtx: OrgOwnerCtx, + reactivateRequest: &app.ReactivateApplicationRequest{ + ProjectId: p.GetId(), + Id: appToReactivateForOrgOwner.GetAppId(), + }, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + + // When + res, err := instance.Client.AppV2Beta.ReactivateApplication(tc.inputCtx, tc.reactivateRequest) + + // Then + require.Equal(t, tc.expectedErrorType, status.Code(err)) + if tc.expectedErrorType == codes.OK { + assert.NotZero(t, res.GetReactivationDate()) + } + }) + } +} + +func TestRegenerateClientSecret(t *testing.T) { + p := instance.CreateProject(IAMOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.Name(), false, false) + + reqForApiAppCreation := &app.CreateApplicationRequest_ApiRequest{ + ApiRequest: &app.CreateAPIApplicationRequest{AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT}, + } + + apiAppToRegen, apiAppCreateErr := instance.Client.AppV2Beta.CreateApplication(IAMOwnerCtx, &app.CreateApplicationRequest{ + ProjectId: p.GetId(), + Name: gofakeit.AppName(), + CreationRequestType: reqForApiAppCreation, + }) + require.Nil(t, apiAppCreateErr) + + reqForOIDCAppCreation := &app.CreateApplicationRequest_OidcRequest{ + OidcRequest: &app.CreateOIDCApplicationRequest{ + RedirectUris: []string{"http://example.com"}, + ResponseTypes: []app.OIDCResponseType{app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE}, + GrantTypes: []app.OIDCGrantType{app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE}, + AppType: app.OIDCAppType_OIDC_APP_TYPE_WEB, + AuthMethodType: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC, + PostLogoutRedirectUris: []string{"http://example.com/home"}, + Version: app.OIDCVersion_OIDC_VERSION_1_0, + AccessTokenType: app.OIDCTokenType_OIDC_TOKEN_TYPE_JWT, + BackChannelLogoutUri: "http://example.com/logout", + LoginVersion: &app.LoginVersion{ + Version: &app.LoginVersion_LoginV2{ + LoginV2: &app.LoginV2{ + BaseUri: &baseURI, + }, + }, + }, + }, + } + + oidcAppToRegen, oidcAppCreateErr := instance.Client.AppV2Beta.CreateApplication(IAMOwnerCtx, &app.CreateApplicationRequest{ + ProjectId: p.GetId(), + Name: gofakeit.AppName(), + CreationRequestType: reqForOIDCAppCreation, + }) + require.Nil(t, oidcAppCreateErr) + + t.Parallel() + + tt := []struct { + testName string + inputCtx context.Context + regenRequest *app.RegenerateClientSecretRequest + + expectedErrorType codes.Code + oldSecret string + }{ + { + testName: "when app to regen is not expected type should return invalid argument error", + inputCtx: IAMOwnerCtx, + regenRequest: &app.RegenerateClientSecretRequest{ + ProjectId: p.GetId(), + ApplicationId: gofakeit.Sentence(2), + }, + expectedErrorType: codes.InvalidArgument, + }, + { + testName: "when app to regen is not found should return not found error", + inputCtx: IAMOwnerCtx, + regenRequest: &app.RegenerateClientSecretRequest{ + ProjectId: p.GetId(), + ApplicationId: gofakeit.Sentence(2), + AppType: &app.RegenerateClientSecretRequest_IsApi{}, + }, + expectedErrorType: codes.NotFound, + }, + { + testName: "when API app to regen is found should return different secret", + inputCtx: IAMOwnerCtx, + regenRequest: &app.RegenerateClientSecretRequest{ + ProjectId: p.GetId(), + ApplicationId: apiAppToRegen.GetAppId(), + AppType: &app.RegenerateClientSecretRequest_IsApi{}, + }, + oldSecret: apiAppToRegen.GetApiResponse().GetClientSecret(), + }, + { + testName: "when OIDC app to regen is found should return different secret", + inputCtx: IAMOwnerCtx, + regenRequest: &app.RegenerateClientSecretRequest{ + ProjectId: p.GetId(), + ApplicationId: oidcAppToRegen.GetAppId(), + AppType: &app.RegenerateClientSecretRequest_IsOidc{}, + }, + oldSecret: oidcAppToRegen.GetOidcResponse().GetClientSecret(), + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + + // When + res, err := instance.Client.AppV2Beta.RegenerateClientSecret(tc.inputCtx, tc.regenRequest) + + // Then + require.Equal(t, tc.expectedErrorType, status.Code(err)) + if tc.expectedErrorType == codes.OK { + assert.NotZero(t, res.GetCreationDate()) + assert.NotEqual(t, tc.oldSecret, res.GetClientSecret()) + } + }) + } + +} + +func TestRegenerateClientSecret_WithDifferentPermissions(t *testing.T) { + p, projectOwnerCtx := getProjectAndProjectContext(t, instance, IAMOwnerCtx) + + apiAppToRegenForLoginUser := createAPIApp(t, IAMOwnerCtx, instance, p.GetId()) + apiAppToRegenForProjectOwner := createAPIApp(t, IAMOwnerCtx, instance, p.GetId()) + apiAppToRegenForOrgOwner := createAPIApp(t, IAMOwnerCtx, instance, p.GetId()) + + oidcAppToRegenForLoginUser := createOIDCApp(t, baseURI, p.GetId()) + oidcAppToRegenForProjectOwner := createOIDCApp(t, baseURI, p.GetId()) + oidcAppToRegenForOrgOwner := createOIDCApp(t, baseURI, p.GetId()) + + t.Parallel() + + tt := []struct { + testName string + inputCtx context.Context + regenRequest *app.RegenerateClientSecretRequest + + expectedErrorType codes.Code + oldSecret string + }{ + // Login user + { + testName: "when user has no project.app.write permission for API app secret regen request should return permission error", + inputCtx: LoginUserCtx, + regenRequest: &app.RegenerateClientSecretRequest{ + ProjectId: p.GetId(), + ApplicationId: apiAppToRegenForLoginUser.GetAppId(), + AppType: &app.RegenerateClientSecretRequest_IsApi{}, + }, + expectedErrorType: codes.PermissionDenied, + }, + { + testName: "when user has no project.app.write permission for OIDC app secret regen request should return permission error", + inputCtx: LoginUserCtx, + regenRequest: &app.RegenerateClientSecretRequest{ + ProjectId: p.GetId(), + ApplicationId: oidcAppToRegenForLoginUser.GetAppId(), + AppType: &app.RegenerateClientSecretRequest_IsOidc{}, + }, + expectedErrorType: codes.PermissionDenied, + }, + + // Project Owner + { + testName: "when user is ProjectOwner regen API app secret request should succeed", + inputCtx: projectOwnerCtx, + regenRequest: &app.RegenerateClientSecretRequest{ + ProjectId: p.GetId(), + ApplicationId: apiAppToRegenForProjectOwner.GetAppId(), + AppType: &app.RegenerateClientSecretRequest_IsApi{}, + }, + oldSecret: apiAppToRegenForProjectOwner.GetApiResponse().GetClientSecret(), + }, + { + testName: "when user is ProjectOwner regen OIDC app secret request should succeed", + inputCtx: projectOwnerCtx, + regenRequest: &app.RegenerateClientSecretRequest{ + ProjectId: p.GetId(), + ApplicationId: oidcAppToRegenForProjectOwner.GetAppId(), + AppType: &app.RegenerateClientSecretRequest_IsOidc{}, + }, + oldSecret: oidcAppToRegenForProjectOwner.GetOidcResponse().GetClientSecret(), + }, + + // Org Owner + { + testName: "when user is OrgOwner regen API app secret request should succeed", + inputCtx: OrgOwnerCtx, + regenRequest: &app.RegenerateClientSecretRequest{ + ProjectId: p.GetId(), + ApplicationId: apiAppToRegenForOrgOwner.GetAppId(), + AppType: &app.RegenerateClientSecretRequest_IsApi{}, + }, + oldSecret: apiAppToRegenForOrgOwner.GetApiResponse().GetClientSecret(), + }, + { + testName: "when user is OrgOwner regen OIDC app secret request should succeed", + inputCtx: OrgOwnerCtx, + regenRequest: &app.RegenerateClientSecretRequest{ + ProjectId: p.GetId(), + ApplicationId: oidcAppToRegenForOrgOwner.GetAppId(), + AppType: &app.RegenerateClientSecretRequest_IsOidc{}, + }, + oldSecret: oidcAppToRegenForOrgOwner.GetOidcResponse().GetClientSecret(), + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + + // When + res, err := instance.Client.AppV2Beta.RegenerateClientSecret(tc.inputCtx, tc.regenRequest) + + // Then + require.Equal(t, tc.expectedErrorType, status.Code(err)) + if tc.expectedErrorType == codes.OK { + assert.NotZero(t, res.GetCreationDate()) + assert.NotEqual(t, tc.oldSecret, res.GetClientSecret()) + } + }) + } + +} diff --git a/internal/api/grpc/app/v2beta/integration_test/query_test.go b/internal/api/grpc/app/v2beta/integration_test/query_test.go new file mode 100644 index 0000000000..4f6679da7f --- /dev/null +++ b/internal/api/grpc/app/v2beta/integration_test/query_test.go @@ -0,0 +1,820 @@ +//go:build integration + +package app_test + +import ( + "context" + "fmt" + "slices" + "testing" + "time" + + "github.com/brianvoe/gofakeit/v6" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/zitadel/zitadel/internal/integration" + app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/filter/v2" +) + +func TestGetApplication(t *testing.T) { + p, projectOwnerCtx := getProjectAndProjectContext(t, instance, IAMOwnerCtx) + + apiAppName := gofakeit.AppName() + createdApiApp, errAPIAppCreation := instance.Client.AppV2Beta.CreateApplication(IAMOwnerCtx, &app.CreateApplicationRequest{ + ProjectId: p.GetId(), + Name: apiAppName, + CreationRequestType: &app.CreateApplicationRequest_ApiRequest{ + ApiRequest: &app.CreateAPIApplicationRequest{ + AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_BASIC, + }, + }, + }) + require.Nil(t, errAPIAppCreation) + + samlAppName := gofakeit.AppName() + createdSAMLApp, errSAMLAppCreation := instance.Client.AppV2Beta.CreateApplication(IAMOwnerCtx, &app.CreateApplicationRequest{ + ProjectId: p.GetId(), + Name: samlAppName, + CreationRequestType: &app.CreateApplicationRequest_SamlRequest{ + SamlRequest: &app.CreateSAMLApplicationRequest{ + LoginVersion: &app.LoginVersion{Version: &app.LoginVersion_LoginV1{LoginV1: &app.LoginV1{}}}, + Metadata: &app.CreateSAMLApplicationRequest_MetadataXml{MetadataXml: samlMetadataGen(gofakeit.URL())}, + }, + }, + }) + require.Nil(t, errSAMLAppCreation) + + oidcAppName := gofakeit.AppName() + createdOIDCApp, errOIDCAppCreation := instance.Client.AppV2Beta.CreateApplication(IAMOwnerCtx, &app.CreateApplicationRequest{ + ProjectId: p.GetId(), + Name: oidcAppName, + CreationRequestType: &app.CreateApplicationRequest_OidcRequest{ + OidcRequest: &app.CreateOIDCApplicationRequest{ + RedirectUris: []string{"http://example.com"}, + ResponseTypes: []app.OIDCResponseType{app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE}, + GrantTypes: []app.OIDCGrantType{app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE}, + AppType: app.OIDCAppType_OIDC_APP_TYPE_WEB, + AuthMethodType: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC, + PostLogoutRedirectUris: []string{"http://example.com/home"}, + Version: app.OIDCVersion_OIDC_VERSION_1_0, + AccessTokenType: app.OIDCTokenType_OIDC_TOKEN_TYPE_JWT, + BackChannelLogoutUri: "http://example.com/logout", + LoginVersion: &app.LoginVersion{Version: &app.LoginVersion_LoginV2{LoginV2: &app.LoginV2{BaseUri: &baseURI}}}, + }, + }, + }) + require.Nil(t, errOIDCAppCreation) + + t.Parallel() + + tt := []struct { + testName string + inputRequest *app.GetApplicationRequest + inputCtx context.Context + + expectedErrorType codes.Code + expectedAppName string + expectedAppID string + expectedApplicationType string + }{ + { + testName: "when unknown app ID should return not found error", + inputCtx: IAMOwnerCtx, + inputRequest: &app.GetApplicationRequest{ + Id: gofakeit.Sentence(2), + }, + + expectedErrorType: codes.NotFound, + }, + { + testName: "when user has no permission should return membership not found error", + inputCtx: NoPermissionCtx, + inputRequest: &app.GetApplicationRequest{ + Id: createdApiApp.GetAppId(), + }, + + expectedErrorType: codes.NotFound, + }, + { + testName: "when providing API app ID should return valid API app result", + inputCtx: projectOwnerCtx, + inputRequest: &app.GetApplicationRequest{ + Id: createdApiApp.GetAppId(), + }, + + expectedAppName: apiAppName, + expectedAppID: createdApiApp.GetAppId(), + expectedApplicationType: fmt.Sprintf("%T", &app.Application_ApiConfig{}), + }, + { + testName: "when providing SAML app ID should return valid SAML app result", + inputCtx: IAMOwnerCtx, + inputRequest: &app.GetApplicationRequest{ + Id: createdSAMLApp.GetAppId(), + }, + + expectedAppName: samlAppName, + expectedAppID: createdSAMLApp.GetAppId(), + expectedApplicationType: fmt.Sprintf("%T", &app.Application_SamlConfig{}), + }, + { + testName: "when providing OIDC app ID should return valid OIDC app result", + inputCtx: IAMOwnerCtx, + inputRequest: &app.GetApplicationRequest{ + Id: createdOIDCApp.GetAppId(), + }, + + expectedAppName: oidcAppName, + expectedAppID: createdOIDCApp.GetAppId(), + expectedApplicationType: fmt.Sprintf("%T", &app.Application_OidcConfig{}), + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tc.inputCtx, 30*time.Second) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + // When + res, err := instance.Client.AppV2Beta.GetApplication(tc.inputCtx, tc.inputRequest) + + // Then + require.Equal(t, tc.expectedErrorType, status.Code(err)) + if tc.expectedErrorType == codes.OK { + + assert.Equal(t, tc.expectedAppID, res.GetApp().GetId()) + assert.Equal(t, tc.expectedAppName, res.GetApp().GetName()) + assert.NotZero(t, res.GetApp().GetCreationDate()) + assert.NotZero(t, res.GetApp().GetChangeDate()) + + appType := fmt.Sprintf("%T", res.GetApp().GetConfig()) + assert.Equal(t, tc.expectedApplicationType, appType) + } + }, retryDuration, tick) + }) + } +} + +func TestListApplications(t *testing.T) { + p, projectOwnerCtx := getProjectAndProjectContext(t, instance, IAMOwnerCtx) + + t.Parallel() + + createdApiApp, apiAppName := createAPIAppWithName(t, IAMOwnerCtx, instance, p.GetId()) + + createdDeactivatedApiApp, deactivatedApiAppName := createAPIAppWithName(t, IAMOwnerCtx, instance, p.GetId()) + deactivateApp(t, createdDeactivatedApiApp, p.GetId()) + + _, createdSAMLApp, samlAppName := createSAMLAppWithName(t, gofakeit.URL(), p.GetId()) + + createdOIDCApp, oidcAppName := createOIDCAppWithName(t, gofakeit.URL(), p.GetId()) + + type appWithName struct { + app *app.CreateApplicationResponse + name string + } + + // Sorting + appsSortedByName := []appWithName{ + {name: apiAppName, app: createdApiApp}, + {name: deactivatedApiAppName, app: createdDeactivatedApiApp}, + {name: samlAppName, app: createdSAMLApp}, + {name: oidcAppName, app: createdOIDCApp}, + } + slices.SortFunc(appsSortedByName, func(a, b appWithName) int { + if a.name < b.name { + return -1 + } + if a.name > b.name { + return 1 + } + + return 0 + }) + + appsSortedByID := []appWithName{ + {name: apiAppName, app: createdApiApp}, + {name: deactivatedApiAppName, app: createdDeactivatedApiApp}, + {name: samlAppName, app: createdSAMLApp}, + {name: oidcAppName, app: createdOIDCApp}, + } + slices.SortFunc(appsSortedByID, func(a, b appWithName) int { + if a.app.GetAppId() < b.app.GetAppId() { + return -1 + } + if a.app.GetAppId() > b.app.GetAppId() { + return 1 + } + return 0 + }) + + appsSortedByCreationDate := []appWithName{ + {name: apiAppName, app: createdApiApp}, + {name: deactivatedApiAppName, app: createdDeactivatedApiApp}, + {name: samlAppName, app: createdSAMLApp}, + {name: oidcAppName, app: createdOIDCApp}, + } + slices.SortFunc(appsSortedByCreationDate, func(a, b appWithName) int { + aCreationDate := a.app.GetCreationDate().AsTime() + bCreationDate := b.app.GetCreationDate().AsTime() + + if aCreationDate.Before(bCreationDate) { + return -1 + } + if bCreationDate.Before(aCreationDate) { + return 1 + } + + return 0 + }) + + tt := []struct { + testName string + inputRequest *app.ListApplicationsRequest + inputCtx context.Context + + expectedOrderedList []appWithName + expectedOrderedKeys func(keys []appWithName) any + actualOrderedKeys func(keys []*app.Application) any + }{ + { + testName: "when no apps found should return empty list", + inputCtx: IAMOwnerCtx, + inputRequest: &app.ListApplicationsRequest{ + ProjectId: "another-id", + }, + + expectedOrderedList: []appWithName{}, + expectedOrderedKeys: func(keys []appWithName) any { return keys }, + actualOrderedKeys: func(keys []*app.Application) any { return keys }, + }, + { + testName: "when user has no read permission should return empty set", + inputCtx: NoPermissionCtx, + inputRequest: &app.ListApplicationsRequest{ + ProjectId: p.GetId(), + }, + + expectedOrderedList: []appWithName{}, + expectedOrderedKeys: func(keys []appWithName) any { return keys }, + actualOrderedKeys: func(keys []*app.Application) any { return keys }, + }, + { + testName: "when sorting by name should return apps sorted by name in descending order", + inputCtx: IAMOwnerCtx, + inputRequest: &app.ListApplicationsRequest{ + ProjectId: p.GetId(), + SortingColumn: app.AppSorting_APP_SORT_BY_NAME, + Pagination: &filter.PaginationRequest{Asc: true}, + }, + + expectedOrderedList: appsSortedByName, + expectedOrderedKeys: func(apps []appWithName) any { + names := make([]string, len(apps)) + for i, a := range apps { + names[i] = a.name + } + + return names + }, + actualOrderedKeys: func(apps []*app.Application) any { + names := make([]string, len(apps)) + for i, a := range apps { + names[i] = a.GetName() + } + + return names + }, + }, + + { + testName: "when user is project owner should return apps sorted by name in ascending order", + inputCtx: projectOwnerCtx, + inputRequest: &app.ListApplicationsRequest{ + ProjectId: p.GetId(), + SortingColumn: app.AppSorting_APP_SORT_BY_NAME, + Pagination: &filter.PaginationRequest{Asc: true}, + }, + + expectedOrderedList: appsSortedByName, + expectedOrderedKeys: func(apps []appWithName) any { + names := make([]string, len(apps)) + for i, a := range apps { + names[i] = a.name + } + + return names + }, + actualOrderedKeys: func(apps []*app.Application) any { + names := make([]string, len(apps)) + for i, a := range apps { + names[i] = a.GetName() + } + + return names + }, + }, + + { + testName: "when sorting by id should return apps sorted by id in descending order", + inputCtx: IAMOwnerCtx, + inputRequest: &app.ListApplicationsRequest{ + ProjectId: p.GetId(), + SortingColumn: app.AppSorting_APP_SORT_BY_ID, + Pagination: &filter.PaginationRequest{Asc: true}, + }, + expectedOrderedList: appsSortedByID, + expectedOrderedKeys: func(apps []appWithName) any { + ids := make([]string, len(apps)) + for i, a := range apps { + ids[i] = a.app.GetAppId() + } + + return ids + }, + actualOrderedKeys: func(apps []*app.Application) any { + ids := make([]string, len(apps)) + for i, a := range apps { + ids[i] = a.GetId() + } + + return ids + }, + }, + { + testName: "when sorting by creation date should return apps sorted by creation date in descending order", + inputCtx: IAMOwnerCtx, + inputRequest: &app.ListApplicationsRequest{ + ProjectId: p.GetId(), + SortingColumn: app.AppSorting_APP_SORT_BY_CREATION_DATE, + Pagination: &filter.PaginationRequest{Asc: true}, + }, + expectedOrderedList: appsSortedByCreationDate, + expectedOrderedKeys: func(apps []appWithName) any { + creationDates := make([]time.Time, len(apps)) + for i, a := range apps { + creationDates[i] = a.app.GetCreationDate().AsTime() + } + + return creationDates + }, + actualOrderedKeys: func(apps []*app.Application) any { + creationDates := make([]time.Time, len(apps)) + for i, a := range apps { + creationDates[i] = a.GetCreationDate().AsTime() + } + + return creationDates + }, + }, + { + testName: "when filtering by active apps should return active apps only", + inputCtx: IAMOwnerCtx, + inputRequest: &app.ListApplicationsRequest{ + ProjectId: p.GetId(), + Pagination: &filter.PaginationRequest{Asc: true}, + Filters: []*app.ApplicationSearchFilter{ + {Filter: &app.ApplicationSearchFilter_StateFilter{StateFilter: app.AppState_APP_STATE_ACTIVE}}, + }, + }, + expectedOrderedList: slices.DeleteFunc( + slices.Clone(appsSortedByID), + func(a appWithName) bool { return a.name == deactivatedApiAppName }, + ), + expectedOrderedKeys: func(apps []appWithName) any { + creationDates := make([]time.Time, len(apps)) + for i, a := range apps { + creationDates[i] = a.app.GetCreationDate().AsTime() + } + + return creationDates + }, + actualOrderedKeys: func(apps []*app.Application) any { + creationDates := make([]time.Time, len(apps)) + for i, a := range apps { + creationDates[i] = a.GetCreationDate().AsTime() + } + + return creationDates + }, + }, + { + testName: "when filtering by app type should return apps of matching type only", + inputCtx: IAMOwnerCtx, + inputRequest: &app.ListApplicationsRequest{ + ProjectId: p.GetId(), + Pagination: &filter.PaginationRequest{Asc: true}, + Filters: []*app.ApplicationSearchFilter{ + {Filter: &app.ApplicationSearchFilter_OidcAppOnly{}}, + }, + }, + expectedOrderedList: slices.DeleteFunc( + slices.Clone(appsSortedByID), + func(a appWithName) bool { return a.name != oidcAppName }, + ), + expectedOrderedKeys: func(apps []appWithName) any { + creationDates := make([]time.Time, len(apps)) + for i, a := range apps { + creationDates[i] = a.app.GetCreationDate().AsTime() + } + + return creationDates + }, + actualOrderedKeys: func(apps []*app.Application) any { + creationDates := make([]time.Time, len(apps)) + for i, a := range apps { + creationDates[i] = a.GetCreationDate().AsTime() + } + + return creationDates + }, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tc.inputCtx, 30*time.Second) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + // When + res, err := instance.Client.AppV2Beta.ListApplications(tc.inputCtx, tc.inputRequest) + + // Then + require.Equal(ttt, codes.OK, status.Code(err)) + + if err == nil { + assert.Len(ttt, res.GetApplications(), len(tc.expectedOrderedList)) + actualOrderedKeys := tc.actualOrderedKeys(res.GetApplications()) + expectedOrderedKeys := tc.expectedOrderedKeys(tc.expectedOrderedList) + assert.ElementsMatch(ttt, expectedOrderedKeys, actualOrderedKeys) + } + }, retryDuration, tick) + }) + } +} + +func TestListApplications_WithPermissionV2(t *testing.T) { + ensureFeaturePermissionV2Enabled(t, instancePermissionV2) + iamOwnerCtx := instancePermissionV2.WithAuthorization(context.Background(), integration.UserTypeIAMOwner) + p, projectOwnerCtx := getProjectAndProjectContext(t, instancePermissionV2, iamOwnerCtx) + _, otherProjectOwnerCtx := getProjectAndProjectContext(t, instancePermissionV2, iamOwnerCtx) + + appName1, appName2, appName3 := gofakeit.AppName(), gofakeit.AppName(), gofakeit.AppName() + reqForAPIAppCreation := &app.CreateApplicationRequest_ApiRequest{ + ApiRequest: &app.CreateAPIApplicationRequest{AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT}, + } + + app1, appAPIConfigChangeErr := instancePermissionV2.Client.AppV2Beta.CreateApplication(iamOwnerCtx, &app.CreateApplicationRequest{ + ProjectId: p.GetId(), + Name: appName1, + CreationRequestType: reqForAPIAppCreation, + }) + require.Nil(t, appAPIConfigChangeErr) + + app2, appAPIConfigChangeErr := instancePermissionV2.Client.AppV2Beta.CreateApplication(iamOwnerCtx, &app.CreateApplicationRequest{ + ProjectId: p.GetId(), + Name: appName2, + CreationRequestType: reqForAPIAppCreation, + }) + require.Nil(t, appAPIConfigChangeErr) + + app3, appAPIConfigChangeErr := instancePermissionV2.Client.AppV2Beta.CreateApplication(iamOwnerCtx, &app.CreateApplicationRequest{ + ProjectId: p.GetId(), + Name: appName3, + CreationRequestType: reqForAPIAppCreation, + }) + require.Nil(t, appAPIConfigChangeErr) + + t.Parallel() + + tt := []struct { + testName string + inputRequest *app.ListApplicationsRequest + inputCtx context.Context + + expectedCode codes.Code + expectedAppIDs []string + }{ + { + testName: "when user has no read permission should return empty set", + inputCtx: instancePermissionV2.WithAuthorization(context.Background(), integration.UserTypeNoPermission), + inputRequest: &app.ListApplicationsRequest{ + ProjectId: p.GetId(), + }, + + expectedAppIDs: []string{}, + }, + { + testName: "when projectOwner should return full app list", + inputCtx: projectOwnerCtx, + inputRequest: &app.ListApplicationsRequest{ + ProjectId: p.GetId(), + }, + + expectedCode: codes.OK, + expectedAppIDs: []string{app1.GetAppId(), app2.GetAppId(), app3.GetAppId()}, + }, + { + testName: "when orgOwner should return full app list", + inputCtx: instancePermissionV2.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), + inputRequest: &app.ListApplicationsRequest{ + ProjectId: p.GetId(), + }, + + expectedAppIDs: []string{app1.GetAppId(), app2.GetAppId(), app3.GetAppId()}, + }, + { + testName: "when iamOwner user should return full app list", + inputCtx: iamOwnerCtx, + inputRequest: &app.ListApplicationsRequest{ + ProjectId: p.GetId(), + }, + + expectedAppIDs: []string{app1.GetAppId(), app2.GetAppId(), app3.GetAppId()}, + }, + { + testName: "when other projectOwner user should return empty list", + inputCtx: otherProjectOwnerCtx, + inputRequest: &app.ListApplicationsRequest{ + ProjectId: p.GetId(), + }, + + expectedAppIDs: []string{}, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tc.inputCtx, 5*time.Second) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + // When + res, err := instancePermissionV2.Client.AppV2Beta.ListApplications(tc.inputCtx, tc.inputRequest) + + // Then + require.Equal(ttt, tc.expectedCode, status.Code(err)) + + if err == nil { + require.Len(ttt, res.GetApplications(), len(tc.expectedAppIDs)) + + resAppIDs := []string{} + for _, a := range res.GetApplications() { + resAppIDs = append(resAppIDs, a.GetId()) + } + + assert.ElementsMatch(ttt, tc.expectedAppIDs, resAppIDs) + } + }, retryDuration, tick) + }) + } +} + +func TestGetApplicationKey(t *testing.T) { + p, projectOwnerCtx := getProjectAndProjectContext(t, instance, IAMOwnerCtx) + createdApiApp := createAPIApp(t, IAMOwnerCtx, instance, p.GetId()) + createdAppKey := createAppKey(t, IAMOwnerCtx, instance, p.GetId(), createdApiApp.GetAppId(), time.Now().AddDate(0, 0, 1)) + + t.Parallel() + + tt := []struct { + testName string + inputRequest *app.GetApplicationKeyRequest + inputCtx context.Context + + expectedErrorType codes.Code + expectedAppKeyID string + }{ + { + testName: "when unknown app ID should return not found error", + inputCtx: IAMOwnerCtx, + inputRequest: &app.GetApplicationKeyRequest{ + Id: gofakeit.Sentence(2), + }, + + expectedErrorType: codes.NotFound, + }, + { + testName: "when user has no permission should return membership not found error", + inputCtx: NoPermissionCtx, + inputRequest: &app.GetApplicationKeyRequest{ + Id: createdAppKey.GetId(), + }, + + expectedErrorType: codes.NotFound, + }, + { + testName: "when providing API app ID should return valid API app result", + inputCtx: projectOwnerCtx, + inputRequest: &app.GetApplicationKeyRequest{ + Id: createdAppKey.GetId(), + }, + + expectedAppKeyID: createdAppKey.GetId(), + }, + { + testName: "when user is OrgOwner should return request key", + inputCtx: OrgOwnerCtx, + inputRequest: &app.GetApplicationKeyRequest{ + Id: createdAppKey.GetId(), + ProjectId: p.GetId(), + }, + + expectedAppKeyID: createdAppKey.GetId(), + }, + { + testName: "when user is IAMOwner should return request key", + inputCtx: OrgOwnerCtx, + inputRequest: &app.GetApplicationKeyRequest{ + Id: createdAppKey.GetId(), + OrganizationId: instance.DefaultOrg.GetId(), + }, + + expectedAppKeyID: createdAppKey.GetId(), + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tc.inputCtx, 30*time.Second) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + // When + res, err := instance.Client.AppV2Beta.GetApplicationKey(tc.inputCtx, tc.inputRequest) + + // Then + require.Equal(t, tc.expectedErrorType, status.Code(err)) + if tc.expectedErrorType == codes.OK { + + assert.Equal(t, tc.expectedAppKeyID, res.GetId()) + assert.NotEmpty(t, res.GetCreationDate()) + assert.NotEmpty(t, res.GetExpirationDate()) + } + }, retryDuration, tick) + }) + } +} + +func TestListApplicationKeys(t *testing.T) { + p, projectOwnerCtx := getProjectAndProjectContext(t, instance, IAMOwnerCtx) + + createdApiApp1 := createAPIApp(t, IAMOwnerCtx, instance, p.GetId()) + createdApiApp2 := createAPIApp(t, IAMOwnerCtx, instance, p.GetId()) + + tomorrow := time.Now().AddDate(0, 0, 1) + in2Days := tomorrow.AddDate(0, 0, 1) + in3Days := in2Days.AddDate(0, 0, 1) + + appKey1 := createAppKey(t, IAMOwnerCtx, instance, p.GetId(), createdApiApp1.GetAppId(), in2Days) + appKey2 := createAppKey(t, IAMOwnerCtx, instance, p.GetId(), createdApiApp1.GetAppId(), in3Days) + appKey3 := createAppKey(t, IAMOwnerCtx, instance, p.GetId(), createdApiApp1.GetAppId(), tomorrow) + appKey4 := createAppKey(t, IAMOwnerCtx, instance, p.GetId(), createdApiApp2.GetAppId(), tomorrow) + + t.Parallel() + + tt := []struct { + testName string + inputRequest *app.ListApplicationKeysRequest + deps func() (projectID, applicationID, organizationID string) + inputCtx context.Context + + expectedErrorType codes.Code + expectedAppKeysIDs []string + }{ + { + testName: "when sorting by expiration date should return keys sorted by expiration date ascending", + inputCtx: LoginUserCtx, + inputRequest: &app.ListApplicationKeysRequest{ + ResourceId: &app.ListApplicationKeysRequest_ProjectId{ProjectId: p.GetId()}, + Pagination: &filter.PaginationRequest{Asc: true}, + SortingColumn: app.ApplicationKeysSorting_APPLICATION_KEYS_SORT_BY_EXPIRATION, + }, + expectedAppKeysIDs: []string{appKey3.GetId(), appKey4.GetId(), appKey1.GetId(), appKey2.GetId()}, + }, + { + testName: "when sorting by creation date should return keys sorted by creation date descending", + inputCtx: IAMOwnerCtx, + inputRequest: &app.ListApplicationKeysRequest{ + ResourceId: &app.ListApplicationKeysRequest_ProjectId{ProjectId: p.GetId()}, + SortingColumn: app.ApplicationKeysSorting_APPLICATION_KEYS_SORT_BY_CREATION_DATE, + }, + expectedAppKeysIDs: []string{appKey4.GetId(), appKey3.GetId(), appKey2.GetId(), appKey1.GetId()}, + }, + { + testName: "when filtering by app ID should return keys matching app ID sorted by ID", + inputCtx: projectOwnerCtx, + inputRequest: &app.ListApplicationKeysRequest{ + Pagination: &filter.PaginationRequest{Asc: true}, + ResourceId: &app.ListApplicationKeysRequest_ApplicationId{ApplicationId: createdApiApp1.GetAppId()}, + }, + expectedAppKeysIDs: []string{appKey1.GetId(), appKey2.GetId(), appKey3.GetId()}, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tc.inputCtx, 5*time.Second) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + // When + res, err := instance.Client.AppV2Beta.ListApplicationKeys(tc.inputCtx, tc.inputRequest) + + // Then + require.Equal(ttt, tc.expectedErrorType, status.Code(err)) + if tc.expectedErrorType == codes.OK { + require.Len(ttt, res.GetKeys(), len(tc.expectedAppKeysIDs)) + + for i, k := range res.GetKeys() { + assert.Equal(ttt, tc.expectedAppKeysIDs[i], k.GetId()) + } + } + }, retryDuration, tick) + }) + } +} + +func TestListApplicationKeys_WithPermissionV2(t *testing.T) { + ensureFeaturePermissionV2Enabled(t, instancePermissionV2) + iamOwnerCtx := instancePermissionV2.WithAuthorization(context.Background(), integration.UserTypeIAMOwner) + loginUserCtx := instancePermissionV2.WithAuthorization(context.Background(), integration.UserTypeLogin) + p, projectOwnerCtx := getProjectAndProjectContext(t, instancePermissionV2, iamOwnerCtx) + + createdApiApp1 := createAPIApp(t, iamOwnerCtx, instancePermissionV2, p.GetId()) + createdApiApp2 := createAPIApp(t, iamOwnerCtx, instancePermissionV2, p.GetId()) + + tomorrow := time.Now().AddDate(0, 0, 1) + in2Days := tomorrow.AddDate(0, 0, 1) + in3Days := in2Days.AddDate(0, 0, 1) + + appKey1 := createAppKey(t, iamOwnerCtx, instancePermissionV2, p.GetId(), createdApiApp1.GetAppId(), in2Days) + appKey2 := createAppKey(t, iamOwnerCtx, instancePermissionV2, p.GetId(), createdApiApp1.GetAppId(), in3Days) + appKey3 := createAppKey(t, iamOwnerCtx, instancePermissionV2, p.GetId(), createdApiApp1.GetAppId(), tomorrow) + appKey4 := createAppKey(t, iamOwnerCtx, instancePermissionV2, p.GetId(), createdApiApp2.GetAppId(), tomorrow) + + t.Parallel() + + tt := []struct { + testName string + inputRequest *app.ListApplicationKeysRequest + deps func() (projectID, applicationID, organizationID string) + inputCtx context.Context + + expectedErrorType codes.Code + expectedAppKeysIDs []string + }{ + { + testName: "when sorting by expiration date should return keys sorted by expiration date ascending", + inputCtx: loginUserCtx, + inputRequest: &app.ListApplicationKeysRequest{ + Pagination: &filter.PaginationRequest{Asc: true}, + SortingColumn: app.ApplicationKeysSorting_APPLICATION_KEYS_SORT_BY_EXPIRATION, + }, + expectedAppKeysIDs: []string{appKey3.GetId(), appKey4.GetId(), appKey1.GetId(), appKey2.GetId()}, + }, + { + testName: "when sorting by creation date should return keys sorted by creation date descending", + inputCtx: iamOwnerCtx, + inputRequest: &app.ListApplicationKeysRequest{ + SortingColumn: app.ApplicationKeysSorting_APPLICATION_KEYS_SORT_BY_CREATION_DATE, + }, + expectedAppKeysIDs: []string{appKey4.GetId(), appKey3.GetId(), appKey2.GetId(), appKey1.GetId()}, + }, + { + testName: "when filtering by app ID should return keys matching app ID sorted by ID", + inputCtx: projectOwnerCtx, + inputRequest: &app.ListApplicationKeysRequest{ + Pagination: &filter.PaginationRequest{Asc: true}, + ResourceId: &app.ListApplicationKeysRequest_ApplicationId{ApplicationId: createdApiApp1.GetAppId()}, + }, + expectedAppKeysIDs: []string{appKey1.GetId(), appKey2.GetId(), appKey3.GetId()}, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + // t.Parallel() + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tc.inputCtx, 5*time.Second) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + // When + res, err := instancePermissionV2.Client.AppV2Beta.ListApplicationKeys(tc.inputCtx, tc.inputRequest) + + // Then + require.Equal(ttt, tc.expectedErrorType, status.Code(err)) + if tc.expectedErrorType == codes.OK { + require.Len(ttt, res.GetKeys(), len(tc.expectedAppKeysIDs)) + + for i, k := range res.GetKeys() { + assert.Equal(ttt, tc.expectedAppKeysIDs[i], k.GetId()) + } + } + }, retryDuration, tick) + }) + } +} diff --git a/internal/api/grpc/app/v2beta/integration_test/server_test.go b/internal/api/grpc/app/v2beta/integration_test/server_test.go new file mode 100644 index 0000000000..8ba012c18b --- /dev/null +++ b/internal/api/grpc/app/v2beta/integration_test/server_test.go @@ -0,0 +1,220 @@ +//go:build integration + +package app_test + +import ( + "context" + "fmt" + "os" + "testing" + "time" + + "github.com/brianvoe/gofakeit/v6" + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/integration" + app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/feature/v2" + project_v2beta "github.com/zitadel/zitadel/pkg/grpc/project/v2beta" +) + +var ( + NoPermissionCtx context.Context + LoginUserCtx context.Context + OrgOwnerCtx context.Context + IAMOwnerCtx context.Context + + instance *integration.Instance + instancePermissionV2 *integration.Instance + + baseURI = "http://example.com" +) + +func TestMain(m *testing.M) { + os.Exit(func() int { + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) + defer cancel() + + instance = integration.NewInstance(ctx) + instancePermissionV2 = integration.NewInstance(ctx) + + IAMOwnerCtx = instance.WithAuthorization(ctx, integration.UserTypeIAMOwner) + + LoginUserCtx = instance.WithAuthorization(ctx, integration.UserTypeLogin) + OrgOwnerCtx = instance.WithAuthorization(ctx, integration.UserTypeOrgOwner) + NoPermissionCtx = instance.WithAuthorization(ctx, integration.UserTypeNoPermission) + + return m.Run() + }()) +} + +func getProjectAndProjectContext(t *testing.T, inst *integration.Instance, ctx context.Context) (*project_v2beta.CreateProjectResponse, context.Context) { + project := inst.CreateProject(ctx, t, inst.DefaultOrg.GetId(), gofakeit.Name(), false, false) + userResp := inst.CreateMachineUser(ctx) + patResp := inst.CreatePersonalAccessToken(ctx, userResp.GetUserId()) + inst.CreateProjectMembership(t, ctx, project.GetId(), userResp.GetUserId()) + projectOwnerCtx := integration.WithAuthorizationToken(context.Background(), patResp.Token) + + return project, projectOwnerCtx +} + +func samlMetadataGen(entityID string) []byte { + str := fmt.Sprintf(` + + + urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified + + + + +`, + entityID) + + return []byte(str) +} + +func createSAMLAppWithName(t *testing.T, baseURI, projectID string) ([]byte, *app.CreateApplicationResponse, string) { + samlMetas := samlMetadataGen(gofakeit.URL()) + appName := gofakeit.AppName() + + appForSAMLConfigChange, appSAMLConfigChangeErr := instance.Client.AppV2Beta.CreateApplication(IAMOwnerCtx, &app.CreateApplicationRequest{ + ProjectId: projectID, + Name: appName, + CreationRequestType: &app.CreateApplicationRequest_SamlRequest{ + SamlRequest: &app.CreateSAMLApplicationRequest{ + Metadata: &app.CreateSAMLApplicationRequest_MetadataXml{ + MetadataXml: samlMetas, + }, + LoginVersion: &app.LoginVersion{ + Version: &app.LoginVersion_LoginV2{ + LoginV2: &app.LoginV2{ + BaseUri: &baseURI, + }, + }, + }, + }, + }, + }) + require.Nil(t, appSAMLConfigChangeErr) + + return samlMetas, appForSAMLConfigChange, appName +} + +func createSAMLApp(t *testing.T, baseURI, projectID string) ([]byte, *app.CreateApplicationResponse) { + metas, app, _ := createSAMLAppWithName(t, baseURI, projectID) + return metas, app +} + +func createOIDCAppWithName(t *testing.T, baseURI, projectID string) (*app.CreateApplicationResponse, string) { + appName := gofakeit.AppName() + + appForOIDCConfigChange, appOIDCConfigChangeErr := instance.Client.AppV2Beta.CreateApplication(IAMOwnerCtx, &app.CreateApplicationRequest{ + ProjectId: projectID, + Name: appName, + CreationRequestType: &app.CreateApplicationRequest_OidcRequest{ + OidcRequest: &app.CreateOIDCApplicationRequest{ + RedirectUris: []string{"http://example.com"}, + ResponseTypes: []app.OIDCResponseType{app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE}, + GrantTypes: []app.OIDCGrantType{app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE}, + AppType: app.OIDCAppType_OIDC_APP_TYPE_WEB, + AuthMethodType: app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC, + PostLogoutRedirectUris: []string{"http://example.com/home"}, + Version: app.OIDCVersion_OIDC_VERSION_1_0, + AccessTokenType: app.OIDCTokenType_OIDC_TOKEN_TYPE_JWT, + BackChannelLogoutUri: "http://example.com/logout", + LoginVersion: &app.LoginVersion{ + Version: &app.LoginVersion_LoginV2{ + LoginV2: &app.LoginV2{ + BaseUri: &baseURI, + }, + }, + }, + }, + }, + }) + require.Nil(t, appOIDCConfigChangeErr) + + return appForOIDCConfigChange, appName +} + +func createOIDCApp(t *testing.T, baseURI, projctID string) *app.CreateApplicationResponse { + app, _ := createOIDCAppWithName(t, baseURI, projctID) + + return app +} + +func createAPIAppWithName(t *testing.T, ctx context.Context, inst *integration.Instance, projectID string) (*app.CreateApplicationResponse, string) { + appName := gofakeit.AppName() + + reqForAPIAppCreation := &app.CreateApplicationRequest_ApiRequest{ + ApiRequest: &app.CreateAPIApplicationRequest{AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT}, + } + + appForAPIConfigChange, appAPIConfigChangeErr := inst.Client.AppV2Beta.CreateApplication(ctx, &app.CreateApplicationRequest{ + ProjectId: projectID, + Name: appName, + CreationRequestType: reqForAPIAppCreation, + }) + require.Nil(t, appAPIConfigChangeErr) + + return appForAPIConfigChange, appName +} + +func createAPIApp(t *testing.T, ctx context.Context, inst *integration.Instance, projectID string) *app.CreateApplicationResponse { + res, _ := createAPIAppWithName(t, ctx, inst, projectID) + return res +} + +func deactivateApp(t *testing.T, appToDeactivate *app.CreateApplicationResponse, projectID string) { + _, appDeactivateErr := instance.Client.AppV2Beta.DeactivateApplication(IAMOwnerCtx, &app.DeactivateApplicationRequest{ + ProjectId: projectID, + Id: appToDeactivate.GetAppId(), + }) + require.Nil(t, appDeactivateErr) +} + +func ensureFeaturePermissionV2Enabled(t *testing.T, instance *integration.Instance) { + ctx := instance.WithAuthorization(context.Background(), integration.UserTypeIAMOwner) + f, err := instance.Client.FeatureV2.GetInstanceFeatures(ctx, &feature.GetInstanceFeaturesRequest{ + Inheritance: true, + }) + require.NoError(t, err) + + if f.PermissionCheckV2.GetEnabled() { + return + } + + _, err = instance.Client.FeatureV2.SetInstanceFeatures(ctx, &feature.SetInstanceFeaturesRequest{ + PermissionCheckV2: gu.Ptr(true), + }) + require.NoError(t, err) + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, 5*time.Minute) + require.EventuallyWithT(t, func(tt *assert.CollectT) { + f, err := instance.Client.FeatureV2.GetInstanceFeatures(ctx, &feature.GetInstanceFeaturesRequest{Inheritance: true}) + require.NoError(tt, err) + assert.True(tt, f.PermissionCheckV2.GetEnabled()) + }, retryDuration, tick, "timed out waiting for ensuring instance feature") +} + +func createAppKey(t *testing.T, ctx context.Context, inst *integration.Instance, projectID, appID string, expirationDate time.Time) *app.CreateApplicationKeyResponse { + res, err := inst.Client.AppV2Beta.CreateApplicationKey(ctx, + &app.CreateApplicationKeyRequest{ + AppId: appID, + ProjectId: projectID, + ExpirationDate: timestamppb.New(expirationDate.UTC()), + }, + ) + + require.Nil(t, err) + + return res +} diff --git a/internal/api/grpc/app/v2beta/query.go b/internal/api/grpc/app/v2beta/query.go new file mode 100644 index 0000000000..ab2a98d14a --- /dev/null +++ b/internal/api/grpc/app/v2beta/query.go @@ -0,0 +1,77 @@ +package app + +import ( + "context" + "strings" + + "connectrpc.com/connect" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/api/grpc/app/v2beta/convert" + filter "github.com/zitadel/zitadel/internal/api/grpc/filter/v2" + "github.com/zitadel/zitadel/internal/query" + app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta" +) + +func (s *Server) GetApplication(ctx context.Context, req *connect.Request[app.GetApplicationRequest]) (*connect.Response[app.GetApplicationResponse], error) { + res, err := s.query.AppByIDWithPermission(ctx, req.Msg.GetId(), false, s.checkPermission) + if err != nil { + return nil, err + } + + return connect.NewResponse(&app.GetApplicationResponse{ + App: convert.AppToPb(res), + }), nil +} + +func (s *Server) ListApplications(ctx context.Context, req *connect.Request[app.ListApplicationsRequest]) (*connect.Response[app.ListApplicationsResponse], error) { + queries, err := convert.ListApplicationsRequestToModel(s.systemDefaults, req.Msg) + if err != nil { + return nil, err + } + + res, err := s.query.SearchApps(ctx, queries, s.checkPermission) + if err != nil { + return nil, err + } + + return connect.NewResponse(&app.ListApplicationsResponse{ + Applications: convert.AppsToPb(res.Apps), + Pagination: filter.QueryToPaginationPb(queries.SearchRequest, res.SearchResponse), + }), nil +} + +func (s *Server) GetApplicationKey(ctx context.Context, req *connect.Request[app.GetApplicationKeyRequest]) (*connect.Response[app.GetApplicationKeyResponse], error) { + queries, err := convert.GetApplicationKeyQueriesRequestToDomain(req.Msg.GetOrganizationId(), req.Msg.GetProjectId(), req.Msg.GetApplicationId()) + if err != nil { + return nil, err + } + + key, err := s.query.GetAuthNKeyByIDWithPermission(ctx, true, strings.TrimSpace(req.Msg.GetId()), s.checkPermission, queries...) + if err != nil { + return nil, err + } + + return connect.NewResponse(&app.GetApplicationKeyResponse{ + Id: key.ID, + CreationDate: timestamppb.New(key.CreationDate), + ExpirationDate: timestamppb.New(key.Expiration), + }), nil +} + +func (s *Server) ListApplicationKeys(ctx context.Context, req *connect.Request[app.ListApplicationKeysRequest]) (*connect.Response[app.ListApplicationKeysResponse], error) { + queries, err := convert.ListApplicationKeysRequestToDomain(s.systemDefaults, req.Msg) + if err != nil { + return nil, err + } + + res, err := s.query.SearchAuthNKeys(ctx, queries, query.JoinFilterUnspecified, s.checkPermission) + if err != nil { + return nil, err + } + + return connect.NewResponse(&app.ListApplicationKeysResponse{ + Keys: convert.ApplicationKeysToPb(res.AuthNKeys), + Pagination: filter.QueryToPaginationPb(queries.SearchRequest, res.SearchResponse), + }), nil +} diff --git a/internal/api/grpc/app/v2beta/server.go b/internal/api/grpc/app/v2beta/server.go new file mode 100644 index 0000000000..54842070cb --- /dev/null +++ b/internal/api/grpc/app/v2beta/server.go @@ -0,0 +1,59 @@ +package app + +import ( + "net/http" + + "connectrpc.com/connect" + "google.golang.org/protobuf/reflect/protoreflect" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/config/systemdefaults" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/query" + app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/app/v2beta/appconnect" +) + +var _ appconnect.AppServiceHandler = (*Server)(nil) + +type Server struct { + command *command.Commands + query *query.Queries + systemDefaults systemdefaults.SystemDefaults + checkPermission domain.PermissionCheck +} + +type Config struct{} + +func CreateServer( + command *command.Commands, + query *query.Queries, + checkPermission domain.PermissionCheck, +) *Server { + return &Server{ + command: command, + query: query, + checkPermission: checkPermission, + } +} + +func (s *Server) RegisterConnectServer(interceptors ...connect.Interceptor) (string, http.Handler) { + return appconnect.NewAppServiceHandler(s, connect.WithInterceptors(interceptors...)) +} + +func (s *Server) FileDescriptor() protoreflect.FileDescriptor { + return app.File_zitadel_app_v2beta_app_service_proto +} + +func (s *Server) AppName() string { + return app.AppService_ServiceDesc.ServiceName +} + +func (s *Server) MethodPrefix() string { + return app.AppService_ServiceDesc.ServiceName +} + +func (s *Server) AuthMethods() authz.MethodMapping { + return app.AppService_AuthMethods +} diff --git a/internal/api/grpc/auth/user.go b/internal/api/grpc/auth/user.go index 13f955fd81..4015b0d370 100644 --- a/internal/api/grpc/auth/user.go +++ b/internal/api/grpc/auth/user.go @@ -32,7 +32,7 @@ func (s *Server) RemoveMyUser(ctx context.Context, _ *auth_pb.RemoveMyUserReques return nil, err } queries := &query.UserGrantsQueries{Queries: []query.SearchQuery{userGrantUserID}} - grants, err := s.query.UserGrants(ctx, queries, true) + grants, err := s.query.UserGrants(ctx, queries, true, nil) if err != nil { return nil, err } @@ -97,7 +97,7 @@ func (s *Server) ListMyMetadata(ctx context.Context, req *auth_pb.ListMyMetadata if err != nil { return nil, err } - res, err := s.query.SearchUserMetadata(ctx, true, authz.GetCtxData(ctx).UserID, queries, false) + res, err := s.query.SearchUserMetadata(ctx, true, authz.GetCtxData(ctx).UserID, queries, nil) if err != nil { return nil, err } @@ -151,7 +151,7 @@ func (s *Server) ListMyUserGrants(ctx context.Context, req *auth_pb.ListMyUserGr if err != nil { return nil, err } - res, err := s.query.UserGrants(ctx, queries, false) + res, err := s.query.UserGrants(ctx, queries, false, nil) if err != nil { return nil, err } @@ -180,7 +180,7 @@ func (s *Server) ListMyProjectOrgs(ctx context.Context, req *auth_pb.ListMyProje return nil, err } - grants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{Queries: []query.SearchQuery{userGrantProjectID, userGrantUserID}}, false) + grants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{Queries: []query.SearchQuery{userGrantProjectID, userGrantUserID}}, false, nil) if err != nil { return nil, err } diff --git a/internal/api/grpc/authorization/v2beta/authorization.go b/internal/api/grpc/authorization/v2beta/authorization.go new file mode 100644 index 0000000000..f5410c959a --- /dev/null +++ b/internal/api/grpc/authorization/v2beta/authorization.go @@ -0,0 +1,76 @@ +package authorization + +import ( + "context" + + "connectrpc.com/connect" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore/v1/models" + authorization "github.com/zitadel/zitadel/pkg/grpc/authorization/v2beta" +) + +func (s *Server) CreateAuthorization(ctx context.Context, req *connect.Request[authorization.CreateAuthorizationRequest]) (*connect.Response[authorization.CreateAuthorizationResponse], error) { + grant := &domain.UserGrant{ + UserID: req.Msg.UserId, + ProjectID: req.Msg.ProjectId, + RoleKeys: req.Msg.RoleKeys, + ObjectRoot: models.ObjectRoot{ + ResourceOwner: req.Msg.GetOrganizationId(), + }, + } + grant, err := s.command.AddUserGrant(ctx, grant, s.command.NewPermissionCheckUserGrantWrite(ctx)) + if err != nil { + return nil, err + } + return connect.NewResponse(&authorization.CreateAuthorizationResponse{ + Id: grant.AggregateID, + CreationDate: timestamppb.New(grant.ChangeDate), + }), nil +} + +func (s *Server) UpdateAuthorization(ctx context.Context, request *connect.Request[authorization.UpdateAuthorizationRequest]) (*connect.Response[authorization.UpdateAuthorizationResponse], error) { + userGrant, err := s.command.ChangeUserGrant(ctx, &domain.UserGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: request.Msg.Id, + }, + RoleKeys: request.Msg.RoleKeys, + }, true, true, s.command.NewPermissionCheckUserGrantWrite(ctx)) + if err != nil { + return nil, err + } + return connect.NewResponse(&authorization.UpdateAuthorizationResponse{ + ChangeDate: timestamppb.New(userGrant.ChangeDate), + }), nil +} + +func (s *Server) DeleteAuthorization(ctx context.Context, request *connect.Request[authorization.DeleteAuthorizationRequest]) (*connect.Response[authorization.DeleteAuthorizationResponse], error) { + details, err := s.command.RemoveUserGrant(ctx, request.Msg.Id, "", true, s.command.NewPermissionCheckUserGrantDelete(ctx)) + if err != nil { + return nil, err + } + return connect.NewResponse(&authorization.DeleteAuthorizationResponse{ + DeletionDate: timestamppb.New(details.EventDate), + }), nil +} + +func (s *Server) ActivateAuthorization(ctx context.Context, request *connect.Request[authorization.ActivateAuthorizationRequest]) (*connect.Response[authorization.ActivateAuthorizationResponse], error) { + details, err := s.command.ReactivateUserGrant(ctx, request.Msg.Id, "", s.command.NewPermissionCheckUserGrantWrite(ctx)) + if err != nil { + return nil, err + } + return connect.NewResponse(&authorization.ActivateAuthorizationResponse{ + ChangeDate: timestamppb.New(details.EventDate), + }), nil +} + +func (s *Server) DeactivateAuthorization(ctx context.Context, request *connect.Request[authorization.DeactivateAuthorizationRequest]) (*connect.Response[authorization.DeactivateAuthorizationResponse], error) { + details, err := s.command.DeactivateUserGrant(ctx, request.Msg.Id, "", s.command.NewPermissionCheckUserGrantWrite(ctx)) + if err != nil { + return nil, err + } + return connect.NewResponse(&authorization.DeactivateAuthorizationResponse{ + ChangeDate: timestamppb.New(details.EventDate), + }), nil +} diff --git a/internal/api/grpc/authorization/v2beta/integration_test/authorization_test.go b/internal/api/grpc/authorization/v2beta/integration_test/authorization_test.go new file mode 100644 index 0000000000..d24844f2a2 --- /dev/null +++ b/internal/api/grpc/authorization/v2beta/integration_test/authorization_test.go @@ -0,0 +1,1023 @@ +//go:build integration + +package authorization_test + +import ( + "context" + "testing" + "time" + + "github.com/brianvoe/gofakeit/v6" + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/integration" + authorization "github.com/zitadel/zitadel/pkg/grpc/authorization/v2beta" + project "github.com/zitadel/zitadel/pkg/grpc/project/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +func TestServer_CreateAuthorization(t *testing.T) { + type args struct { + prepare func(*testing.T, *authorization.CreateAuthorizationRequest) context.Context + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "add authorization, project owned, PROJECT_OWNER, ok", + args: args{ + func(t *testing.T, request *authorization.CreateAuthorizationRequest) context.Context { + selfOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + request.OrganizationId = &selfOrgId + request.ProjectId = Instance.CreateProject(IAMCTX, t, selfOrgId, gofakeit.AppName(), false, false).Id + request.RoleKeys = []string{gofakeit.AppName()} + Instance.AddProjectRole(IAMCTX, t, request.ProjectId, request.RoleKeys[0], gofakeit.AppName(), "") + request.UserId = Instance.Users.Get(integration.UserTypeIAMOwner).ID + callingUser := Instance.CreateUserTypeMachine(IAMCTX, selfOrgId) + Instance.CreateProjectMembership(t, IAMCTX, request.ProjectId, callingUser.Id) + token, err := Instance.Client.UserV2.AddPersonalAccessToken(IAMCTX, &user.AddPersonalAccessTokenRequest{UserId: callingUser.Id, ExpirationDate: timestamppb.New(time.Now().Add(24 * time.Hour))}) + require.NoError(t, err) + return integration.WithAuthorizationToken(EmptyCTX, token.Token) + }, + }, + }, + { + name: "add authorization, project owned, PROJECT_OWNER, no org id, ok", + args: args{ + func(t *testing.T, request *authorization.CreateAuthorizationRequest) context.Context { + selfOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + request.ProjectId = Instance.CreateProject(IAMCTX, t, selfOrgId, gofakeit.AppName(), false, false).Id + request.RoleKeys = []string{gofakeit.AppName()} + Instance.AddProjectRole(IAMCTX, t, request.ProjectId, request.RoleKeys[0], gofakeit.AppName(), "") + request.UserId = Instance.Users.Get(integration.UserTypeIAMOwner).ID + callingUser := Instance.CreateUserTypeMachine(IAMCTX, selfOrgId) + Instance.CreateProjectMembership(t, IAMCTX, request.ProjectId, callingUser.Id) + token, err := Instance.Client.UserV2.AddPersonalAccessToken(IAMCTX, &user.AddPersonalAccessTokenRequest{UserId: callingUser.Id, ExpirationDate: timestamppb.New(time.Now().Add(24 * time.Hour))}) + require.NoError(t, err) + return integration.WithAuthorizationToken(EmptyCTX, token.Token) + }, + }, + }, + { + name: "add authorization, project owned, ORG_OWNER, ok", + args: args{ + func(t *testing.T, request *authorization.CreateAuthorizationRequest) context.Context { + selfOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + request.OrganizationId = &selfOrgId + request.ProjectId = Instance.CreateProject(IAMCTX, t, selfOrgId, gofakeit.AppName(), false, false).Id + request.RoleKeys = []string{gofakeit.AppName()} + Instance.AddProjectRole(IAMCTX, t, request.ProjectId, request.RoleKeys[0], gofakeit.AppName(), "") + request.UserId = Instance.Users.Get(integration.UserTypeIAMOwner).ID + callingUser := Instance.CreateUserTypeMachine(IAMCTX, selfOrgId) + Instance.CreateOrgMembership(t, IAMCTX, selfOrgId, callingUser.Id) + token, err := Instance.Client.UserV2.AddPersonalAccessToken(IAMCTX, &user.AddPersonalAccessTokenRequest{UserId: callingUser.Id, ExpirationDate: timestamppb.New(time.Now().Add(24 * time.Hour))}) + require.NoError(t, err) + return integration.WithAuthorizationToken(EmptyCTX, token.Token) + }, + }, + }, + { + name: "add authorization, project owned, no permission, error", + args: args{ + + func(t *testing.T, request *authorization.CreateAuthorizationRequest) context.Context { + selfOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + request.OrganizationId = &selfOrgId + request.ProjectId = Instance.CreateProject(IAMCTX, t, selfOrgId, gofakeit.AppName(), false, false).Id + request.RoleKeys = []string{gofakeit.AppName()} + Instance.AddProjectRole(IAMCTX, t, request.ProjectId, request.RoleKeys[0], gofakeit.AppName(), "") + request.UserId = Instance.Users.Get(integration.UserTypeIAMOwner).ID + callingUser := Instance.CreateUserTypeMachine(IAMCTX, selfOrgId) + token, err := Instance.Client.UserV2.AddPersonalAccessToken(IAMCTX, &user.AddPersonalAccessTokenRequest{UserId: callingUser.Id, ExpirationDate: timestamppb.New(time.Now().Add(24 * time.Hour))}) + require.NoError(t, err) + return integration.WithAuthorizationToken(EmptyCTX, token.Token) + }, + }, + wantErr: true, + }, + { + name: "add authorization, role does not exist, error", + args: args{ + func(t *testing.T, request *authorization.CreateAuthorizationRequest) context.Context { + selfOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + request.OrganizationId = &selfOrgId + request.ProjectId = Instance.CreateProject(IAMCTX, t, selfOrgId, gofakeit.AppName(), false, false).Id + request.RoleKeys = []string{gofakeit.AppName()} + request.UserId = Instance.Users.Get(integration.UserTypeIAMOwner).ID + callingUser := Instance.CreateUserTypeMachine(IAMCTX, selfOrgId) + Instance.CreateProjectMembership(t, IAMCTX, request.ProjectId, callingUser.Id) + token, err := Instance.Client.UserV2.AddPersonalAccessToken(IAMCTX, &user.AddPersonalAccessTokenRequest{UserId: callingUser.Id, ExpirationDate: timestamppb.New(time.Now().Add(24 * time.Hour))}) + require.NoError(t, err) + return integration.WithAuthorizationToken(EmptyCTX, token.Token) + }, + }, + wantErr: true, + }, + { + name: "add authorization, project does not exist, error", + args: args{ + func(t *testing.T, request *authorization.CreateAuthorizationRequest) context.Context { + selfOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + request.OrganizationId = &selfOrgId + request.ProjectId = gofakeit.AppName() + request.UserId = Instance.Users.Get(integration.UserTypeIAMOwner).ID + callingUser := Instance.CreateUserTypeMachine(IAMCTX, selfOrgId) + token, err := Instance.Client.UserV2.AddPersonalAccessToken(IAMCTX, &user.AddPersonalAccessTokenRequest{UserId: callingUser.Id, ExpirationDate: timestamppb.New(time.Now().Add(24 * time.Hour))}) + require.NoError(t, err) + return integration.WithAuthorizationToken(EmptyCTX, token.Token) + }, + }, + wantErr: true, + }, + { + name: "add authorization, org does not exist, error", + args: args{ + func(t *testing.T, request *authorization.CreateAuthorizationRequest) context.Context { + selfOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + request.OrganizationId = gu.Ptr(gofakeit.AppName()) + request.ProjectId = Instance.CreateProject(IAMCTX, t, selfOrgId, gofakeit.AppName(), false, false).Id + request.UserId = Instance.Users.Get(integration.UserTypeIAMOwner).ID + callingUser := Instance.CreateUserTypeMachine(IAMCTX, selfOrgId) + Instance.CreateProjectMembership(t, IAMCTX, request.ProjectId, callingUser.Id) + token, err := Instance.Client.UserV2.AddPersonalAccessToken(IAMCTX, &user.AddPersonalAccessTokenRequest{UserId: callingUser.Id, ExpirationDate: timestamppb.New(time.Now().Add(24 * time.Hour))}) + require.NoError(t, err) + return integration.WithAuthorizationToken(EmptyCTX, token.Token) + + }, + }, + wantErr: true, + }, + { + name: "add authorization, project owner, project granted, no permission", + args: args{ + func(t *testing.T, request *authorization.CreateAuthorizationRequest) context.Context { + selfOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + request.OrganizationId = &selfOrgId + foreignOrg := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()) + request.ProjectId = Instance.CreateProject(IAMCTX, t, foreignOrg.OrganizationId, gofakeit.AppName(), false, false).Id + request.RoleKeys = []string{gofakeit.AppName()} + Instance.AddProjectRole(IAMCTX, t, request.ProjectId, request.RoleKeys[0], gofakeit.AppName(), "") + Instance.CreateProjectGrant(IAMCTX, t, request.ProjectId, selfOrgId, request.RoleKeys...) + request.UserId = Instance.Users.Get(integration.UserTypeIAMOwner).ID + callingUser := Instance.CreateUserTypeMachine(IAMCTX, selfOrgId) + Instance.CreateProjectMembership(t, IAMCTX, request.ProjectId, callingUser.Id) + token, err := Instance.Client.UserV2.AddPersonalAccessToken(IAMCTX, &user.AddPersonalAccessTokenRequest{UserId: callingUser.Id, ExpirationDate: timestamppb.New(time.Now().Add(24 * time.Hour))}) + require.NoError(t, err) + return integration.WithAuthorizationToken(EmptyCTX, token.Token) + }, + }, + wantErr: true, + }, + { + name: "add authorization, role key not granted, error", + args: args{ + func(t *testing.T, request *authorization.CreateAuthorizationRequest) context.Context { + selfOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + request.OrganizationId = &selfOrgId + foreignOrg := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()) + request.ProjectId = Instance.CreateProject(IAMCTX, t, foreignOrg.OrganizationId, gofakeit.AppName(), false, false).Id + request.RoleKeys = []string{gofakeit.AppName()} + Instance.AddProjectRole(IAMCTX, t, request.ProjectId, request.RoleKeys[0], gofakeit.AppName(), "") + Instance.CreateProjectGrant(IAMCTX, t, request.ProjectId, selfOrgId) + request.UserId = Instance.Users.Get(integration.UserTypeIAMOwner).ID + callingUser := Instance.CreateUserTypeMachine(IAMCTX, selfOrgId) + Instance.CreateProjectMembership(t, IAMCTX, request.ProjectId, callingUser.Id) + token, err := Instance.Client.UserV2.AddPersonalAccessToken(IAMCTX, &user.AddPersonalAccessTokenRequest{UserId: callingUser.Id, ExpirationDate: timestamppb.New(time.Now().Add(24 * time.Hour))}) + require.NoError(t, err) + return integration.WithAuthorizationToken(EmptyCTX, token.Token) + }, + }, + wantErr: true, + }, + { + name: "add authorization, grant does not exist, error", + args: args{ + func(t *testing.T, request *authorization.CreateAuthorizationRequest) context.Context { + selfOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + request.OrganizationId = &selfOrgId + foreignOrg := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()) + projectID := Instance.CreateProject(IAMCTX, t, foreignOrg.OrganizationId, gofakeit.AppName(), false, false).Id + request.ProjectId = projectID + request.RoleKeys = []string{gofakeit.AppName()} + Instance.AddProjectRole(IAMCTX, t, projectID, request.RoleKeys[0], gofakeit.AppName(), "") + request.UserId = Instance.Users.Get(integration.UserTypeIAMOwner).ID + callingUser := Instance.CreateUserTypeMachine(IAMCTX, selfOrgId) + Instance.CreateProjectMembership(t, IAMCTX, projectID, callingUser.Id) + token, err := Instance.Client.UserV2.AddPersonalAccessToken(IAMCTX, &user.AddPersonalAccessTokenRequest{UserId: callingUser.Id, ExpirationDate: timestamppb.New(time.Now().Add(24 * time.Hour))}) + require.NoError(t, err) + return integration.WithAuthorizationToken(EmptyCTX, token.Token) + + }, + }, + wantErr: true, + }, + { + name: "add authorization, PROJECT_OWNER on wrong org, error", + args: args{ + func(t *testing.T, request *authorization.CreateAuthorizationRequest) context.Context { + selfOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + request.OrganizationId = &selfOrgId + foreignOrg := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()) + request.ProjectId = Instance.CreateProject(IAMCTX, t, foreignOrg.OrganizationId, gofakeit.AppName(), false, false).Id + request.RoleKeys = []string{gofakeit.AppName()} + Instance.AddProjectRole(IAMCTX, t, request.ProjectId, request.RoleKeys[0], gofakeit.AppName(), "") + request.UserId = Instance.Users.Get(integration.UserTypeIAMOwner).ID + callingUser := Instance.CreateUserTypeMachine(IAMCTX, selfOrgId) + Instance.CreateProjectMembership(t, authz.SetCtxData(IAMCTX, authz.CtxData{OrgID: foreignOrg.OrganizationId}), request.ProjectId, callingUser.Id) + token, err := Instance.Client.UserV2.AddPersonalAccessToken(IAMCTX, &user.AddPersonalAccessTokenRequest{UserId: callingUser.Id, ExpirationDate: timestamppb.New(time.Now().Add(24 * time.Hour))}) + require.NoError(t, err) + return integration.WithAuthorizationToken(EmptyCTX, token.Token) + + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + req := &authorization.CreateAuthorizationRequest{} + ctx := tt.args.prepare(t, req) + got, err := Instance.Client.AuthorizationV2Beta.CreateAuthorization(ctx, req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.NotEmpty(t, got.Id, "id is empty") + creationDate := got.CreationDate.AsTime() + assert.Greater(t, creationDate, now, "creation date is before the test started") + assert.Less(t, creationDate, time.Now(), "creation date is in the future") + }) + } +} + +func TestServer_UpdateAuthorization(t *testing.T) { + type args struct { + prepare func(*testing.T, *authorization.UpdateAuthorizationRequest) context.Context + } + tests := []struct { + name string + args args + wantErr bool + wantChangedDateDuringPrepare bool + }{ + { + name: "update authorization, owned project, ok", + args: args{ + func(t *testing.T, request *authorization.UpdateAuthorizationRequest) context.Context { + selfOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + projectId := Instance.CreateProject(IAMCTX, t, selfOrgId, gofakeit.AppName(), false, false).Id + projectRole1 := gofakeit.AppName() + projectRole2 := gofakeit.AppName() + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole1, projectRole1, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole2, projectRole2, "") + preparedAuthorization, err := Instance.Client.AuthorizationV2Beta.CreateAuthorization(IAMCTX, &authorization.CreateAuthorizationRequest{ + UserId: Instance.Users.Get(integration.UserTypeIAMOwner).ID, + ProjectId: projectId, + RoleKeys: []string{projectRole1, projectRole2}, + }) + require.NoError(t, err) + request.Id = preparedAuthorization.Id + request.RoleKeys = []string{projectRole1} + callingUser := Instance.CreateUserTypeMachine(IAMCTX, selfOrgId) + Instance.CreateProjectMembership(t, IAMCTX, projectId, callingUser.Id) + token, err := Instance.Client.UserV2.AddPersonalAccessToken(IAMCTX, &user.AddPersonalAccessTokenRequest{UserId: callingUser.Id, ExpirationDate: timestamppb.New(time.Now().Add(24 * time.Hour))}) + require.NoError(t, err) + return integration.WithAuthorizationToken(EmptyCTX, token.Token) + }, + }, + }, + { + name: "update authorization, owned project, role not found, error", + args: args{ + func(t *testing.T, request *authorization.UpdateAuthorizationRequest) context.Context { + selfOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + projectId := Instance.CreateProject(IAMCTX, t, selfOrgId, gofakeit.AppName(), false, false).Id + projectRole1 := gofakeit.AppName() + projectRole2 := gofakeit.AppName() + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole1, projectRole1, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole2, projectRole2, "") + preparedAuthorization, err := Instance.Client.AuthorizationV2Beta.CreateAuthorization(IAMCTX, &authorization.CreateAuthorizationRequest{ + UserId: Instance.Users.Get(integration.UserTypeIAMOwner).ID, + ProjectId: projectId, + RoleKeys: []string{projectRole1, projectRole2}, + }) + require.NoError(t, err) + request.Id = preparedAuthorization.Id + request.RoleKeys = []string{projectRole1, projectRole2, gofakeit.AppName()} + callingUser := Instance.CreateUserTypeMachine(IAMCTX, selfOrgId) + Instance.CreateProjectMembership(t, IAMCTX, projectId, callingUser.Id) + token, err := Instance.Client.UserV2.AddPersonalAccessToken(IAMCTX, &user.AddPersonalAccessTokenRequest{UserId: callingUser.Id, ExpirationDate: timestamppb.New(time.Now().Add(24 * time.Hour))}) + require.NoError(t, err) + return integration.WithAuthorizationToken(EmptyCTX, token.Token) + }, + }, + wantErr: true, + }, + { + name: "update authorization, owned project, unchanged, ok, changed date is creation date", + args: args{ + func(t *testing.T, request *authorization.UpdateAuthorizationRequest) context.Context { + selfOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + projectId := Instance.CreateProject(IAMCTX, t, selfOrgId, gofakeit.AppName(), false, false).Id + projectRole1 := gofakeit.AppName() + projectRole2 := gofakeit.AppName() + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole1, projectRole1, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole2, projectRole2, "") + preparedAuthorization, err := Instance.Client.AuthorizationV2Beta.CreateAuthorization(IAMCTX, &authorization.CreateAuthorizationRequest{ + UserId: Instance.Users.Get(integration.UserTypeIAMOwner).ID, + ProjectId: projectId, + RoleKeys: []string{projectRole1, projectRole2}, + }) + require.NoError(t, err) + request.Id = preparedAuthorization.Id + request.RoleKeys = []string{projectRole1, projectRole2} + callingUser := Instance.CreateUserTypeMachine(IAMCTX, selfOrgId) + Instance.CreateProjectMembership(t, IAMCTX, projectId, callingUser.Id) + token, err := Instance.Client.UserV2.AddPersonalAccessToken(IAMCTX, &user.AddPersonalAccessTokenRequest{UserId: callingUser.Id, ExpirationDate: timestamppb.New(time.Now().Add(24 * time.Hour))}) + require.NoError(t, err) + return integration.WithAuthorizationToken(EmptyCTX, token.Token) + }, + }, + wantChangedDateDuringPrepare: true, + }, + { + name: "update authorization, granted project, ok", + args: args{ + func(t *testing.T, request *authorization.UpdateAuthorizationRequest) context.Context { + selfOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + foreignOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + projectId := Instance.CreateProject(IAMCTX, t, foreignOrgId, gofakeit.AppName(), false, false).Id + projectRole1 := gofakeit.AppName() + projectRole2 := gofakeit.AppName() + projectRole3 := gofakeit.AppName() + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole1, projectRole1, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole2, projectRole2, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole3, projectRole3, "") + Instance.CreateProjectGrant(IAMCTX, t, projectId, selfOrgId, projectRole1, projectRole2) + + preparedAuthorization, err := Instance.Client.AuthorizationV2Beta.CreateAuthorization(IAMCTX, &authorization.CreateAuthorizationRequest{ + UserId: Instance.Users.Get(integration.UserTypeIAMOwner).ID, + ProjectId: projectId, + OrganizationId: &selfOrgId, + RoleKeys: []string{projectRole1, projectRole2}, + }) + require.NoError(t, err) + request.Id = preparedAuthorization.Id + request.RoleKeys = []string{projectRole1} + token := createUserWithProjectGrantMembership(IAMCTX, t, Instance, projectId, selfOrgId) + return integration.WithAuthorizationToken(EmptyCTX, token) + + }, + }, + }, + { + name: "update authorization, granted project, role not granted, error", + args: args{ + func(t *testing.T, request *authorization.UpdateAuthorizationRequest) context.Context { + selfOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + foreignOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + projectId := Instance.CreateProject(IAMCTX, t, foreignOrgId, gofakeit.AppName(), false, false).Id + projectRole1 := gofakeit.AppName() + projectRole2 := gofakeit.AppName() + projectRole3 := gofakeit.AppName() + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole1, projectRole1, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole2, projectRole2, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole3, projectRole3, "") + Instance.CreateProjectGrant(IAMCTX, t, projectId, selfOrgId, projectRole1, projectRole2) + preparedAuthorization, err := Instance.Client.AuthorizationV2Beta.CreateAuthorization(IAMCTX, &authorization.CreateAuthorizationRequest{ + UserId: Instance.Users.Get(integration.UserTypeIAMOwner).ID, + ProjectId: projectId, + OrganizationId: &selfOrgId, + RoleKeys: []string{projectRole1, projectRole2}, + }) + require.NoError(t, err) + request.Id = preparedAuthorization.Id + request.RoleKeys = []string{projectId, projectRole3, projectRole3} + callingUser := Instance.CreateUserTypeMachine(IAMCTX, selfOrgId) + Instance.CreateProjectGrantMembership(t, IAMCTX, projectId, selfOrgId, callingUser.Id) + token, err := Instance.Client.UserV2.AddPersonalAccessToken(IAMCTX, &user.AddPersonalAccessTokenRequest{UserId: callingUser.Id, ExpirationDate: timestamppb.New(time.Now().Add(24 * time.Hour))}) + require.NoError(t, err) + return integration.WithAuthorizationToken(EmptyCTX, token.Token) + }, + }, + wantErr: true, + }, + { + name: "update authorization, granted project, grant removed, error", + args: args{ + func(t *testing.T, request *authorization.UpdateAuthorizationRequest) context.Context { + selfOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + foreignOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + projectId := Instance.CreateProject(IAMCTX, t, foreignOrgId, gofakeit.AppName(), false, false).Id + projectRole1 := gofakeit.AppName() + projectRole2 := gofakeit.AppName() + projectRole3 := gofakeit.AppName() + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole1, projectRole1, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole2, projectRole2, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole3, projectRole3, "") + Instance.CreateProjectGrant(IAMCTX, t, projectId, selfOrgId, projectRole1, projectRole2) + preparedAuthorization, err := Instance.Client.AuthorizationV2Beta.CreateAuthorization(IAMCTX, &authorization.CreateAuthorizationRequest{ + UserId: Instance.Users.Get(integration.UserTypeIAMOwner).ID, + ProjectId: projectId, + OrganizationId: &selfOrgId, + RoleKeys: []string{projectRole1, projectRole2}, + }) + require.NoError(t, err) + request.Id = preparedAuthorization.Id + request.RoleKeys = []string{projectRole1} + callingUser := Instance.CreateUserTypeMachine(IAMCTX, selfOrgId) + Instance.CreateProjectGrantMembership(t, IAMCTX, projectId, selfOrgId, callingUser.Id) + _, err = Instance.Client.Projectv2Beta.DeleteProjectGrant(IAMCTX, &project.DeleteProjectGrantRequest{ + ProjectId: projectId, + GrantedOrganizationId: selfOrgId, + }) + require.NoError(t, err) + token, err := Instance.Client.UserV2.AddPersonalAccessToken(IAMCTX, &user.AddPersonalAccessTokenRequest{UserId: callingUser.Id, ExpirationDate: timestamppb.New(time.Now().Add(24 * time.Hour))}) + require.NoError(t, err) + return integration.WithAuthorizationToken(EmptyCTX, token.Token) + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + req := &authorization.UpdateAuthorizationRequest{} + ctx := tt.args.prepare(t, req) + afterPrepare := time.Now() + got, err := Instance.Client.AuthorizationV2Beta.UpdateAuthorization(ctx, req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.NotEmpty(t, got.ChangeDate, "change date is empty") + changeDate := got.ChangeDate.AsTime() + assert.Greater(t, changeDate, now, "change date is before the test started") + if tt.wantChangedDateDuringPrepare { + assert.Less(t, changeDate, afterPrepare, "change date is after prepare finished") + } else { + assert.Less(t, changeDate, time.Now(), "change date is in the future") + } + }) + } +} + +func TestServer_DeleteAuthorization(t *testing.T) { + type args struct { + prepare func(*testing.T, *authorization.DeleteAuthorizationRequest) context.Context + } + tests := []struct { + name string + args args + wantErr bool + wantDeletionDateDuringPrepare bool + }{ + { + name: "delete authorization, project owned by calling users org, ok", + args: args{ + func(t *testing.T, request *authorization.DeleteAuthorizationRequest) context.Context { + selfOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + projectId := Instance.CreateProject(IAMCTX, t, selfOrgId, gofakeit.AppName(), false, false).Id + projectRole1 := gofakeit.AppName() + projectRole2 := gofakeit.AppName() + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole1, projectRole1, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole2, projectRole2, "") + preparedAuthorization, err := Instance.Client.AuthorizationV2Beta.CreateAuthorization(IAMCTX, &authorization.CreateAuthorizationRequest{ + UserId: Instance.Users.Get(integration.UserTypeIAMOwner).ID, + ProjectId: projectId, + RoleKeys: []string{projectRole1, projectRole2}, + }) + require.NoError(t, err) + request.Id = preparedAuthorization.Id + callingUser := Instance.CreateUserTypeMachine(IAMCTX, selfOrgId) + Instance.CreateProjectMembership(t, IAMCTX, projectId, callingUser.Id) + token, err := Instance.Client.UserV2.AddPersonalAccessToken(IAMCTX, &user.AddPersonalAccessTokenRequest{UserId: callingUser.Id, ExpirationDate: timestamppb.New(time.Now().Add(24 * time.Hour))}) + require.NoError(t, err) + return integration.WithAuthorizationToken(EmptyCTX, token.Token) + }, + }, + }, { + name: "delete authorization, owned project, user membership on project owning org, ok", + args: args{ + func(t *testing.T, request *authorization.DeleteAuthorizationRequest) context.Context { + selfOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + foreignOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + projectId := Instance.CreateProject(IAMCTX, t, foreignOrgId, gofakeit.AppName(), false, false).Id + projectRole1 := gofakeit.AppName() + projectRole2 := gofakeit.AppName() + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole1, projectRole1, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole2, projectRole2, "") + preparedAuthorization, err := Instance.Client.AuthorizationV2Beta.CreateAuthorization(IAMCTX, &authorization.CreateAuthorizationRequest{ + UserId: Instance.Users.Get(integration.UserTypeIAMOwner).ID, + ProjectId: projectId, + RoleKeys: []string{projectRole1, projectRole2}, + }) + require.NoError(t, err) + request.Id = preparedAuthorization.Id + callingUser := Instance.CreateUserTypeMachine(IAMCTX, selfOrgId) + Instance.CreateProjectMembership(t, authz.SetCtxData(IAMCTX, authz.CtxData{OrgID: foreignOrgId}), projectId, callingUser.Id) + token, err := Instance.Client.UserV2.AddPersonalAccessToken(IAMCTX, &user.AddPersonalAccessTokenRequest{UserId: callingUser.Id, ExpirationDate: timestamppb.New(time.Now().Add(24 * time.Hour))}) + require.NoError(t, err) + return integration.WithAuthorizationToken(EmptyCTX, token.Token) + }, + }, + }, + { + name: "delete authorization, granted project, user membership on project owning org, error", + args: args{ + func(t *testing.T, request *authorization.DeleteAuthorizationRequest) context.Context { + selfOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + foreignOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + projectId := Instance.CreateProject(IAMCTX, t, foreignOrgId, gofakeit.AppName(), false, false).Id + projectRole1 := gofakeit.AppName() + projectRole2 := gofakeit.AppName() + projectRole3 := gofakeit.AppName() + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole1, projectRole1, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole2, projectRole2, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole3, projectRole3, "") + Instance.CreateProjectGrant(IAMCTX, t, projectId, selfOrgId, projectRole1, projectRole2) + preparedAuthorization, err := Instance.Client.AuthorizationV2Beta.CreateAuthorization(IAMCTX, &authorization.CreateAuthorizationRequest{ + UserId: Instance.Users.Get(integration.UserTypeIAMOwner).ID, + ProjectId: projectId, + OrganizationId: &selfOrgId, + RoleKeys: []string{projectRole1, projectRole2}, + }) + require.NoError(t, err) + request.Id = preparedAuthorization.Id + callingUser := Instance.CreateUserTypeMachine(IAMCTX, selfOrgId) + Instance.CreateProjectMembership(t, authz.SetCtxData(IAMCTX, authz.CtxData{OrgID: foreignOrgId}), projectId, callingUser.Id) + token, err := Instance.Client.UserV2.AddPersonalAccessToken(IAMCTX, &user.AddPersonalAccessTokenRequest{UserId: callingUser.Id, ExpirationDate: timestamppb.New(time.Now().Add(24 * time.Hour))}) + require.NoError(t, err) + return integration.WithAuthorizationToken(EmptyCTX, token.Token) + }, + }, + wantErr: true, + }, + { + name: "delete authorization, granted project, user membership on project granted org, ok", + args: args{ + func(t *testing.T, request *authorization.DeleteAuthorizationRequest) context.Context { + selfOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + foreignOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + projectId := Instance.CreateProject(IAMCTX, t, foreignOrgId, gofakeit.AppName(), false, false).Id + projectRole1 := gofakeit.AppName() + projectRole2 := gofakeit.AppName() + projectRole3 := gofakeit.AppName() + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole1, projectRole1, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole2, projectRole2, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole3, projectRole3, "") + Instance.CreateProjectGrant(IAMCTX, t, projectId, selfOrgId, projectRole1, projectRole2) + + preparedAuthorization, err := Instance.Client.AuthorizationV2Beta.CreateAuthorization(IAMCTX, &authorization.CreateAuthorizationRequest{ + UserId: Instance.Users.Get(integration.UserTypeIAMOwner).ID, + ProjectId: projectId, + OrganizationId: &selfOrgId, + RoleKeys: []string{projectRole1, projectRole2}, + }) + require.NoError(t, err) + request.Id = preparedAuthorization.Id + return integration.WithAuthorizationToken(EmptyCTX, createUserWithProjectGrantMembership(IAMCTX, t, Instance, projectId, selfOrgId)) + + }, + }, + }, + { + name: "delete authorization, already deleted, ok, deletion date is creation date", + args: args{ + func(t *testing.T, request *authorization.DeleteAuthorizationRequest) context.Context { + selfOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + projectId := Instance.CreateProject(IAMCTX, t, selfOrgId, gofakeit.AppName(), false, false).Id + projectRole1 := gofakeit.AppName() + projectRole2 := gofakeit.AppName() + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole1, projectRole1, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole2, projectRole2, "") + preparedAuthorization, err := Instance.Client.AuthorizationV2Beta.CreateAuthorization(IAMCTX, &authorization.CreateAuthorizationRequest{ + UserId: Instance.Users.Get(integration.UserTypeIAMOwner).ID, + ProjectId: projectId, + RoleKeys: []string{projectRole1, projectRole2}, + }) + require.NoError(t, err) + _, err = Instance.Client.AuthorizationV2Beta.DeleteAuthorization(IAMCTX, &authorization.DeleteAuthorizationRequest{ + Id: preparedAuthorization.Id, + }) + require.NoError(t, err) + request.Id = preparedAuthorization.Id + callingUser := Instance.CreateUserTypeMachine(IAMCTX, selfOrgId) + Instance.CreateProjectMembership(t, IAMCTX, projectId, callingUser.Id) + token, err := Instance.Client.UserV2.AddPersonalAccessToken(IAMCTX, &user.AddPersonalAccessTokenRequest{UserId: callingUser.Id, ExpirationDate: timestamppb.New(time.Now().Add(24 * time.Hour))}) + require.NoError(t, err) + return integration.WithAuthorizationToken(EmptyCTX, token.Token) + }, + }, + wantDeletionDateDuringPrepare: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + req := &authorization.DeleteAuthorizationRequest{} + ctx := tt.args.prepare(t, req) + afterPrepare := time.Now() + got, err := Instance.Client.AuthorizationV2Beta.DeleteAuthorization(ctx, req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.NotEmpty(t, got.DeletionDate, "deletion date is empty") + changeDate := got.DeletionDate.AsTime() + assert.Greater(t, changeDate, now, "deletion date is before the test started") + if tt.wantDeletionDateDuringPrepare { + assert.Less(t, changeDate, afterPrepare, "deletion date is after prepare finished") + } else { + assert.Less(t, changeDate, time.Now(), "deletion date is in the future") + } + }) + } +} + +func TestServer_DeactivateAuthorization(t *testing.T) { + type args struct { + prepare func(*testing.T, *authorization.DeactivateAuthorizationRequest) context.Context + } + tests := []struct { + name string + args args + wantErr bool + wantDeletionDateDuringPrepare bool + }{ + { + name: "deactivate authorization, project owned by calling users org, ok", + args: args{ + func(t *testing.T, request *authorization.DeactivateAuthorizationRequest) context.Context { + selfOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + projectId := Instance.CreateProject(IAMCTX, t, selfOrgId, gofakeit.AppName(), false, false).Id + projectRole1 := gofakeit.AppName() + projectRole2 := gofakeit.AppName() + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole1, projectRole1, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole2, projectRole2, "") + preparedAuthorization, err := Instance.Client.AuthorizationV2Beta.CreateAuthorization(IAMCTX, &authorization.CreateAuthorizationRequest{ + UserId: Instance.Users.Get(integration.UserTypeIAMOwner).ID, + ProjectId: projectId, + RoleKeys: []string{projectRole1, projectRole2}, + }) + require.NoError(t, err) + request.Id = preparedAuthorization.Id + callingUser := Instance.CreateUserTypeMachine(IAMCTX, selfOrgId) + Instance.CreateProjectMembership(t, IAMCTX, projectId, callingUser.Id) + token, err := Instance.Client.UserV2.AddPersonalAccessToken(IAMCTX, &user.AddPersonalAccessTokenRequest{UserId: callingUser.Id, ExpirationDate: timestamppb.New(time.Now().Add(24 * time.Hour))}) + require.NoError(t, err) + return integration.WithAuthorizationToken(EmptyCTX, token.Token) + }, + }, + }, { + name: "deactivate authorization, owned project, user membership on project owning org, ok", + args: args{ + func(t *testing.T, request *authorization.DeactivateAuthorizationRequest) context.Context { + selfOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + foreignOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + projectId := Instance.CreateProject(IAMCTX, t, foreignOrgId, gofakeit.AppName(), false, false).Id + projectRole1 := gofakeit.AppName() + projectRole2 := gofakeit.AppName() + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole1, projectRole1, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole2, projectRole2, "") + preparedAuthorization, err := Instance.Client.AuthorizationV2Beta.CreateAuthorization(IAMCTX, &authorization.CreateAuthorizationRequest{ + UserId: Instance.Users.Get(integration.UserTypeIAMOwner).ID, + ProjectId: projectId, + RoleKeys: []string{projectRole1, projectRole2}, + }) + require.NoError(t, err) + request.Id = preparedAuthorization.Id + callingUser := Instance.CreateUserTypeMachine(IAMCTX, selfOrgId) + Instance.CreateProjectMembership(t, authz.SetCtxData(IAMCTX, authz.CtxData{OrgID: foreignOrgId}), projectId, callingUser.Id) + token, err := Instance.Client.UserV2.AddPersonalAccessToken(IAMCTX, &user.AddPersonalAccessTokenRequest{UserId: callingUser.Id, ExpirationDate: timestamppb.New(time.Now().Add(24 * time.Hour))}) + require.NoError(t, err) + return integration.WithAuthorizationToken(EmptyCTX, token.Token) + }, + }, + }, + { + name: "deactivate authorization, granted project, user membership on project owning org, error", + args: args{ + func(t *testing.T, request *authorization.DeactivateAuthorizationRequest) context.Context { + selfOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + foreignOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + projectId := Instance.CreateProject(IAMCTX, t, foreignOrgId, gofakeit.AppName(), false, false).Id + projectRole1 := gofakeit.AppName() + projectRole2 := gofakeit.AppName() + projectRole3 := gofakeit.AppName() + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole1, projectRole1, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole2, projectRole2, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole3, projectRole3, "") + Instance.CreateProjectGrant(IAMCTX, t, projectId, selfOrgId, projectRole1, projectRole2) + preparedAuthorization, err := Instance.Client.AuthorizationV2Beta.CreateAuthorization(IAMCTX, &authorization.CreateAuthorizationRequest{ + UserId: Instance.Users.Get(integration.UserTypeIAMOwner).ID, + ProjectId: projectId, + OrganizationId: &selfOrgId, + RoleKeys: []string{projectRole1, projectRole2}, + }) + require.NoError(t, err) + request.Id = preparedAuthorization.Id + callingUser := Instance.CreateUserTypeMachine(IAMCTX, selfOrgId) + Instance.CreateProjectMembership(t, authz.SetCtxData(IAMCTX, authz.CtxData{OrgID: foreignOrgId}), projectId, callingUser.Id) + token, err := Instance.Client.UserV2.AddPersonalAccessToken(IAMCTX, &user.AddPersonalAccessTokenRequest{UserId: callingUser.Id, ExpirationDate: timestamppb.New(time.Now().Add(24 * time.Hour))}) + require.NoError(t, err) + return integration.WithAuthorizationToken(EmptyCTX, token.Token) + }, + }, + wantErr: true, + }, + { + name: "deactivate authorization, granted project, user membership on project granted org, ok", + args: args{ + func(t *testing.T, request *authorization.DeactivateAuthorizationRequest) context.Context { + selfOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + foreignOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + projectId := Instance.CreateProject(IAMCTX, t, foreignOrgId, gofakeit.AppName(), false, false).Id + projectRole1 := gofakeit.AppName() + projectRole2 := gofakeit.AppName() + projectRole3 := gofakeit.AppName() + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole1, projectRole1, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole2, projectRole2, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole3, projectRole3, "") + Instance.CreateProjectGrant(IAMCTX, t, projectId, selfOrgId, projectRole1, projectRole2) + + preparedAuthorization, err := Instance.Client.AuthorizationV2Beta.CreateAuthorization(IAMCTX, &authorization.CreateAuthorizationRequest{ + UserId: Instance.Users.Get(integration.UserTypeIAMOwner).ID, + ProjectId: projectId, + OrganizationId: &selfOrgId, + RoleKeys: []string{projectRole1, projectRole2}, + }) + require.NoError(t, err) + request.Id = preparedAuthorization.Id + return integration.WithAuthorizationToken(EmptyCTX, createUserWithProjectGrantMembership(IAMCTX, t, Instance, projectId, selfOrgId)) + }, + }, + }, + { + name: "deactivate authorization, already inactive, ok, change date is creation date", + args: args{ + func(t *testing.T, request *authorization.DeactivateAuthorizationRequest) context.Context { + selfOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + projectId := Instance.CreateProject(IAMCTX, t, selfOrgId, gofakeit.AppName(), false, false).Id + projectRole1 := gofakeit.AppName() + projectRole2 := gofakeit.AppName() + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole1, projectRole1, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole2, projectRole2, "") + preparedAuthorization, err := Instance.Client.AuthorizationV2Beta.CreateAuthorization(IAMCTX, &authorization.CreateAuthorizationRequest{ + UserId: Instance.Users.Get(integration.UserTypeIAMOwner).ID, + ProjectId: projectId, + RoleKeys: []string{projectRole1, projectRole2}, + }) + require.NoError(t, err) + _, err = Instance.Client.AuthorizationV2Beta.DeactivateAuthorization(IAMCTX, &authorization.DeactivateAuthorizationRequest{ + Id: preparedAuthorization.Id, + }) + require.NoError(t, err) + request.Id = preparedAuthorization.Id + callingUser := Instance.CreateUserTypeMachine(IAMCTX, selfOrgId) + Instance.CreateProjectMembership(t, IAMCTX, projectId, callingUser.Id) + token, err := Instance.Client.UserV2.AddPersonalAccessToken(IAMCTX, &user.AddPersonalAccessTokenRequest{UserId: callingUser.Id, ExpirationDate: timestamppb.New(time.Now().Add(24 * time.Hour))}) + require.NoError(t, err) + return integration.WithAuthorizationToken(EmptyCTX, token.Token) + }, + }, + wantDeletionDateDuringPrepare: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + req := &authorization.DeactivateAuthorizationRequest{} + ctx := tt.args.prepare(t, req) + afterPrepare := time.Now() + got, err := Instance.Client.AuthorizationV2Beta.DeactivateAuthorization(ctx, req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.NotEmpty(t, got.ChangeDate, "change date is empty") + changeDate := got.ChangeDate.AsTime() + assert.Greater(t, changeDate, now, "change date is before the test started") + if tt.wantDeletionDateDuringPrepare { + assert.Less(t, changeDate, afterPrepare, "change date is after prepare finished") + } else { + assert.Less(t, changeDate, time.Now(), "change date is in the future") + } + }) + } +} + +func TestServer_ActivateAuthorization(t *testing.T) { + type args struct { + prepare func(*testing.T, *authorization.ActivateAuthorizationRequest) context.Context + } + tests := []struct { + name string + args args + wantErr bool + wantDeletionDateDuringPrepare bool + }{ + { + name: "activate authorization, project owned by calling users org, ok", + args: args{ + func(t *testing.T, request *authorization.ActivateAuthorizationRequest) context.Context { + selfOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + projectId := Instance.CreateProject(IAMCTX, t, selfOrgId, gofakeit.AppName(), false, false).Id + projectRole1 := gofakeit.AppName() + projectRole2 := gofakeit.AppName() + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole1, projectRole1, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole2, projectRole2, "") + preparedAuthorization, err := Instance.Client.AuthorizationV2Beta.CreateAuthorization(IAMCTX, &authorization.CreateAuthorizationRequest{ + UserId: Instance.Users.Get(integration.UserTypeIAMOwner).ID, + ProjectId: projectId, + RoleKeys: []string{projectRole1, projectRole2}, + }) + require.NoError(t, err) + request.Id = preparedAuthorization.Id + callingUser := Instance.CreateUserTypeMachine(IAMCTX, selfOrgId) + Instance.CreateProjectMembership(t, IAMCTX, projectId, callingUser.Id) + _, err = Instance.Client.AuthorizationV2Beta.DeactivateAuthorization(IAMCTX, &authorization.DeactivateAuthorizationRequest{ + Id: preparedAuthorization.Id, + }) + require.NoError(t, err) + token, err := Instance.Client.UserV2.AddPersonalAccessToken(IAMCTX, &user.AddPersonalAccessTokenRequest{UserId: callingUser.Id, ExpirationDate: timestamppb.New(time.Now().Add(24 * time.Hour))}) + require.NoError(t, err) + return integration.WithAuthorizationToken(EmptyCTX, token.Token) + }, + }, + }, { + name: "activate authorization, owned project, user membership on project owning org, ok", + args: args{ + func(t *testing.T, request *authorization.ActivateAuthorizationRequest) context.Context { + selfOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + foreignOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + projectId := Instance.CreateProject(IAMCTX, t, foreignOrgId, gofakeit.AppName(), false, false).Id + projectRole1 := gofakeit.AppName() + projectRole2 := gofakeit.AppName() + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole1, projectRole1, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole2, projectRole2, "") + preparedAuthorization, err := Instance.Client.AuthorizationV2Beta.CreateAuthorization(IAMCTX, &authorization.CreateAuthorizationRequest{ + UserId: Instance.Users.Get(integration.UserTypeIAMOwner).ID, + ProjectId: projectId, + RoleKeys: []string{projectRole1, projectRole2}, + }) + require.NoError(t, err) + request.Id = preparedAuthorization.Id + callingUser := Instance.CreateUserTypeMachine(IAMCTX, selfOrgId) + Instance.CreateProjectMembership(t, authz.SetCtxData(IAMCTX, authz.CtxData{OrgID: foreignOrgId}), projectId, callingUser.Id) + _, err = Instance.Client.AuthorizationV2Beta.DeactivateAuthorization(IAMCTX, &authorization.DeactivateAuthorizationRequest{ + Id: preparedAuthorization.Id, + }) + require.NoError(t, err) + token, err := Instance.Client.UserV2.AddPersonalAccessToken(IAMCTX, &user.AddPersonalAccessTokenRequest{UserId: callingUser.Id, ExpirationDate: timestamppb.New(time.Now().Add(24 * time.Hour))}) + require.NoError(t, err) + return integration.WithAuthorizationToken(EmptyCTX, token.Token) + }, + }, + }, + { + name: "activate authorization, granted project, user membership on project owning org, error", + args: args{ + func(t *testing.T, request *authorization.ActivateAuthorizationRequest) context.Context { + selfOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + foreignOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + projectId := Instance.CreateProject(IAMCTX, t, foreignOrgId, gofakeit.AppName(), false, false).Id + projectRole1 := gofakeit.AppName() + projectRole2 := gofakeit.AppName() + projectRole3 := gofakeit.AppName() + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole1, projectRole1, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole2, projectRole2, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole3, projectRole3, "") + Instance.CreateProjectGrant(IAMCTX, t, projectId, selfOrgId, projectRole1, projectRole2) + preparedAuthorization, err := Instance.Client.AuthorizationV2Beta.CreateAuthorization(IAMCTX, &authorization.CreateAuthorizationRequest{ + UserId: Instance.Users.Get(integration.UserTypeIAMOwner).ID, + ProjectId: projectId, + OrganizationId: &selfOrgId, + RoleKeys: []string{projectRole1, projectRole2}, + }) + require.NoError(t, err) + request.Id = preparedAuthorization.Id + callingUser := Instance.CreateUserTypeMachine(IAMCTX, selfOrgId) + Instance.CreateProjectMembership(t, authz.SetCtxData(IAMCTX, authz.CtxData{OrgID: foreignOrgId}), projectId, callingUser.Id) + _, err = Instance.Client.AuthorizationV2Beta.DeactivateAuthorization(IAMCTX, &authorization.DeactivateAuthorizationRequest{ + Id: preparedAuthorization.Id, + }) + require.NoError(t, err) + token, err := Instance.Client.UserV2.AddPersonalAccessToken(IAMCTX, &user.AddPersonalAccessTokenRequest{UserId: callingUser.Id, ExpirationDate: timestamppb.New(time.Now().Add(24 * time.Hour))}) + require.NoError(t, err) + return integration.WithAuthorizationToken(EmptyCTX, token.Token) + }, + }, + wantErr: true, + }, + { + name: "activate authorization, granted project, user membership on project granted org, ok", + args: args{ + func(t *testing.T, request *authorization.ActivateAuthorizationRequest) context.Context { + selfOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + foreignOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + projectId := Instance.CreateProject(IAMCTX, t, foreignOrgId, gofakeit.AppName(), false, false).Id + projectRole1 := gofakeit.AppName() + projectRole2 := gofakeit.AppName() + projectRole3 := gofakeit.AppName() + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole1, projectRole1, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole2, projectRole2, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole3, projectRole3, "") + Instance.CreateProjectGrant(IAMCTX, t, projectId, selfOrgId, projectRole1, projectRole2) + preparedAuthorization, err := Instance.Client.AuthorizationV2Beta.CreateAuthorization(IAMCTX, &authorization.CreateAuthorizationRequest{ + UserId: Instance.Users.Get(integration.UserTypeIAMOwner).ID, + ProjectId: projectId, + OrganizationId: &selfOrgId, + RoleKeys: []string{projectRole1, projectRole2}, + }) + require.NoError(t, err) + request.Id = preparedAuthorization.Id + _, err = Instance.Client.AuthorizationV2Beta.DeactivateAuthorization(IAMCTX, &authorization.DeactivateAuthorizationRequest{ + Id: preparedAuthorization.Id, + }) + require.NoError(t, err) + return integration.WithAuthorizationToken(EmptyCTX, createUserWithProjectGrantMembership(IAMCTX, t, Instance, projectId, selfOrgId)) + }, + }, + }, + { + name: "activate authorization, already active, ok, change date is creation date", + args: args{ + func(t *testing.T, request *authorization.ActivateAuthorizationRequest) context.Context { + selfOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + projectId := Instance.CreateProject(IAMCTX, t, selfOrgId, gofakeit.AppName(), false, false).Id + projectRole1 := gofakeit.AppName() + projectRole2 := gofakeit.AppName() + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole1, projectRole1, "") + Instance.AddProjectRole(IAMCTX, t, projectId, projectRole2, projectRole2, "") + preparedAuthorization, err := Instance.Client.AuthorizationV2Beta.CreateAuthorization(IAMCTX, &authorization.CreateAuthorizationRequest{ + UserId: Instance.Users.Get(integration.UserTypeIAMOwner).ID, + ProjectId: projectId, + RoleKeys: []string{projectRole1, projectRole2}, + }) + require.NoError(t, err) + request.Id = preparedAuthorization.Id + callingUser := Instance.CreateUserTypeMachine(IAMCTX, selfOrgId) + Instance.CreateProjectMembership(t, IAMCTX, projectId, callingUser.Id) + token, err := Instance.Client.UserV2.AddPersonalAccessToken(IAMCTX, &user.AddPersonalAccessTokenRequest{UserId: callingUser.Id, ExpirationDate: timestamppb.New(time.Now().Add(24 * time.Hour))}) + require.NoError(t, err) + return integration.WithAuthorizationToken(EmptyCTX, token.Token) + }, + }, + wantDeletionDateDuringPrepare: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + req := &authorization.ActivateAuthorizationRequest{} + ctx := tt.args.prepare(t, req) + afterPrepare := time.Now() + got, err := Instance.Client.AuthorizationV2Beta.ActivateAuthorization(ctx, req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.NotEmpty(t, got.ChangeDate, "change date is empty") + changeDate := got.ChangeDate.AsTime() + assert.Greater(t, changeDate, now, "change date is before the test started") + if tt.wantDeletionDateDuringPrepare { + assert.Less(t, changeDate, afterPrepare, "change date is after prepare finished") + } else { + assert.Less(t, changeDate, time.Now(), "change date is in the future") + } + }) + } +} + +func createUserWithProjectGrantMembership(ctx context.Context, t *testing.T, instance *integration.Instance, projectID, grantID string) string { + selfOrgId := Instance.CreateOrganization(IAMCTX, gofakeit.AppName(), gofakeit.Email()).OrganizationId + callingUser := instance.CreateUserTypeMachine(ctx, selfOrgId) + instance.CreateProjectGrantMembership(t, ctx, projectID, grantID, callingUser.Id) + token, err := instance.Client.UserV2.AddPersonalAccessToken(IAMCTX, &user.AddPersonalAccessTokenRequest{UserId: callingUser.Id, ExpirationDate: timestamppb.New(time.Now().Add(24 * time.Hour))}) + require.NoError(t, err) + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, 10*time.Minute) + require.EventuallyWithT(t, func(tt *assert.CollectT) { + got, err := instance.Client.AuthorizationV2Beta.ListAuthorizations(ctx, &authorization.ListAuthorizationsRequest{ + Filters: nil, + }) + assert.NoError(tt, err) + if !assert.NotEmpty(tt, got.Pagination.TotalResult) { + return + } + }, retryDuration, tick) + return token.GetToken() +} diff --git a/internal/api/grpc/authorization/v2beta/integration_test/query_test.go b/internal/api/grpc/authorization/v2beta/integration_test/query_test.go new file mode 100644 index 0000000000..c3579d9192 --- /dev/null +++ b/internal/api/grpc/authorization/v2beta/integration_test/query_test.go @@ -0,0 +1,969 @@ +//go:build integration + +package authorization_test + +import ( + "context" + "testing" + "time" + + "github.com/brianvoe/gofakeit/v6" + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/internal/integration" + authorization "github.com/zitadel/zitadel/pkg/grpc/authorization/v2beta" + filter "github.com/zitadel/zitadel/pkg/grpc/filter/v2beta" + project "github.com/zitadel/zitadel/pkg/grpc/project/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +func TestServer_ListAuthorizations(t *testing.T) { + iamOwnerCtx := Instance.WithAuthorizationToken(EmptyCTX, integration.UserTypeIAMOwner) + projectOwnerResp := Instance.CreateMachineUser(iamOwnerCtx) + projectOwnerPatResp := Instance.CreatePersonalAccessToken(iamOwnerCtx, projectOwnerResp.GetUserId()) + projectResp := createProject(iamOwnerCtx, Instance, t, Instance.DefaultOrg.GetId(), false, false) + Instance.CreateProjectMembership(t, iamOwnerCtx, projectResp.GetId(), projectOwnerResp.GetUserId()) + projectOwnerCtx := integration.WithAuthorizationToken(EmptyCTX, projectOwnerPatResp.Token) + + projectGrantOwnerResp := Instance.CreateMachineUser(iamOwnerCtx) + projectGrantOwnerPatResp := Instance.CreatePersonalAccessToken(iamOwnerCtx, projectGrantOwnerResp.GetUserId()) + grantedProjectResp := createGrantedProject(iamOwnerCtx, Instance, t, projectResp) + Instance.CreateProjectGrantMembership(t, iamOwnerCtx, projectResp.GetId(), grantedProjectResp.GetGrantedOrganizationId(), projectGrantOwnerResp.GetUserId()) + projectGrantOwnerCtx := integration.WithAuthorizationToken(EmptyCTX, projectGrantOwnerPatResp.Token) + + type args struct { + ctx context.Context + dep func(*authorization.ListAuthorizationsRequest, *authorization.ListAuthorizationsResponse) + req *authorization.ListAuthorizationsRequest + } + tests := []struct { + name string + args args + want *authorization.ListAuthorizationsResponse + wantErr bool + }{ + { + name: "list by user id, unauthenticated", + args: args{ + ctx: EmptyCTX, + dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) { + userResp := Instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + + request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_UserId{ + UserId: &filter.IDFilter{ + Id: userResp.GetId(), + }, + } + createAuthorization(iamOwnerCtx, Instance, t, Instance.DefaultOrg.GetId(), userResp.GetId(), false) + }, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{{}}, + }, + }, + wantErr: true, + }, + { + name: "list by id, no permission", + args: args{ + ctx: Instance.WithAuthorizationToken(EmptyCTX, integration.UserTypeNoPermission), + dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) { + userResp := Instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + + request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_UserId{ + UserId: &filter.IDFilter{ + Id: userResp.GetId(), + }, + } + createAuthorization(iamOwnerCtx, Instance, t, Instance.DefaultOrg.GetId(), userResp.GetId(), false) + }, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{{}}, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Authorizations: []*authorization.Authorization{}, + }, + }, + { + name: "list, not found", + args: args{ + ctx: iamOwnerCtx, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{ + {Filter: &authorization.AuthorizationsSearchFilter_UserId{ + UserId: &filter.IDFilter{ + Id: "notexisting", + }, + }, + }, + }, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 0, + AppliedLimit: 100, + }, + }, + }, + { + name: "list single id, project", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) { + userResp := Instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + + request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_UserId{ + UserId: &filter.IDFilter{ + Id: userResp.GetId(), + }, + } + response.Authorizations[0] = createAuthorization(iamOwnerCtx, Instance, t, Instance.DefaultOrg.GetId(), userResp.GetId(), false) + }, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{{}}, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Authorizations: []*authorization.Authorization{ + {}, + }, + }, + }, + { + name: "list single id", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) { + userResp := Instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + + resp := createAuthorization(iamOwnerCtx, Instance, t, Instance.DefaultOrg.GetId(), userResp.GetId(), false) + request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_AuthorizationIds{ + AuthorizationIds: &filter.InIDsFilter{ + Ids: []string{resp.GetId()}, + }, + } + response.Authorizations[0] = resp + }, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{{}}, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Authorizations: []*authorization.Authorization{ + {}, + }, + }, + }, + { + name: "list single project id", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) { + userResp := Instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + + resp := createAuthorization(iamOwnerCtx, Instance, t, Instance.DefaultOrg.GetId(), userResp.GetId(), false) + request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_ProjectId{ + ProjectId: &filter.IDFilter{ + Id: resp.GetProjectId(), + }, + } + response.Authorizations[0] = resp + }, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{{}}, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Authorizations: []*authorization.Authorization{ + {}, + }, + }, + }, + { + name: "list single project name", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) { + userResp := Instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + + resp := createAuthorization(iamOwnerCtx, Instance, t, Instance.DefaultOrg.GetId(), userResp.GetId(), false) + request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_ProjectName{ + ProjectName: &authorization.ProjectNameQuery{ + Name: resp.GetProjectName(), + Method: filter.TextFilterMethod_TEXT_FILTER_METHOD_EQUALS, + }, + } + response.Authorizations[0] = resp + }, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{{}}, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Authorizations: []*authorization.Authorization{ + {}, + }, + }, + }, + { + name: "list single id, project grant", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) { + userResp := Instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + + request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_UserId{ + UserId: &filter.IDFilter{ + Id: userResp.GetId(), + }, + } + response.Authorizations[0] = createAuthorization(iamOwnerCtx, Instance, t, Instance.DefaultOrg.GetId(), userResp.GetId(), true) + }, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{{}}, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Authorizations: []*authorization.Authorization{ + {}, + }, + }, + }, + { + name: "list single grant id, project grant", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) { + userResp := Instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + + resp := createAuthorization(iamOwnerCtx, Instance, t, Instance.DefaultOrg.GetId(), userResp.GetId(), true) + request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_ProjectGrantId{ + ProjectGrantId: &filter.IDFilter{ + Id: resp.GetProjectGrantId(), + }, + } + response.Authorizations[0] = resp + }, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{{}}, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Authorizations: []*authorization.Authorization{ + {}, + }, + }, + }, + { + name: "list single id, project and project grant, multiple", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) { + userResp := Instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + + request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_UserId{ + UserId: &filter.IDFilter{ + Id: userResp.GetId(), + }, + } + response.Authorizations[5] = createAuthorization(iamOwnerCtx, Instance, t, Instance.DefaultOrg.GetId(), userResp.GetId(), false) + response.Authorizations[4] = createAuthorization(iamOwnerCtx, Instance, t, Instance.DefaultOrg.GetId(), userResp.GetId(), false) + response.Authorizations[3] = createAuthorization(iamOwnerCtx, Instance, t, Instance.DefaultOrg.GetId(), userResp.GetId(), false) + response.Authorizations[2] = createAuthorization(iamOwnerCtx, Instance, t, Instance.DefaultOrg.GetId(), userResp.GetId(), true) + response.Authorizations[1] = createAuthorization(iamOwnerCtx, Instance, t, Instance.DefaultOrg.GetId(), userResp.GetId(), true) + response.Authorizations[0] = createAuthorization(iamOwnerCtx, Instance, t, Instance.DefaultOrg.GetId(), userResp.GetId(), true) + }, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{{}}, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 6, + AppliedLimit: 100, + }, + Authorizations: []*authorization.Authorization{ + {}, {}, {}, {}, {}, {}, + }, + }, + }, + { + name: "list single id, project and project grant, org owner", + args: args{ + ctx: Instance.WithAuthorizationToken(EmptyCTX, integration.UserTypeOrgOwner), + dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) { + userResp := Instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + + request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_UserId{ + UserId: &filter.IDFilter{ + Id: userResp.GetId(), + }, + } + + response.Authorizations[1] = createAuthorizationForProject(iamOwnerCtx, Instance, t, Instance.DefaultOrg.GetId(), userResp.GetId(), projectResp.GetName(), projectResp.GetId()) + response.Authorizations[0] = createAuthorizationWithProjectGrant(iamOwnerCtx, Instance, t, Instance.DefaultOrg.GetId(), userResp.GetId(), grantedProjectResp.GetName(), grantedProjectResp.GetId()) + }, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{{}}, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 2, + AppliedLimit: 100, + }, + Authorizations: []*authorization.Authorization{ + {}, {}, + }, + }, + }, + { + name: "list single id, project and project grant, project owner", + args: args{ + ctx: projectOwnerCtx, + dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) { + userResp := Instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + + request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_UserId{ + UserId: &filter.IDFilter{ + Id: userResp.GetId(), + }, + } + + response.Authorizations[0] = createAuthorizationForProject(iamOwnerCtx, Instance, t, Instance.DefaultOrg.GetId(), userResp.GetId(), projectResp.GetName(), projectResp.GetId()) + createAuthorizationWithProjectGrant(iamOwnerCtx, Instance, t, Instance.DefaultOrg.GetId(), userResp.GetId(), grantedProjectResp.GetName(), grantedProjectResp.GetId()) + }, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{{}}, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 2, + AppliedLimit: 100, + }, + Authorizations: []*authorization.Authorization{ + {}, + }, + }, + }, + { + name: "list single id, project and project grant, project grant owner", + args: args{ + ctx: projectGrantOwnerCtx, + dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) { + userResp := Instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + + request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_UserId{ + UserId: &filter.IDFilter{ + Id: userResp.GetId(), + }, + } + + createAuthorizationForProject(iamOwnerCtx, Instance, t, Instance.DefaultOrg.GetId(), userResp.GetId(), projectResp.GetName(), projectResp.GetId()) + response.Authorizations[0] = createAuthorizationForProjectGrant(iamOwnerCtx, Instance, t, Instance.DefaultOrg.GetId(), userResp.GetId(), grantedProjectResp.GetName(), grantedProjectResp.GetId(), grantedProjectResp.GetGrantedOrganizationId()) + }, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{{}}, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 2, + AppliedLimit: 100, + }, + Authorizations: []*authorization.Authorization{ + {}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.args.dep != nil { + tt.args.dep(tt.args.req, tt.want) + } + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(iamOwnerCtx, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, listErr := Instance.Client.AuthorizationV2Beta.ListAuthorizations(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(ttt, listErr) + return + } + require.NoError(ttt, listErr) + + // always first check length, otherwise its failed anyway + if assert.Len(ttt, got.Authorizations, len(tt.want.Authorizations)) { + for i := range tt.want.Authorizations { + assert.EqualExportedValues(ttt, tt.want.Authorizations[i], got.Authorizations[i]) + } + } + assertPaginationResponse(ttt, tt.want.Pagination, got.Pagination) + }, retryDuration, tick, "timeout waiting for expected execution result") + }) + } +} + +func assertPaginationResponse(t *assert.CollectT, expected *filter.PaginationResponse, actual *filter.PaginationResponse) { + assert.Equal(t, expected.AppliedLimit, actual.AppliedLimit) + assert.Equal(t, expected.TotalResult, actual.TotalResult) +} + +func createAuthorization(ctx context.Context, instance *integration.Instance, t *testing.T, orgID, userID string, grant bool) *authorization.Authorization { + projectName := gofakeit.AppName() + projectResp := instance.CreateProject(ctx, t, orgID, projectName, false, false) + + if grant { + return createAuthorizationWithProjectGrant(ctx, instance, t, orgID, userID, projectName, projectResp.GetId()) + } + return createAuthorizationForProject(ctx, instance, t, orgID, userID, projectName, projectResp.GetId()) +} + +func createAuthorizationForProject(ctx context.Context, instance *integration.Instance, t *testing.T, orgID, userID, projectName, projectID string) *authorization.Authorization { + userResp, err := instance.Client.UserV2.GetUserByID(ctx, &user.GetUserByIDRequest{UserId: userID}) + require.NoError(t, err) + + userGrantResp := instance.CreateProjectUserGrant(t, ctx, projectID, userID) + return &authorization.Authorization{ + Id: userGrantResp.GetUserGrantId(), + ProjectId: projectID, + ProjectName: projectName, + ProjectOrganizationId: orgID, + OrganizationId: orgID, + CreationDate: userGrantResp.Details.GetCreationDate(), + ChangeDate: userGrantResp.Details.GetCreationDate(), + State: 1, + User: &authorization.User{ + Id: userID, + PreferredLoginName: userResp.User.GetPreferredLoginName(), + DisplayName: userResp.User.GetHuman().GetProfile().GetDisplayName(), + AvatarUrl: userResp.User.GetHuman().GetProfile().GetAvatarUrl(), + OrganizationId: userResp.GetUser().GetDetails().GetResourceOwner(), + }, + } +} + +func createAuthorizationWithProjectGrant(ctx context.Context, instance *integration.Instance, t *testing.T, orgID, userID, projectName, projectID string) *authorization.Authorization { + grantedOrgName := gofakeit.Company() + integration.RandString(10) + grantedOrg := instance.CreateOrganization(ctx, grantedOrgName, gofakeit.Email()) + instance.CreateProjectGrant(ctx, t, projectID, grantedOrg.GetOrganizationId()) + + return createAuthorizationForProjectGrant(ctx, instance, t, orgID, userID, projectName, projectID, grantedOrg.GetOrganizationId()) +} + +func createAuthorizationForProjectGrant(ctx context.Context, instance *integration.Instance, t *testing.T, orgID, userID, projectName, projectID, grantedOrgID string) *authorization.Authorization { + userResp, err := instance.Client.UserV2.GetUserByID(ctx, &user.GetUserByIDRequest{UserId: userID}) + require.NoError(t, err) + + userGrantResp := instance.CreateProjectGrantUserGrant(ctx, orgID, projectID, grantedOrgID, userID) + return &authorization.Authorization{ + Id: userGrantResp.GetUserGrantId(), + ProjectId: projectID, + ProjectName: projectName, + ProjectOrganizationId: orgID, + ProjectGrantId: gu.Ptr(grantedOrgID), + GrantedOrganizationId: gu.Ptr(grantedOrgID), + OrganizationId: orgID, + CreationDate: userGrantResp.Details.GetCreationDate(), + ChangeDate: userGrantResp.Details.GetCreationDate(), + State: 1, + User: &authorization.User{ + Id: userID, + PreferredLoginName: userResp.User.GetPreferredLoginName(), + DisplayName: userResp.User.GetHuman().GetProfile().GetDisplayName(), + AvatarUrl: userResp.User.GetHuman().GetProfile().GetAvatarUrl(), + OrganizationId: userResp.GetUser().GetDetails().GetResourceOwner(), + }, + } +} + +func createProject(ctx context.Context, instance *integration.Instance, t *testing.T, orgID string, projectRoleCheck, hasProjectCheck bool) *project.Project { + name := gofakeit.AppName() + resp := instance.CreateProject(ctx, t, orgID, name, projectRoleCheck, hasProjectCheck) + return &project.Project{ + Id: resp.GetId(), + Name: name, + OrganizationId: orgID, + CreationDate: resp.GetCreationDate(), + ChangeDate: resp.GetCreationDate(), + State: 1, + ProjectRoleAssertion: false, + ProjectAccessRequired: hasProjectCheck, + AuthorizationRequired: projectRoleCheck, + PrivateLabelingSetting: project.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_UNSPECIFIED, + } +} + +func createGrantedProject(ctx context.Context, instance *integration.Instance, t *testing.T, projectToGrant *project.Project) *project.Project { + grantedOrgName := gofakeit.AppName() + grantedOrg := instance.CreateOrganization(ctx, grantedOrgName, gofakeit.Email()) + projectGrantResp := instance.CreateProjectGrant(ctx, t, projectToGrant.GetId(), grantedOrg.GetOrganizationId()) + + return &project.Project{ + Id: projectToGrant.GetId(), + Name: projectToGrant.GetName(), + OrganizationId: projectToGrant.GetOrganizationId(), + CreationDate: projectGrantResp.GetCreationDate(), + ChangeDate: projectGrantResp.GetCreationDate(), + State: 1, + ProjectRoleAssertion: false, + ProjectAccessRequired: projectToGrant.GetProjectAccessRequired(), + AuthorizationRequired: projectToGrant.GetAuthorizationRequired(), + PrivateLabelingSetting: project.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_UNSPECIFIED, + GrantedOrganizationId: gu.Ptr(grantedOrg.GetOrganizationId()), + GrantedOrganizationName: gu.Ptr(grantedOrgName), + GrantedState: 1, + } +} + +func TestServer_ListAuthorizations_PermissionsV2(t *testing.T) { + // removed as permission v2 is not implemented yet for project grant level permissions + // ensureFeaturePermissionV2Enabled(t, InstancePermissionV2) + iamOwnerCtx := InstancePermissionV2.WithAuthorizationToken(EmptyCTX, integration.UserTypeIAMOwner) + + projectOwnerResp := InstancePermissionV2.CreateMachineUser(iamOwnerCtx) + projectOwnerPatResp := InstancePermissionV2.CreatePersonalAccessToken(iamOwnerCtx, projectOwnerResp.GetUserId()) + projectResp := createProject(iamOwnerCtx, InstancePermissionV2, t, InstancePermissionV2.DefaultOrg.GetId(), false, false) + InstancePermissionV2.CreateProjectMembership(t, iamOwnerCtx, projectResp.GetId(), projectOwnerResp.GetUserId()) + projectOwnerCtx := integration.WithAuthorizationToken(EmptyCTX, projectOwnerPatResp.Token) + + projectGrantOwnerResp := InstancePermissionV2.CreateMachineUser(iamOwnerCtx) + projectGrantOwnerPatResp := InstancePermissionV2.CreatePersonalAccessToken(iamOwnerCtx, projectGrantOwnerResp.GetUserId()) + grantedProjectResp := createGrantedProject(iamOwnerCtx, InstancePermissionV2, t, projectResp) + InstancePermissionV2.CreateProjectGrantMembership(t, iamOwnerCtx, projectResp.GetId(), grantedProjectResp.GetGrantedOrganizationId(), projectGrantOwnerResp.GetUserId()) + projectGrantOwnerCtx := integration.WithAuthorizationToken(EmptyCTX, projectGrantOwnerPatResp.Token) + + type args struct { + ctx context.Context + dep func(*authorization.ListAuthorizationsRequest, *authorization.ListAuthorizationsResponse) + req *authorization.ListAuthorizationsRequest + } + tests := []struct { + name string + args args + want *authorization.ListAuthorizationsResponse + wantErr bool + }{ + { + name: "list by user id, unauthenticated", + args: args{ + ctx: EmptyCTX, + dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) { + userResp := InstancePermissionV2.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + + request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_UserId{ + UserId: &filter.IDFilter{ + Id: userResp.GetId(), + }, + } + createAuthorization(iamOwnerCtx, InstancePermissionV2, t, InstancePermissionV2.DefaultOrg.GetId(), userResp.GetId(), false) + }, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{{}}, + }, + }, + wantErr: true, + }, + { + name: "list by id, no permission", + args: args{ + ctx: InstancePermissionV2.WithAuthorizationToken(EmptyCTX, integration.UserTypeNoPermission), + dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) { + userResp := InstancePermissionV2.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + + request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_UserId{ + UserId: &filter.IDFilter{ + Id: userResp.GetId(), + }, + } + createAuthorization(iamOwnerCtx, InstancePermissionV2, t, InstancePermissionV2.DefaultOrg.GetId(), userResp.GetId(), false) + }, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{{}}, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Authorizations: []*authorization.Authorization{}, + }, + }, + { + name: "list, not found", + args: args{ + ctx: iamOwnerCtx, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{ + {Filter: &authorization.AuthorizationsSearchFilter_UserId{ + UserId: &filter.IDFilter{ + Id: "notexisting", + }, + }, + }, + }, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 0, + AppliedLimit: 100, + }, + }, + }, + { + name: "list single id, project", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) { + userResp := InstancePermissionV2.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + + request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_UserId{ + UserId: &filter.IDFilter{ + Id: userResp.GetId(), + }, + } + response.Authorizations[0] = createAuthorization(iamOwnerCtx, InstancePermissionV2, t, InstancePermissionV2.DefaultOrg.GetId(), userResp.GetId(), false) + }, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{{}}, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Authorizations: []*authorization.Authorization{ + {}, + }, + }, + }, + { + name: "list single id", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) { + userResp := InstancePermissionV2.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + + resp := createAuthorization(iamOwnerCtx, InstancePermissionV2, t, InstancePermissionV2.DefaultOrg.GetId(), userResp.GetId(), false) + request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_AuthorizationIds{ + AuthorizationIds: &filter.InIDsFilter{ + Ids: []string{resp.GetId()}, + }, + } + response.Authorizations[0] = resp + }, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{{}}, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Authorizations: []*authorization.Authorization{ + {}, + }, + }, + }, + { + name: "list single project id", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) { + userResp := InstancePermissionV2.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + + resp := createAuthorization(iamOwnerCtx, InstancePermissionV2, t, InstancePermissionV2.DefaultOrg.GetId(), userResp.GetId(), false) + request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_ProjectId{ + ProjectId: &filter.IDFilter{ + Id: resp.GetProjectId(), + }, + } + response.Authorizations[0] = resp + }, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{{}}, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Authorizations: []*authorization.Authorization{ + {}, + }, + }, + }, + { + name: "list single project name", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) { + userResp := InstancePermissionV2.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + + resp := createAuthorization(iamOwnerCtx, InstancePermissionV2, t, InstancePermissionV2.DefaultOrg.GetId(), userResp.GetId(), false) + request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_ProjectName{ + ProjectName: &authorization.ProjectNameQuery{ + Name: resp.GetProjectName(), + Method: filter.TextFilterMethod_TEXT_FILTER_METHOD_EQUALS, + }, + } + response.Authorizations[0] = resp + }, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{{}}, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Authorizations: []*authorization.Authorization{ + {}, + }, + }, + }, + { + name: "list single id, project grant", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) { + userResp := InstancePermissionV2.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + + request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_UserId{ + UserId: &filter.IDFilter{ + Id: userResp.GetId(), + }, + } + response.Authorizations[0] = createAuthorization(iamOwnerCtx, InstancePermissionV2, t, InstancePermissionV2.DefaultOrg.GetId(), userResp.GetId(), true) + }, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{{}}, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Authorizations: []*authorization.Authorization{ + {}, + }, + }, + }, + { + name: "list single grant id, project grant", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) { + userResp := InstancePermissionV2.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + + resp := createAuthorization(iamOwnerCtx, InstancePermissionV2, t, InstancePermissionV2.DefaultOrg.GetId(), userResp.GetId(), true) + request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_ProjectGrantId{ + ProjectGrantId: &filter.IDFilter{ + Id: resp.GetProjectGrantId(), + }, + } + response.Authorizations[0] = resp + }, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{{}}, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Authorizations: []*authorization.Authorization{ + {}, + }, + }, + }, + { + name: "list single id, project and project grant, multiple", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) { + userResp := InstancePermissionV2.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + + request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_UserId{ + UserId: &filter.IDFilter{ + Id: userResp.GetId(), + }, + } + response.Authorizations[5] = createAuthorization(iamOwnerCtx, InstancePermissionV2, t, InstancePermissionV2.DefaultOrg.GetId(), userResp.GetId(), false) + response.Authorizations[4] = createAuthorization(iamOwnerCtx, InstancePermissionV2, t, InstancePermissionV2.DefaultOrg.GetId(), userResp.GetId(), false) + response.Authorizations[3] = createAuthorization(iamOwnerCtx, InstancePermissionV2, t, InstancePermissionV2.DefaultOrg.GetId(), userResp.GetId(), false) + response.Authorizations[2] = createAuthorization(iamOwnerCtx, InstancePermissionV2, t, InstancePermissionV2.DefaultOrg.GetId(), userResp.GetId(), true) + response.Authorizations[1] = createAuthorization(iamOwnerCtx, InstancePermissionV2, t, InstancePermissionV2.DefaultOrg.GetId(), userResp.GetId(), true) + response.Authorizations[0] = createAuthorization(iamOwnerCtx, InstancePermissionV2, t, InstancePermissionV2.DefaultOrg.GetId(), userResp.GetId(), true) + }, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{{}}, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 6, + AppliedLimit: 100, + }, + Authorizations: []*authorization.Authorization{ + {}, {}, {}, {}, {}, {}, + }, + }, + }, + { + name: "list single id, project and project grant, org owner", + args: args{ + ctx: InstancePermissionV2.WithAuthorizationToken(EmptyCTX, integration.UserTypeOrgOwner), + dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) { + userResp := InstancePermissionV2.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + + request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_UserId{ + UserId: &filter.IDFilter{ + Id: userResp.GetId(), + }, + } + + response.Authorizations[1] = createAuthorizationForProject(iamOwnerCtx, InstancePermissionV2, t, InstancePermissionV2.DefaultOrg.GetId(), userResp.GetId(), projectResp.GetName(), projectResp.GetId()) + response.Authorizations[0] = createAuthorizationWithProjectGrant(iamOwnerCtx, InstancePermissionV2, t, InstancePermissionV2.DefaultOrg.GetId(), userResp.GetId(), grantedProjectResp.GetName(), grantedProjectResp.GetId()) + }, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{{}}, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 2, + AppliedLimit: 100, + }, + Authorizations: []*authorization.Authorization{ + {}, {}, + }, + }, + }, + { + name: "list single id, project and project grant, project owner", + args: args{ + ctx: projectOwnerCtx, + dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) { + userResp := InstancePermissionV2.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + + request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_UserId{ + UserId: &filter.IDFilter{ + Id: userResp.GetId(), + }, + } + + response.Authorizations[0] = createAuthorizationForProject(iamOwnerCtx, InstancePermissionV2, t, InstancePermissionV2.DefaultOrg.GetId(), userResp.GetId(), projectResp.GetName(), projectResp.GetId()) + createAuthorizationWithProjectGrant(iamOwnerCtx, InstancePermissionV2, t, InstancePermissionV2.DefaultOrg.GetId(), userResp.GetId(), grantedProjectResp.GetName(), grantedProjectResp.GetId()) + }, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{{}}, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 2, + AppliedLimit: 100, + }, + Authorizations: []*authorization.Authorization{ + {}, + }, + }, + }, + { + name: "list single id, project and project grant, project grant owner", + args: args{ + ctx: projectGrantOwnerCtx, + dep: func(request *authorization.ListAuthorizationsRequest, response *authorization.ListAuthorizationsResponse) { + userResp := InstancePermissionV2.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + + request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_UserId{ + UserId: &filter.IDFilter{ + Id: userResp.GetId(), + }, + } + + createAuthorizationForProject(iamOwnerCtx, InstancePermissionV2, t, InstancePermissionV2.DefaultOrg.GetId(), userResp.GetId(), projectResp.GetName(), projectResp.GetId()) + response.Authorizations[0] = createAuthorizationForProjectGrant(iamOwnerCtx, InstancePermissionV2, t, InstancePermissionV2.DefaultOrg.GetId(), userResp.GetId(), grantedProjectResp.GetName(), grantedProjectResp.GetId(), grantedProjectResp.GetGrantedOrganizationId()) + }, + req: &authorization.ListAuthorizationsRequest{ + Filters: []*authorization.AuthorizationsSearchFilter{{}}, + }, + }, + want: &authorization.ListAuthorizationsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 2, + AppliedLimit: 100, + }, + Authorizations: []*authorization.Authorization{ + {}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.args.dep != nil { + tt.args.dep(tt.args.req, tt.want) + } + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(iamOwnerCtx, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, listErr := InstancePermissionV2.Client.AuthorizationV2Beta.ListAuthorizations(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(ttt, listErr) + return + } + require.NoError(ttt, listErr) + + // always first check length, otherwise its failed anyway + if assert.Len(ttt, got.Authorizations, len(tt.want.Authorizations)) { + for i := range tt.want.Authorizations { + assert.EqualExportedValues(ttt, tt.want.Authorizations[i], got.Authorizations[i]) + } + } + assertPaginationResponse(ttt, tt.want.Pagination, got.Pagination) + }, retryDuration, tick, "timeout waiting for expected execution result") + }) + } +} diff --git a/internal/api/grpc/authorization/v2beta/integration_test/server_test.go b/internal/api/grpc/authorization/v2beta/integration_test/server_test.go new file mode 100644 index 0000000000..fc59713708 --- /dev/null +++ b/internal/api/grpc/authorization/v2beta/integration_test/server_test.go @@ -0,0 +1,65 @@ +//go:build integration + +package authorization_test + +import ( + "context" + "os" + "testing" + "time" + + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/internal/integration" + "github.com/zitadel/zitadel/pkg/grpc/feature/v2" +) + +var ( + EmptyCTX context.Context + IAMCTX context.Context + Instance *integration.Instance + InstancePermissionV2 *integration.Instance +) + +func TestMain(m *testing.M) { + os.Exit(func() int { + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) + defer cancel() + EmptyCTX = ctx + Instance = integration.NewInstance(ctx) + IAMCTX = Instance.WithAuthorizationToken(ctx, integration.UserTypeIAMOwner) + InstancePermissionV2 = integration.NewInstance(ctx) + return m.Run() + }()) +} + +func ensureFeaturePermissionV2Enabled(t *testing.T, instance *integration.Instance) { + ctx := instance.WithAuthorizationToken(EmptyCTX, integration.UserTypeIAMOwner) + f, err := instance.Client.FeatureV2.GetInstanceFeatures(ctx, &feature.GetInstanceFeaturesRequest{ + Inheritance: true, + }) + require.NoError(t, err) + if f.PermissionCheckV2.GetEnabled() { + return + } + _, err = instance.Client.FeatureV2.SetInstanceFeatures(ctx, &feature.SetInstanceFeaturesRequest{ + PermissionCheckV2: gu.Ptr(true), + }) + require.NoError(t, err) + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, 5*time.Minute) + require.EventuallyWithT(t, + func(ttt *assert.CollectT) { + f, err := instance.Client.FeatureV2.GetInstanceFeatures(ctx, &feature.GetInstanceFeaturesRequest{ + Inheritance: true, + }) + assert.NoError(ttt, err) + if f.PermissionCheckV2.GetEnabled() { + return + } + }, + retryDuration, + tick, + "timed out waiting for ensuring instance feature") +} diff --git a/internal/api/grpc/authorization/v2beta/query.go b/internal/api/grpc/authorization/v2beta/query.go new file mode 100644 index 0000000000..75c3d67178 --- /dev/null +++ b/internal/api/grpc/authorization/v2beta/query.go @@ -0,0 +1,208 @@ +package authorization + +import ( + "context" + "errors" + + "connectrpc.com/connect" + "google.golang.org/protobuf/types/known/timestamppb" + + filter "github.com/zitadel/zitadel/internal/api/grpc/filter/v2beta" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/query" + authorization "github.com/zitadel/zitadel/pkg/grpc/authorization/v2beta" + filter_pb "github.com/zitadel/zitadel/pkg/grpc/filter/v2beta" +) + +func (s *Server) ListAuthorizations(ctx context.Context, req *connect.Request[authorization.ListAuthorizationsRequest]) (*connect.Response[authorization.ListAuthorizationsResponse], error) { + queries, err := s.listAuthorizationsRequestToModel(req.Msg) + if err != nil { + return nil, err + } + resp, err := s.query.UserGrants(ctx, queries, false, s.checkPermission) + if err != nil { + return nil, err + } + return connect.NewResponse(&authorization.ListAuthorizationsResponse{ + Authorizations: userGrantsToPb(resp.UserGrants), + Pagination: filter.QueryToPaginationPb(queries.SearchRequest, resp.SearchResponse), + }), nil +} + +func (s *Server) listAuthorizationsRequestToModel(req *authorization.ListAuthorizationsRequest) (*query.UserGrantsQueries, error) { + offset, limit, asc, err := filter.PaginationPbToQuery(s.systemDefaults, req.Pagination) + if err != nil { + return nil, err + } + queries, err := AuthorizationQueriesToQuery(req.Filters) + if err != nil { + return nil, err + } + return &query.UserGrantsQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + SortingColumn: authorizationFieldNameToSortingColumn(req.GetSortingColumn()), + }, + Queries: queries, + }, nil +} + +func authorizationFieldNameToSortingColumn(field authorization.AuthorizationFieldName) query.Column { + switch field { + case authorization.AuthorizationFieldName_AUTHORIZATION_FIELD_NAME_UNSPECIFIED: + return query.UserGrantCreationDate + case authorization.AuthorizationFieldName_AUTHORIZATION_FIELD_NAME_CREATED_DATE: + return query.UserGrantCreationDate + case authorization.AuthorizationFieldName_AUTHORIZATION_FIELD_NAME_CHANGED_DATE: + return query.UserGrantChangeDate + case authorization.AuthorizationFieldName_AUTHORIZATION_FIELD_NAME_ID: + return query.UserGrantID + case authorization.AuthorizationFieldName_AUTHORIZATION_FIELD_NAME_USER_ID: + return query.UserGrantUserID + case authorization.AuthorizationFieldName_AUTHORIZATION_FIELD_NAME_PROJECT_ID: + return query.UserGrantProjectID + case authorization.AuthorizationFieldName_AUTHORIZATION_FIELD_NAME_ORGANIZATION_ID: + return query.UserGrantResourceOwner + case authorization.AuthorizationFieldName_AUTHORIZATION_FIELD_NAME_USER_ORGANIZATION_ID: + return query.UserResourceOwnerCol + default: + return query.UserGrantCreationDate + } +} + +func AuthorizationQueriesToQuery(queries []*authorization.AuthorizationsSearchFilter) (q []query.SearchQuery, err error) { + q = make([]query.SearchQuery, len(queries)) + for i, query := range queries { + q[i], err = AuthorizationSearchFilterToQuery(query) + if err != nil { + return nil, err + } + } + return q, nil +} + +func AuthorizationSearchFilterToQuery(query *authorization.AuthorizationsSearchFilter) (query.SearchQuery, error) { + switch q := query.Filter.(type) { + case *authorization.AuthorizationsSearchFilter_AuthorizationIds: + return AuthorizationIDQueryToModel(q.AuthorizationIds) + case *authorization.AuthorizationsSearchFilter_OrganizationId: + return AuthorizationOrganizationIDQueryToModel(q.OrganizationId) + case *authorization.AuthorizationsSearchFilter_State: + return AuthorizationStateQueryToModel(q.State) + case *authorization.AuthorizationsSearchFilter_UserId: + return AuthorizationUserUserIDQueryToModel(q.UserId) + case *authorization.AuthorizationsSearchFilter_UserOrganizationId: + return AuthorizationUserOrganizationIDQueryToModel(q.UserOrganizationId) + case *authorization.AuthorizationsSearchFilter_UserPreferredLoginName: + return AuthorizationUserNameQueryToModel(q.UserPreferredLoginName) + case *authorization.AuthorizationsSearchFilter_UserDisplayName: + return AuthorizationDisplayNameQueryToModel(q.UserDisplayName) + case *authorization.AuthorizationsSearchFilter_ProjectId: + return AuthorizationProjectIDQueryToModel(q.ProjectId) + case *authorization.AuthorizationsSearchFilter_ProjectName: + return AuthorizationProjectNameQueryToModel(q.ProjectName) + case *authorization.AuthorizationsSearchFilter_RoleKey: + return AuthorizationRoleKeyQueryToModel(q.RoleKey) + case *authorization.AuthorizationsSearchFilter_ProjectGrantId: + return AuthorizationProjectGrantIDQueryToModel(q.ProjectGrantId) + default: + return nil, errors.New("invalid query") + } +} + +func AuthorizationIDQueryToModel(q *filter_pb.InIDsFilter) (query.SearchQuery, error) { + return query.NewUserGrantInIDsSearchQuery(q.Ids) +} + +func AuthorizationDisplayNameQueryToModel(q *authorization.UserDisplayNameQuery) (query.SearchQuery, error) { + return query.NewUserGrantDisplayNameQuery(q.DisplayName, filter.TextMethodPbToQuery(q.Method)) +} + +func AuthorizationOrganizationIDQueryToModel(q *filter_pb.IDFilter) (query.SearchQuery, error) { + return query.NewUserGrantResourceOwnerSearchQuery(q.Id) +} + +func AuthorizationProjectIDQueryToModel(q *filter_pb.IDFilter) (query.SearchQuery, error) { + return query.NewUserGrantProjectIDSearchQuery(q.Id) +} + +func AuthorizationProjectNameQueryToModel(q *authorization.ProjectNameQuery) (query.SearchQuery, error) { + return query.NewUserGrantProjectNameQuery(q.Name, filter.TextMethodPbToQuery(q.Method)) +} + +func AuthorizationProjectGrantIDQueryToModel(q *filter_pb.IDFilter) (query.SearchQuery, error) { + return query.NewUserGrantGrantIDSearchQuery(q.Id) +} + +func AuthorizationRoleKeyQueryToModel(q *authorization.RoleKeyQuery) (query.SearchQuery, error) { + return query.NewUserGrantRoleQuery(q.Key) +} + +func AuthorizationUserNameQueryToModel(q *authorization.UserPreferredLoginNameQuery) (query.SearchQuery, error) { + return query.NewUserGrantUsernameQuery(q.LoginName, filter.TextMethodPbToQuery(q.Method)) +} + +func AuthorizationUserUserIDQueryToModel(q *filter_pb.IDFilter) (query.SearchQuery, error) { + return query.NewUserGrantUserIDSearchQuery(q.Id) +} + +func AuthorizationUserOrganizationIDQueryToModel(q *filter_pb.IDFilter) (query.SearchQuery, error) { + return query.NewUserGrantUserResourceOwnerSearchQuery(q.Id) +} + +func AuthorizationStateQueryToModel(q *authorization.StateQuery) (query.SearchQuery, error) { + return query.NewUserGrantStateQuery(domain.UserGrantState(q.State)) +} + +func userGrantsToPb(userGrants []*query.UserGrant) []*authorization.Authorization { + o := make([]*authorization.Authorization, len(userGrants)) + for i, grant := range userGrants { + o[i] = userGrantToPb(grant) + } + return o +} + +func userGrantToPb(userGrant *query.UserGrant) *authorization.Authorization { + var grantID, grantedOrgID *string + if userGrant.GrantID != "" { + grantID = &userGrant.GrantID + } + if userGrant.GrantedOrgID != "" { + grantedOrgID = &userGrant.GrantedOrgID + } + return &authorization.Authorization{ + Id: userGrant.ID, + ProjectId: userGrant.ProjectID, + ProjectName: userGrant.ProjectName, + ProjectOrganizationId: userGrant.ProjectResourceOwner, + ProjectGrantId: grantID, + GrantedOrganizationId: grantedOrgID, + OrganizationId: userGrant.ResourceOwner, + CreationDate: timestamppb.New(userGrant.CreationDate), + ChangeDate: timestamppb.New(userGrant.ChangeDate), + State: userGrantStateToPb(userGrant.State), + User: &authorization.User{ + Id: userGrant.UserID, + PreferredLoginName: userGrant.PreferredLoginName, + DisplayName: userGrant.DisplayName, + AvatarUrl: userGrant.AvatarURL, + OrganizationId: userGrant.UserResourceOwner, + }, + Roles: userGrant.Roles, + } +} + +func userGrantStateToPb(state domain.UserGrantState) authorization.State { + switch state { + case domain.UserGrantStateActive: + return authorization.State_STATE_ACTIVE + case domain.UserGrantStateInactive: + return authorization.State_STATE_INACTIVE + case domain.UserGrantStateUnspecified, domain.UserGrantStateRemoved: + return authorization.State_STATE_UNSPECIFIED + default: + return authorization.State_STATE_UNSPECIFIED + } +} diff --git a/internal/api/grpc/authorization/v2beta/server.go b/internal/api/grpc/authorization/v2beta/server.go new file mode 100644 index 0000000000..4d66309d2a --- /dev/null +++ b/internal/api/grpc/authorization/v2beta/server.go @@ -0,0 +1,67 @@ +package authorization + +import ( + "net/http" + + "connectrpc.com/connect" + "google.golang.org/protobuf/reflect/protoreflect" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/api/grpc/server" + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/config/systemdefaults" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/query" + authorization "github.com/zitadel/zitadel/pkg/grpc/authorization/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/authorization/v2beta/authorizationconnect" +) + +var _ authorizationconnect.AuthorizationServiceHandler = (*Server)(nil) + +type Server struct { + systemDefaults systemdefaults.SystemDefaults + command *command.Commands + query *query.Queries + + checkPermission domain.PermissionCheck +} + +type Config struct{} + +func CreateServer( + systemDefaults systemdefaults.SystemDefaults, + command *command.Commands, + query *query.Queries, + checkPermission domain.PermissionCheck, +) *Server { + return &Server{ + systemDefaults: systemDefaults, + command: command, + query: query, + checkPermission: checkPermission, + } +} + +func (s *Server) RegisterConnectServer(interceptors ...connect.Interceptor) (string, http.Handler) { + return authorizationconnect.NewAuthorizationServiceHandler(s, connect.WithInterceptors(interceptors...)) +} + +func (s *Server) FileDescriptor() protoreflect.FileDescriptor { + return authorization.File_zitadel_authorization_v2beta_authorization_service_proto +} + +func (s *Server) AppName() string { + return authorization.AuthorizationService_ServiceDesc.ServiceName +} + +func (s *Server) MethodPrefix() string { + return authorization.AuthorizationService_ServiceDesc.ServiceName +} + +func (s *Server) AuthMethods() authz.MethodMapping { + return authorization.AuthorizationService_AuthMethods +} + +func (s *Server) RegisterGateway() server.RegisterGatewayFunc { + return authorization.RegisterAuthorizationServiceHandler +} diff --git a/internal/api/grpc/feature/v2/converter.go b/internal/api/grpc/feature/v2/converter.go index e146ac2db6..ab8ddc7d75 100644 --- a/internal/api/grpc/feature/v2/converter.go +++ b/internal/api/grpc/feature/v2/converter.go @@ -18,34 +18,30 @@ func systemFeaturesToCommand(req *feature_pb.SetSystemFeaturesRequest) (*command return nil, err } return &command.SystemFeatures{ - LoginDefaultOrg: req.LoginDefaultOrg, - TriggerIntrospectionProjections: req.OidcTriggerIntrospectionProjections, - LegacyIntrospection: req.OidcLegacyIntrospection, - UserSchema: req.UserSchema, - TokenExchange: req.OidcTokenExchange, - ImprovedPerformance: improvedPerformanceListToDomain(req.ImprovedPerformance), - OIDCSingleV1SessionTermination: req.OidcSingleV1SessionTermination, - DisableUserTokenEvent: req.DisableUserTokenEvent, - EnableBackChannelLogout: req.EnableBackChannelLogout, - LoginV2: loginV2, - PermissionCheckV2: req.PermissionCheckV2, + LoginDefaultOrg: req.LoginDefaultOrg, + UserSchema: req.UserSchema, + TokenExchange: req.OidcTokenExchange, + ImprovedPerformance: improvedPerformanceListToDomain(req.ImprovedPerformance), + OIDCSingleV1SessionTermination: req.OidcSingleV1SessionTermination, + DisableUserTokenEvent: req.DisableUserTokenEvent, + EnableBackChannelLogout: req.EnableBackChannelLogout, + LoginV2: loginV2, + PermissionCheckV2: req.PermissionCheckV2, }, nil } func systemFeaturesToPb(f *query.SystemFeatures) *feature_pb.GetSystemFeaturesResponse { return &feature_pb.GetSystemFeaturesResponse{ - Details: object.DomainToDetailsPb(f.Details), - LoginDefaultOrg: featureSourceToFlagPb(&f.LoginDefaultOrg), - OidcTriggerIntrospectionProjections: featureSourceToFlagPb(&f.TriggerIntrospectionProjections), - OidcLegacyIntrospection: featureSourceToFlagPb(&f.LegacyIntrospection), - UserSchema: featureSourceToFlagPb(&f.UserSchema), - OidcTokenExchange: featureSourceToFlagPb(&f.TokenExchange), - ImprovedPerformance: featureSourceToImprovedPerformanceFlagPb(&f.ImprovedPerformance), - OidcSingleV1SessionTermination: featureSourceToFlagPb(&f.OIDCSingleV1SessionTermination), - DisableUserTokenEvent: featureSourceToFlagPb(&f.DisableUserTokenEvent), - EnableBackChannelLogout: featureSourceToFlagPb(&f.EnableBackChannelLogout), - LoginV2: loginV2ToLoginV2FlagPb(f.LoginV2), - PermissionCheckV2: featureSourceToFlagPb(&f.PermissionCheckV2), + Details: object.DomainToDetailsPb(f.Details), + LoginDefaultOrg: featureSourceToFlagPb(&f.LoginDefaultOrg), + UserSchema: featureSourceToFlagPb(&f.UserSchema), + OidcTokenExchange: featureSourceToFlagPb(&f.TokenExchange), + ImprovedPerformance: featureSourceToImprovedPerformanceFlagPb(&f.ImprovedPerformance), + OidcSingleV1SessionTermination: featureSourceToFlagPb(&f.OIDCSingleV1SessionTermination), + DisableUserTokenEvent: featureSourceToFlagPb(&f.DisableUserTokenEvent), + EnableBackChannelLogout: featureSourceToFlagPb(&f.EnableBackChannelLogout), + LoginV2: loginV2ToLoginV2FlagPb(f.LoginV2), + PermissionCheckV2: featureSourceToFlagPb(&f.PermissionCheckV2), } } @@ -55,40 +51,34 @@ func instanceFeaturesToCommand(req *feature_pb.SetInstanceFeaturesRequest) (*com return nil, err } return &command.InstanceFeatures{ - LoginDefaultOrg: req.LoginDefaultOrg, - TriggerIntrospectionProjections: req.OidcTriggerIntrospectionProjections, - LegacyIntrospection: req.OidcLegacyIntrospection, - UserSchema: req.UserSchema, - TokenExchange: req.OidcTokenExchange, - ImprovedPerformance: improvedPerformanceListToDomain(req.ImprovedPerformance), - WebKey: req.WebKey, - DebugOIDCParentError: req.DebugOidcParentError, - OIDCSingleV1SessionTermination: req.OidcSingleV1SessionTermination, - DisableUserTokenEvent: req.DisableUserTokenEvent, - EnableBackChannelLogout: req.EnableBackChannelLogout, - LoginV2: loginV2, - PermissionCheckV2: req.PermissionCheckV2, - ConsoleUseV2UserApi: req.ConsoleUseV2UserApi, + LoginDefaultOrg: req.LoginDefaultOrg, + UserSchema: req.UserSchema, + TokenExchange: req.OidcTokenExchange, + ImprovedPerformance: improvedPerformanceListToDomain(req.ImprovedPerformance), + DebugOIDCParentError: req.DebugOidcParentError, + OIDCSingleV1SessionTermination: req.OidcSingleV1SessionTermination, + DisableUserTokenEvent: req.DisableUserTokenEvent, + EnableBackChannelLogout: req.EnableBackChannelLogout, + LoginV2: loginV2, + PermissionCheckV2: req.PermissionCheckV2, + ConsoleUseV2UserApi: req.ConsoleUseV2UserApi, }, nil } func instanceFeaturesToPb(f *query.InstanceFeatures) *feature_pb.GetInstanceFeaturesResponse { return &feature_pb.GetInstanceFeaturesResponse{ - Details: object.DomainToDetailsPb(f.Details), - LoginDefaultOrg: featureSourceToFlagPb(&f.LoginDefaultOrg), - OidcTriggerIntrospectionProjections: featureSourceToFlagPb(&f.TriggerIntrospectionProjections), - OidcLegacyIntrospection: featureSourceToFlagPb(&f.LegacyIntrospection), - UserSchema: featureSourceToFlagPb(&f.UserSchema), - OidcTokenExchange: featureSourceToFlagPb(&f.TokenExchange), - ImprovedPerformance: featureSourceToImprovedPerformanceFlagPb(&f.ImprovedPerformance), - WebKey: featureSourceToFlagPb(&f.WebKey), - DebugOidcParentError: featureSourceToFlagPb(&f.DebugOIDCParentError), - OidcSingleV1SessionTermination: featureSourceToFlagPb(&f.OIDCSingleV1SessionTermination), - DisableUserTokenEvent: featureSourceToFlagPb(&f.DisableUserTokenEvent), - EnableBackChannelLogout: featureSourceToFlagPb(&f.EnableBackChannelLogout), - LoginV2: loginV2ToLoginV2FlagPb(f.LoginV2), - PermissionCheckV2: featureSourceToFlagPb(&f.PermissionCheckV2), - ConsoleUseV2UserApi: featureSourceToFlagPb(&f.ConsoleUseV2UserApi), + Details: object.DomainToDetailsPb(f.Details), + LoginDefaultOrg: featureSourceToFlagPb(&f.LoginDefaultOrg), + UserSchema: featureSourceToFlagPb(&f.UserSchema), + OidcTokenExchange: featureSourceToFlagPb(&f.TokenExchange), + ImprovedPerformance: featureSourceToImprovedPerformanceFlagPb(&f.ImprovedPerformance), + DebugOidcParentError: featureSourceToFlagPb(&f.DebugOIDCParentError), + OidcSingleV1SessionTermination: featureSourceToFlagPb(&f.OIDCSingleV1SessionTermination), + DisableUserTokenEvent: featureSourceToFlagPb(&f.DisableUserTokenEvent), + EnableBackChannelLogout: featureSourceToFlagPb(&f.EnableBackChannelLogout), + LoginV2: loginV2ToLoginV2FlagPb(f.LoginV2), + PermissionCheckV2: featureSourceToFlagPb(&f.PermissionCheckV2), + ConsoleUseV2UserApi: featureSourceToFlagPb(&f.ConsoleUseV2UserApi), } } diff --git a/internal/api/grpc/feature/v2/converter_test.go b/internal/api/grpc/feature/v2/converter_test.go index f481e4f65a..7b11fc0d17 100644 --- a/internal/api/grpc/feature/v2/converter_test.go +++ b/internal/api/grpc/feature/v2/converter_test.go @@ -19,26 +19,22 @@ import ( func Test_systemFeaturesToCommand(t *testing.T) { arg := &feature_pb.SetSystemFeaturesRequest{ - LoginDefaultOrg: gu.Ptr(true), - OidcTriggerIntrospectionProjections: gu.Ptr(false), - OidcLegacyIntrospection: nil, - UserSchema: gu.Ptr(true), - OidcTokenExchange: gu.Ptr(true), - ImprovedPerformance: nil, - OidcSingleV1SessionTermination: gu.Ptr(true), + LoginDefaultOrg: gu.Ptr(true), + UserSchema: gu.Ptr(true), + OidcTokenExchange: gu.Ptr(true), + ImprovedPerformance: nil, + OidcSingleV1SessionTermination: gu.Ptr(true), LoginV2: &feature_pb.LoginV2{ Required: true, BaseUri: gu.Ptr("https://login.com"), }, } want := &command.SystemFeatures{ - LoginDefaultOrg: gu.Ptr(true), - TriggerIntrospectionProjections: gu.Ptr(false), - LegacyIntrospection: nil, - UserSchema: gu.Ptr(true), - TokenExchange: gu.Ptr(true), - ImprovedPerformance: nil, - OIDCSingleV1SessionTermination: gu.Ptr(true), + LoginDefaultOrg: gu.Ptr(true), + UserSchema: gu.Ptr(true), + TokenExchange: gu.Ptr(true), + ImprovedPerformance: nil, + OIDCSingleV1SessionTermination: gu.Ptr(true), LoginV2: &feature.LoginV2{ Required: true, BaseURI: &url.URL{Scheme: "https", Host: "login.com"}, @@ -60,14 +56,6 @@ func Test_systemFeaturesToPb(t *testing.T) { Level: feature.LevelSystem, Value: true, }, - TriggerIntrospectionProjections: query.FeatureSource[bool]{ - Level: feature.LevelUnspecified, - Value: false, - }, - LegacyIntrospection: query.FeatureSource[bool]{ - Level: feature.LevelSystem, - Value: true, - }, UserSchema: query.FeatureSource[bool]{ Level: feature.LevelSystem, Value: true, @@ -110,14 +98,6 @@ func Test_systemFeaturesToPb(t *testing.T) { Enabled: true, Source: feature_pb.Source_SOURCE_SYSTEM, }, - OidcTriggerIntrospectionProjections: &feature_pb.FeatureFlag{ - Enabled: false, - Source: feature_pb.Source_SOURCE_UNSPECIFIED, - }, - OidcLegacyIntrospection: &feature_pb.FeatureFlag{ - Enabled: true, - Source: feature_pb.Source_SOURCE_SYSTEM, - }, UserSchema: &feature_pb.FeatureFlag{ Enabled: true, Source: feature_pb.Source_SOURCE_SYSTEM, @@ -158,16 +138,13 @@ func Test_systemFeaturesToPb(t *testing.T) { func Test_instanceFeaturesToCommand(t *testing.T) { arg := &feature_pb.SetInstanceFeaturesRequest{ - LoginDefaultOrg: gu.Ptr(true), - OidcTriggerIntrospectionProjections: gu.Ptr(false), - OidcLegacyIntrospection: nil, - UserSchema: gu.Ptr(true), - OidcTokenExchange: gu.Ptr(true), - ImprovedPerformance: nil, - WebKey: gu.Ptr(true), - DebugOidcParentError: gu.Ptr(true), - OidcSingleV1SessionTermination: gu.Ptr(true), - EnableBackChannelLogout: gu.Ptr(true), + LoginDefaultOrg: gu.Ptr(true), + UserSchema: gu.Ptr(true), + OidcTokenExchange: gu.Ptr(true), + ImprovedPerformance: nil, + DebugOidcParentError: gu.Ptr(true), + OidcSingleV1SessionTermination: gu.Ptr(true), + EnableBackChannelLogout: gu.Ptr(true), LoginV2: &feature_pb.LoginV2{ Required: true, BaseUri: gu.Ptr("https://login.com"), @@ -175,16 +152,13 @@ func Test_instanceFeaturesToCommand(t *testing.T) { ConsoleUseV2UserApi: gu.Ptr(true), } want := &command.InstanceFeatures{ - LoginDefaultOrg: gu.Ptr(true), - TriggerIntrospectionProjections: gu.Ptr(false), - LegacyIntrospection: nil, - UserSchema: gu.Ptr(true), - TokenExchange: gu.Ptr(true), - ImprovedPerformance: nil, - WebKey: gu.Ptr(true), - DebugOIDCParentError: gu.Ptr(true), - OIDCSingleV1SessionTermination: gu.Ptr(true), - EnableBackChannelLogout: gu.Ptr(true), + LoginDefaultOrg: gu.Ptr(true), + UserSchema: gu.Ptr(true), + TokenExchange: gu.Ptr(true), + ImprovedPerformance: nil, + DebugOIDCParentError: gu.Ptr(true), + OIDCSingleV1SessionTermination: gu.Ptr(true), + EnableBackChannelLogout: gu.Ptr(true), LoginV2: &feature.LoginV2{ Required: true, BaseURI: &url.URL{Scheme: "https", Host: "login.com"}, @@ -207,14 +181,6 @@ func Test_instanceFeaturesToPb(t *testing.T) { Level: feature.LevelSystem, Value: true, }, - TriggerIntrospectionProjections: query.FeatureSource[bool]{ - Level: feature.LevelUnspecified, - Value: false, - }, - LegacyIntrospection: query.FeatureSource[bool]{ - Level: feature.LevelInstance, - Value: true, - }, UserSchema: query.FeatureSource[bool]{ Level: feature.LevelInstance, Value: true, @@ -227,10 +193,6 @@ func Test_instanceFeaturesToPb(t *testing.T) { Level: feature.LevelSystem, Value: []feature.ImprovedPerformanceType{feature.ImprovedPerformanceTypeOrgByID}, }, - WebKey: query.FeatureSource[bool]{ - Level: feature.LevelInstance, - Value: true, - }, OIDCSingleV1SessionTermination: query.FeatureSource[bool]{ Level: feature.LevelInstance, Value: true, @@ -265,14 +227,6 @@ func Test_instanceFeaturesToPb(t *testing.T) { Enabled: true, Source: feature_pb.Source_SOURCE_SYSTEM, }, - OidcTriggerIntrospectionProjections: &feature_pb.FeatureFlag{ - Enabled: false, - Source: feature_pb.Source_SOURCE_UNSPECIFIED, - }, - OidcLegacyIntrospection: &feature_pb.FeatureFlag{ - Enabled: true, - Source: feature_pb.Source_SOURCE_INSTANCE, - }, UserSchema: &feature_pb.FeatureFlag{ Enabled: true, Source: feature_pb.Source_SOURCE_INSTANCE, @@ -285,10 +239,6 @@ func Test_instanceFeaturesToPb(t *testing.T) { ExecutionPaths: []feature_pb.ImprovedPerformance{feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_ORG_BY_ID}, Source: feature_pb.Source_SOURCE_SYSTEM, }, - WebKey: &feature_pb.FeatureFlag{ - Enabled: true, - Source: feature_pb.Source_SOURCE_INSTANCE, - }, DebugOidcParentError: &feature_pb.FeatureFlag{ Enabled: false, Source: feature_pb.Source_SOURCE_UNSPECIFIED, diff --git a/internal/api/grpc/feature/v2/feature.go b/internal/api/grpc/feature/v2/feature.go index f4527689fc..f450f734e4 100644 --- a/internal/api/grpc/feature/v2/feature.go +++ b/internal/api/grpc/feature/v2/feature.go @@ -3,6 +3,7 @@ package feature import ( "context" + "connectrpc.com/connect" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -10,8 +11,8 @@ import ( "github.com/zitadel/zitadel/pkg/grpc/feature/v2" ) -func (s *Server) SetSystemFeatures(ctx context.Context, req *feature.SetSystemFeaturesRequest) (_ *feature.SetSystemFeaturesResponse, err error) { - features, err := systemFeaturesToCommand(req) +func (s *Server) SetSystemFeatures(ctx context.Context, req *connect.Request[feature.SetSystemFeaturesRequest]) (_ *connect.Response[feature.SetSystemFeaturesResponse], err error) { + features, err := systemFeaturesToCommand(req.Msg) if err != nil { return nil, err } @@ -19,31 +20,31 @@ func (s *Server) SetSystemFeatures(ctx context.Context, req *feature.SetSystemFe if err != nil { return nil, err } - return &feature.SetSystemFeaturesResponse{ + return connect.NewResponse(&feature.SetSystemFeaturesResponse{ Details: object.DomainToDetailsPb(details), - }, nil + }), nil } -func (s *Server) ResetSystemFeatures(ctx context.Context, req *feature.ResetSystemFeaturesRequest) (_ *feature.ResetSystemFeaturesResponse, err error) { +func (s *Server) ResetSystemFeatures(ctx context.Context, req *connect.Request[feature.ResetSystemFeaturesRequest]) (_ *connect.Response[feature.ResetSystemFeaturesResponse], err error) { details, err := s.command.ResetSystemFeatures(ctx) if err != nil { return nil, err } - return &feature.ResetSystemFeaturesResponse{ + return connect.NewResponse(&feature.ResetSystemFeaturesResponse{ Details: object.DomainToDetailsPb(details), - }, nil + }), nil } -func (s *Server) GetSystemFeatures(ctx context.Context, req *feature.GetSystemFeaturesRequest) (_ *feature.GetSystemFeaturesResponse, err error) { +func (s *Server) GetSystemFeatures(ctx context.Context, req *connect.Request[feature.GetSystemFeaturesRequest]) (_ *connect.Response[feature.GetSystemFeaturesResponse], err error) { f, err := s.query.GetSystemFeatures(ctx) if err != nil { return nil, err } - return systemFeaturesToPb(f), nil + return connect.NewResponse(systemFeaturesToPb(f)), nil } -func (s *Server) SetInstanceFeatures(ctx context.Context, req *feature.SetInstanceFeaturesRequest) (_ *feature.SetInstanceFeaturesResponse, err error) { - features, err := instanceFeaturesToCommand(req) +func (s *Server) SetInstanceFeatures(ctx context.Context, req *connect.Request[feature.SetInstanceFeaturesRequest]) (_ *connect.Response[feature.SetInstanceFeaturesResponse], err error) { + features, err := instanceFeaturesToCommand(req.Msg) if err != nil { return nil, err } @@ -51,44 +52,44 @@ func (s *Server) SetInstanceFeatures(ctx context.Context, req *feature.SetInstan if err != nil { return nil, err } - return &feature.SetInstanceFeaturesResponse{ + return connect.NewResponse(&feature.SetInstanceFeaturesResponse{ Details: object.DomainToDetailsPb(details), - }, nil + }), nil } -func (s *Server) ResetInstanceFeatures(ctx context.Context, req *feature.ResetInstanceFeaturesRequest) (_ *feature.ResetInstanceFeaturesResponse, err error) { +func (s *Server) ResetInstanceFeatures(ctx context.Context, req *connect.Request[feature.ResetInstanceFeaturesRequest]) (_ *connect.Response[feature.ResetInstanceFeaturesResponse], err error) { details, err := s.command.ResetInstanceFeatures(ctx) if err != nil { return nil, err } - return &feature.ResetInstanceFeaturesResponse{ + return connect.NewResponse(&feature.ResetInstanceFeaturesResponse{ Details: object.DomainToDetailsPb(details), - }, nil + }), nil } -func (s *Server) GetInstanceFeatures(ctx context.Context, req *feature.GetInstanceFeaturesRequest) (_ *feature.GetInstanceFeaturesResponse, err error) { - f, err := s.query.GetInstanceFeatures(ctx, req.GetInheritance()) +func (s *Server) GetInstanceFeatures(ctx context.Context, req *connect.Request[feature.GetInstanceFeaturesRequest]) (_ *connect.Response[feature.GetInstanceFeaturesResponse], err error) { + f, err := s.query.GetInstanceFeatures(ctx, req.Msg.GetInheritance()) if err != nil { return nil, err } - return instanceFeaturesToPb(f), nil + return connect.NewResponse(instanceFeaturesToPb(f)), nil } -func (s *Server) SetOrganizationFeatures(ctx context.Context, req *feature.SetOrganizationFeaturesRequest) (_ *feature.SetOrganizationFeaturesResponse, err error) { +func (s *Server) SetOrganizationFeatures(ctx context.Context, req *connect.Request[feature.SetOrganizationFeaturesRequest]) (_ *connect.Response[feature.SetOrganizationFeaturesResponse], err error) { return nil, status.Errorf(codes.Unimplemented, "method SetOrganizationFeatures not implemented") } -func (s *Server) ResetOrganizationFeatures(ctx context.Context, req *feature.ResetOrganizationFeaturesRequest) (_ *feature.ResetOrganizationFeaturesResponse, err error) { +func (s *Server) ResetOrganizationFeatures(ctx context.Context, req *connect.Request[feature.ResetOrganizationFeaturesRequest]) (_ *connect.Response[feature.ResetOrganizationFeaturesResponse], err error) { return nil, status.Errorf(codes.Unimplemented, "method ResetOrganizationFeatures not implemented") } -func (s *Server) GetOrganizationFeatures(ctx context.Context, req *feature.GetOrganizationFeaturesRequest) (_ *feature.GetOrganizationFeaturesResponse, err error) { +func (s *Server) GetOrganizationFeatures(ctx context.Context, req *connect.Request[feature.GetOrganizationFeaturesRequest]) (_ *connect.Response[feature.GetOrganizationFeaturesResponse], err error) { return nil, status.Errorf(codes.Unimplemented, "method GetOrganizationFeatures not implemented") } -func (s *Server) SetUserFeatures(ctx context.Context, req *feature.SetUserFeatureRequest) (_ *feature.SetUserFeaturesResponse, err error) { +func (s *Server) SetUserFeatures(ctx context.Context, req *connect.Request[feature.SetUserFeatureRequest]) (_ *connect.Response[feature.SetUserFeaturesResponse], err error) { return nil, status.Errorf(codes.Unimplemented, "method SetUserFeatures not implemented") } -func (s *Server) ResetUserFeatures(ctx context.Context, req *feature.ResetUserFeaturesRequest) (_ *feature.ResetUserFeaturesResponse, err error) { +func (s *Server) ResetUserFeatures(ctx context.Context, req *connect.Request[feature.ResetUserFeaturesRequest]) (_ *connect.Response[feature.ResetUserFeaturesResponse], err error) { return nil, status.Errorf(codes.Unimplemented, "method ResetUserFeatures not implemented") } -func (s *Server) GetUserFeatures(ctx context.Context, req *feature.GetUserFeaturesRequest) (_ *feature.GetUserFeaturesResponse, err error) { +func (s *Server) GetUserFeatures(ctx context.Context, req *connect.Request[feature.GetUserFeaturesRequest]) (_ *connect.Response[feature.GetUserFeaturesResponse], err error) { return nil, status.Errorf(codes.Unimplemented, "method GetUserFeatures not implemented") } diff --git a/internal/api/grpc/feature/v2/integration_test/feature_test.go b/internal/api/grpc/feature/v2/integration_test/feature_test.go index f27b57ff8c..369f5b37b8 100644 --- a/internal/api/grpc/feature/v2/integration_test/feature_test.go +++ b/internal/api/grpc/feature/v2/integration_test/feature_test.go @@ -58,7 +58,7 @@ func TestServer_SetSystemFeatures(t *testing.T) { args: args{ ctx: IamCTX, req: &feature.SetSystemFeaturesRequest{ - OidcTriggerIntrospectionProjections: gu.Ptr(true), + LoginDefaultOrg: gu.Ptr(true), }, }, wantErr: true, @@ -76,7 +76,7 @@ func TestServer_SetSystemFeatures(t *testing.T) { args: args{ ctx: SystemCTX, req: &feature.SetSystemFeaturesRequest{ - OidcTriggerIntrospectionProjections: gu.Ptr(true), + LoginDefaultOrg: gu.Ptr(true), }, }, want: &feature.SetSystemFeaturesResponse{ @@ -170,8 +170,8 @@ func TestServer_GetSystemFeatures(t *testing.T) { name: "some features", prepare: func(t *testing.T) { _, err := Client.SetSystemFeatures(SystemCTX, &feature.SetSystemFeaturesRequest{ - LoginDefaultOrg: gu.Ptr(true), - OidcTriggerIntrospectionProjections: gu.Ptr(false), + LoginDefaultOrg: gu.Ptr(true), + UserSchema: gu.Ptr(false), }) require.NoError(t, err) }, @@ -184,7 +184,7 @@ func TestServer_GetSystemFeatures(t *testing.T) { Enabled: true, Source: feature.Source_SOURCE_SYSTEM, }, - OidcTriggerIntrospectionProjections: &feature.FeatureFlag{ + UserSchema: &feature.FeatureFlag{ Enabled: false, Source: feature.Source_SOURCE_SYSTEM, }, @@ -208,8 +208,6 @@ func TestServer_GetSystemFeatures(t *testing.T) { } require.NoError(t, err) assertFeatureFlag(t, tt.want.LoginDefaultOrg, got.LoginDefaultOrg) - assertFeatureFlag(t, tt.want.OidcTriggerIntrospectionProjections, got.OidcTriggerIntrospectionProjections) - assertFeatureFlag(t, tt.want.OidcLegacyIntrospection, got.OidcLegacyIntrospection) assertFeatureFlag(t, tt.want.UserSchema, got.UserSchema) }) } @@ -231,7 +229,7 @@ func TestServer_SetInstanceFeatures(t *testing.T) { args: args{ ctx: OrgCTX, req: &feature.SetInstanceFeaturesRequest{ - OidcTriggerIntrospectionProjections: gu.Ptr(true), + LoginDefaultOrg: gu.Ptr(true), }, }, wantErr: true, @@ -249,7 +247,7 @@ func TestServer_SetInstanceFeatures(t *testing.T) { args: args{ ctx: IamCTX, req: &feature.SetInstanceFeaturesRequest{ - OidcTriggerIntrospectionProjections: gu.Ptr(true), + LoginDefaultOrg: gu.Ptr(true), }, }, want: &feature.SetInstanceFeaturesResponse{ @@ -321,7 +319,7 @@ func TestServer_ResetInstanceFeatures(t *testing.T) { func TestServer_GetInstanceFeatures(t *testing.T) { _, err := Client.SetSystemFeatures(SystemCTX, &feature.SetSystemFeaturesRequest{ - OidcLegacyIntrospection: gu.Ptr(true), + LoginDefaultOrg: gu.Ptr(true), }) require.NoError(t, err) t.Cleanup(func() { @@ -358,14 +356,6 @@ func TestServer_GetInstanceFeatures(t *testing.T) { }, want: &feature.GetInstanceFeaturesResponse{ LoginDefaultOrg: &feature.FeatureFlag{ - Enabled: false, - Source: feature.Source_SOURCE_UNSPECIFIED, - }, - OidcTriggerIntrospectionProjections: &feature.FeatureFlag{ - Enabled: false, - Source: feature.Source_SOURCE_UNSPECIFIED, - }, - OidcLegacyIntrospection: &feature.FeatureFlag{ Enabled: true, Source: feature.Source_SOURCE_SYSTEM, }, @@ -379,9 +369,8 @@ func TestServer_GetInstanceFeatures(t *testing.T) { name: "some features, no inheritance", prepare: func(t *testing.T) { _, err := Client.SetInstanceFeatures(IamCTX, &feature.SetInstanceFeaturesRequest{ - LoginDefaultOrg: gu.Ptr(true), - OidcTriggerIntrospectionProjections: gu.Ptr(false), - UserSchema: gu.Ptr(true), + LoginDefaultOrg: gu.Ptr(true), + UserSchema: gu.Ptr(true), }) require.NoError(t, err) }, @@ -394,10 +383,6 @@ func TestServer_GetInstanceFeatures(t *testing.T) { Enabled: true, Source: feature.Source_SOURCE_INSTANCE, }, - OidcTriggerIntrospectionProjections: &feature.FeatureFlag{ - Enabled: false, - Source: feature.Source_SOURCE_INSTANCE, - }, UserSchema: &feature.FeatureFlag{ Enabled: true, Source: feature.Source_SOURCE_INSTANCE, @@ -423,14 +408,6 @@ func TestServer_GetInstanceFeatures(t *testing.T) { Enabled: true, Source: feature.Source_SOURCE_INSTANCE, }, - OidcTriggerIntrospectionProjections: &feature.FeatureFlag{ - Enabled: false, - Source: feature.Source_SOURCE_UNSPECIFIED, - }, - OidcLegacyIntrospection: &feature.FeatureFlag{ - Enabled: true, - Source: feature.Source_SOURCE_SYSTEM, - }, UserSchema: &feature.FeatureFlag{ Enabled: false, Source: feature.Source_SOURCE_UNSPECIFIED, @@ -455,8 +432,6 @@ func TestServer_GetInstanceFeatures(t *testing.T) { } require.NoError(t, err) assertFeatureFlag(t, tt.want.LoginDefaultOrg, got.LoginDefaultOrg) - assertFeatureFlag(t, tt.want.OidcTriggerIntrospectionProjections, got.OidcTriggerIntrospectionProjections) - assertFeatureFlag(t, tt.want.OidcLegacyIntrospection, got.OidcLegacyIntrospection) assertFeatureFlag(t, tt.want.UserSchema, got.UserSchema) }) } diff --git a/internal/api/grpc/feature/v2/server.go b/internal/api/grpc/feature/v2/server.go index ab92df5822..3eb4cc6813 100644 --- a/internal/api/grpc/feature/v2/server.go +++ b/internal/api/grpc/feature/v2/server.go @@ -1,17 +1,22 @@ package feature import ( - "google.golang.org/grpc" + "net/http" + + "connectrpc.com/connect" + "google.golang.org/protobuf/reflect/protoreflect" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/grpc/server" "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/pkg/grpc/feature/v2" + "github.com/zitadel/zitadel/pkg/grpc/feature/v2/featureconnect" ) +var _ featureconnect.FeatureServiceHandler = (*Server)(nil) + type Server struct { - feature.UnimplementedFeatureServiceServer command *command.Commands query *query.Queries } @@ -26,8 +31,12 @@ func CreateServer( } } -func (s *Server) RegisterServer(grpcServer *grpc.Server) { - feature.RegisterFeatureServiceServer(grpcServer, s) +func (s *Server) RegisterConnectServer(interceptors ...connect.Interceptor) (string, http.Handler) { + return featureconnect.NewFeatureServiceHandler(s, connect.WithInterceptors(interceptors...)) +} + +func (s *Server) FileDescriptor() protoreflect.FileDescriptor { + return feature.File_zitadel_feature_v2_feature_service_proto } func (s *Server) AppName() string { diff --git a/internal/api/grpc/feature/v2beta/converter.go b/internal/api/grpc/feature/v2beta/converter.go index 9739e1c4c8..dc791d4c51 100644 --- a/internal/api/grpc/feature/v2beta/converter.go +++ b/internal/api/grpc/feature/v2beta/converter.go @@ -10,55 +10,45 @@ import ( func systemFeaturesToCommand(req *feature_pb.SetSystemFeaturesRequest) *command.SystemFeatures { return &command.SystemFeatures{ - LoginDefaultOrg: req.LoginDefaultOrg, - TriggerIntrospectionProjections: req.OidcTriggerIntrospectionProjections, - LegacyIntrospection: req.OidcLegacyIntrospection, - UserSchema: req.UserSchema, - TokenExchange: req.OidcTokenExchange, - ImprovedPerformance: improvedPerformanceListToDomain(req.ImprovedPerformance), - OIDCSingleV1SessionTermination: req.OidcSingleV1SessionTermination, + LoginDefaultOrg: req.LoginDefaultOrg, + UserSchema: req.UserSchema, + TokenExchange: req.OidcTokenExchange, + ImprovedPerformance: improvedPerformanceListToDomain(req.ImprovedPerformance), + OIDCSingleV1SessionTermination: req.OidcSingleV1SessionTermination, } } func systemFeaturesToPb(f *query.SystemFeatures) *feature_pb.GetSystemFeaturesResponse { return &feature_pb.GetSystemFeaturesResponse{ - Details: object.DomainToDetailsPb(f.Details), - LoginDefaultOrg: featureSourceToFlagPb(&f.LoginDefaultOrg), - OidcTriggerIntrospectionProjections: featureSourceToFlagPb(&f.TriggerIntrospectionProjections), - OidcLegacyIntrospection: featureSourceToFlagPb(&f.LegacyIntrospection), - UserSchema: featureSourceToFlagPb(&f.UserSchema), - OidcTokenExchange: featureSourceToFlagPb(&f.TokenExchange), - ImprovedPerformance: featureSourceToImprovedPerformanceFlagPb(&f.ImprovedPerformance), - OidcSingleV1SessionTermination: featureSourceToFlagPb(&f.OIDCSingleV1SessionTermination), + Details: object.DomainToDetailsPb(f.Details), + LoginDefaultOrg: featureSourceToFlagPb(&f.LoginDefaultOrg), + UserSchema: featureSourceToFlagPb(&f.UserSchema), + OidcTokenExchange: featureSourceToFlagPb(&f.TokenExchange), + ImprovedPerformance: featureSourceToImprovedPerformanceFlagPb(&f.ImprovedPerformance), + OidcSingleV1SessionTermination: featureSourceToFlagPb(&f.OIDCSingleV1SessionTermination), } } func instanceFeaturesToCommand(req *feature_pb.SetInstanceFeaturesRequest) *command.InstanceFeatures { return &command.InstanceFeatures{ - LoginDefaultOrg: req.LoginDefaultOrg, - TriggerIntrospectionProjections: req.OidcTriggerIntrospectionProjections, - LegacyIntrospection: req.OidcLegacyIntrospection, - UserSchema: req.UserSchema, - TokenExchange: req.OidcTokenExchange, - ImprovedPerformance: improvedPerformanceListToDomain(req.ImprovedPerformance), - WebKey: req.WebKey, - DebugOIDCParentError: req.DebugOidcParentError, - OIDCSingleV1SessionTermination: req.OidcSingleV1SessionTermination, + LoginDefaultOrg: req.LoginDefaultOrg, + UserSchema: req.UserSchema, + TokenExchange: req.OidcTokenExchange, + ImprovedPerformance: improvedPerformanceListToDomain(req.ImprovedPerformance), + DebugOIDCParentError: req.DebugOidcParentError, + OIDCSingleV1SessionTermination: req.OidcSingleV1SessionTermination, } } func instanceFeaturesToPb(f *query.InstanceFeatures) *feature_pb.GetInstanceFeaturesResponse { return &feature_pb.GetInstanceFeaturesResponse{ - Details: object.DomainToDetailsPb(f.Details), - LoginDefaultOrg: featureSourceToFlagPb(&f.LoginDefaultOrg), - OidcTriggerIntrospectionProjections: featureSourceToFlagPb(&f.TriggerIntrospectionProjections), - OidcLegacyIntrospection: featureSourceToFlagPb(&f.LegacyIntrospection), - UserSchema: featureSourceToFlagPb(&f.UserSchema), - OidcTokenExchange: featureSourceToFlagPb(&f.TokenExchange), - ImprovedPerformance: featureSourceToImprovedPerformanceFlagPb(&f.ImprovedPerformance), - WebKey: featureSourceToFlagPb(&f.WebKey), - DebugOidcParentError: featureSourceToFlagPb(&f.DebugOIDCParentError), - OidcSingleV1SessionTermination: featureSourceToFlagPb(&f.OIDCSingleV1SessionTermination), + Details: object.DomainToDetailsPb(f.Details), + LoginDefaultOrg: featureSourceToFlagPb(&f.LoginDefaultOrg), + UserSchema: featureSourceToFlagPb(&f.UserSchema), + OidcTokenExchange: featureSourceToFlagPb(&f.TokenExchange), + ImprovedPerformance: featureSourceToImprovedPerformanceFlagPb(&f.ImprovedPerformance), + DebugOidcParentError: featureSourceToFlagPb(&f.DebugOIDCParentError), + OidcSingleV1SessionTermination: featureSourceToFlagPb(&f.OIDCSingleV1SessionTermination), } } diff --git a/internal/api/grpc/feature/v2beta/converter_test.go b/internal/api/grpc/feature/v2beta/converter_test.go index 72d91b10d4..ec681011f0 100644 --- a/internal/api/grpc/feature/v2beta/converter_test.go +++ b/internal/api/grpc/feature/v2beta/converter_test.go @@ -18,22 +18,18 @@ import ( func Test_systemFeaturesToCommand(t *testing.T) { arg := &feature_pb.SetSystemFeaturesRequest{ - LoginDefaultOrg: gu.Ptr(true), - OidcTriggerIntrospectionProjections: gu.Ptr(false), - OidcLegacyIntrospection: nil, - UserSchema: gu.Ptr(true), - OidcTokenExchange: gu.Ptr(true), - ImprovedPerformance: nil, - OidcSingleV1SessionTermination: gu.Ptr(true), + LoginDefaultOrg: gu.Ptr(true), + UserSchema: gu.Ptr(true), + OidcTokenExchange: gu.Ptr(true), + ImprovedPerformance: nil, + OidcSingleV1SessionTermination: gu.Ptr(true), } want := &command.SystemFeatures{ - LoginDefaultOrg: gu.Ptr(true), - TriggerIntrospectionProjections: gu.Ptr(false), - LegacyIntrospection: nil, - UserSchema: gu.Ptr(true), - TokenExchange: gu.Ptr(true), - ImprovedPerformance: nil, - OIDCSingleV1SessionTermination: gu.Ptr(true), + LoginDefaultOrg: gu.Ptr(true), + UserSchema: gu.Ptr(true), + TokenExchange: gu.Ptr(true), + ImprovedPerformance: nil, + OIDCSingleV1SessionTermination: gu.Ptr(true), } got := systemFeaturesToCommand(arg) assert.Equal(t, want, got) @@ -50,14 +46,6 @@ func Test_systemFeaturesToPb(t *testing.T) { Level: feature.LevelSystem, Value: true, }, - TriggerIntrospectionProjections: query.FeatureSource[bool]{ - Level: feature.LevelUnspecified, - Value: false, - }, - LegacyIntrospection: query.FeatureSource[bool]{ - Level: feature.LevelSystem, - Value: true, - }, UserSchema: query.FeatureSource[bool]{ Level: feature.LevelSystem, Value: true, @@ -85,14 +73,6 @@ func Test_systemFeaturesToPb(t *testing.T) { Enabled: true, Source: feature_pb.Source_SOURCE_SYSTEM, }, - OidcTriggerIntrospectionProjections: &feature_pb.FeatureFlag{ - Enabled: false, - Source: feature_pb.Source_SOURCE_UNSPECIFIED, - }, - OidcLegacyIntrospection: &feature_pb.FeatureFlag{ - Enabled: true, - Source: feature_pb.Source_SOURCE_SYSTEM, - }, UserSchema: &feature_pb.FeatureFlag{ Enabled: true, Source: feature_pb.Source_SOURCE_SYSTEM, @@ -116,24 +96,18 @@ func Test_systemFeaturesToPb(t *testing.T) { func Test_instanceFeaturesToCommand(t *testing.T) { arg := &feature_pb.SetInstanceFeaturesRequest{ - LoginDefaultOrg: gu.Ptr(true), - OidcTriggerIntrospectionProjections: gu.Ptr(false), - OidcLegacyIntrospection: nil, - UserSchema: gu.Ptr(true), - OidcTokenExchange: gu.Ptr(true), - ImprovedPerformance: nil, - WebKey: gu.Ptr(true), - OidcSingleV1SessionTermination: gu.Ptr(true), + LoginDefaultOrg: gu.Ptr(true), + UserSchema: gu.Ptr(true), + OidcTokenExchange: gu.Ptr(true), + ImprovedPerformance: nil, + OidcSingleV1SessionTermination: gu.Ptr(true), } want := &command.InstanceFeatures{ - LoginDefaultOrg: gu.Ptr(true), - TriggerIntrospectionProjections: gu.Ptr(false), - LegacyIntrospection: nil, - UserSchema: gu.Ptr(true), - TokenExchange: gu.Ptr(true), - ImprovedPerformance: nil, - WebKey: gu.Ptr(true), - OIDCSingleV1SessionTermination: gu.Ptr(true), + LoginDefaultOrg: gu.Ptr(true), + UserSchema: gu.Ptr(true), + TokenExchange: gu.Ptr(true), + ImprovedPerformance: nil, + OIDCSingleV1SessionTermination: gu.Ptr(true), } got := instanceFeaturesToCommand(arg) assert.Equal(t, want, got) @@ -150,14 +124,6 @@ func Test_instanceFeaturesToPb(t *testing.T) { Level: feature.LevelSystem, Value: true, }, - TriggerIntrospectionProjections: query.FeatureSource[bool]{ - Level: feature.LevelUnspecified, - Value: false, - }, - LegacyIntrospection: query.FeatureSource[bool]{ - Level: feature.LevelInstance, - Value: true, - }, UserSchema: query.FeatureSource[bool]{ Level: feature.LevelInstance, Value: true, @@ -170,10 +136,6 @@ func Test_instanceFeaturesToPb(t *testing.T) { Level: feature.LevelSystem, Value: []feature.ImprovedPerformanceType{feature.ImprovedPerformanceTypeOrgByID}, }, - WebKey: query.FeatureSource[bool]{ - Level: feature.LevelInstance, - Value: true, - }, OIDCSingleV1SessionTermination: query.FeatureSource[bool]{ Level: feature.LevelInstance, Value: true, @@ -189,14 +151,6 @@ func Test_instanceFeaturesToPb(t *testing.T) { Enabled: true, Source: feature_pb.Source_SOURCE_SYSTEM, }, - OidcTriggerIntrospectionProjections: &feature_pb.FeatureFlag{ - Enabled: false, - Source: feature_pb.Source_SOURCE_UNSPECIFIED, - }, - OidcLegacyIntrospection: &feature_pb.FeatureFlag{ - Enabled: true, - Source: feature_pb.Source_SOURCE_INSTANCE, - }, UserSchema: &feature_pb.FeatureFlag{ Enabled: true, Source: feature_pb.Source_SOURCE_INSTANCE, @@ -209,10 +163,6 @@ func Test_instanceFeaturesToPb(t *testing.T) { ExecutionPaths: []feature_pb.ImprovedPerformance{feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_ORG_BY_ID}, Source: feature_pb.Source_SOURCE_SYSTEM, }, - WebKey: &feature_pb.FeatureFlag{ - Enabled: true, - Source: feature_pb.Source_SOURCE_INSTANCE, - }, DebugOidcParentError: &feature_pb.FeatureFlag{ Enabled: false, Source: feature_pb.Source_SOURCE_UNSPECIFIED, diff --git a/internal/api/grpc/feature/v2beta/feature.go b/internal/api/grpc/feature/v2beta/feature.go index b94f8e7de2..4ff51af883 100644 --- a/internal/api/grpc/feature/v2beta/feature.go +++ b/internal/api/grpc/feature/v2beta/feature.go @@ -3,6 +3,7 @@ package feature import ( "context" + "connectrpc.com/connect" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -10,77 +11,77 @@ import ( feature "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta" ) -func (s *Server) SetSystemFeatures(ctx context.Context, req *feature.SetSystemFeaturesRequest) (_ *feature.SetSystemFeaturesResponse, err error) { - details, err := s.command.SetSystemFeatures(ctx, systemFeaturesToCommand(req)) +func (s *Server) SetSystemFeatures(ctx context.Context, req *connect.Request[feature.SetSystemFeaturesRequest]) (_ *connect.Response[feature.SetSystemFeaturesResponse], err error) { + details, err := s.command.SetSystemFeatures(ctx, systemFeaturesToCommand(req.Msg)) if err != nil { return nil, err } - return &feature.SetSystemFeaturesResponse{ + return connect.NewResponse(&feature.SetSystemFeaturesResponse{ Details: object.DomainToDetailsPb(details), - }, nil + }), nil } -func (s *Server) ResetSystemFeatures(ctx context.Context, req *feature.ResetSystemFeaturesRequest) (_ *feature.ResetSystemFeaturesResponse, err error) { +func (s *Server) ResetSystemFeatures(ctx context.Context, req *connect.Request[feature.ResetSystemFeaturesRequest]) (_ *connect.Response[feature.ResetSystemFeaturesResponse], err error) { details, err := s.command.ResetSystemFeatures(ctx) if err != nil { return nil, err } - return &feature.ResetSystemFeaturesResponse{ + return connect.NewResponse(&feature.ResetSystemFeaturesResponse{ Details: object.DomainToDetailsPb(details), - }, nil + }), nil } -func (s *Server) GetSystemFeatures(ctx context.Context, req *feature.GetSystemFeaturesRequest) (_ *feature.GetSystemFeaturesResponse, err error) { +func (s *Server) GetSystemFeatures(ctx context.Context, req *connect.Request[feature.GetSystemFeaturesRequest]) (_ *connect.Response[feature.GetSystemFeaturesResponse], err error) { f, err := s.query.GetSystemFeatures(ctx) if err != nil { return nil, err } - return systemFeaturesToPb(f), nil + return connect.NewResponse(systemFeaturesToPb(f)), nil } -func (s *Server) SetInstanceFeatures(ctx context.Context, req *feature.SetInstanceFeaturesRequest) (_ *feature.SetInstanceFeaturesResponse, err error) { - details, err := s.command.SetInstanceFeatures(ctx, instanceFeaturesToCommand(req)) +func (s *Server) SetInstanceFeatures(ctx context.Context, req *connect.Request[feature.SetInstanceFeaturesRequest]) (_ *connect.Response[feature.SetInstanceFeaturesResponse], err error) { + details, err := s.command.SetInstanceFeatures(ctx, instanceFeaturesToCommand(req.Msg)) if err != nil { return nil, err } - return &feature.SetInstanceFeaturesResponse{ + return connect.NewResponse(&feature.SetInstanceFeaturesResponse{ Details: object.DomainToDetailsPb(details), - }, nil + }), nil } -func (s *Server) ResetInstanceFeatures(ctx context.Context, req *feature.ResetInstanceFeaturesRequest) (_ *feature.ResetInstanceFeaturesResponse, err error) { +func (s *Server) ResetInstanceFeatures(ctx context.Context, req *connect.Request[feature.ResetInstanceFeaturesRequest]) (_ *connect.Response[feature.ResetInstanceFeaturesResponse], err error) { details, err := s.command.ResetInstanceFeatures(ctx) if err != nil { return nil, err } - return &feature.ResetInstanceFeaturesResponse{ + return connect.NewResponse(&feature.ResetInstanceFeaturesResponse{ Details: object.DomainToDetailsPb(details), - }, nil + }), nil } -func (s *Server) GetInstanceFeatures(ctx context.Context, req *feature.GetInstanceFeaturesRequest) (_ *feature.GetInstanceFeaturesResponse, err error) { - f, err := s.query.GetInstanceFeatures(ctx, req.GetInheritance()) +func (s *Server) GetInstanceFeatures(ctx context.Context, req *connect.Request[feature.GetInstanceFeaturesRequest]) (_ *connect.Response[feature.GetInstanceFeaturesResponse], err error) { + f, err := s.query.GetInstanceFeatures(ctx, req.Msg.GetInheritance()) if err != nil { return nil, err } - return instanceFeaturesToPb(f), nil + return connect.NewResponse(instanceFeaturesToPb(f)), nil } -func (s *Server) SetOrganizationFeatures(ctx context.Context, req *feature.SetOrganizationFeaturesRequest) (_ *feature.SetOrganizationFeaturesResponse, err error) { +func (s *Server) SetOrganizationFeatures(ctx context.Context, req *connect.Request[feature.SetOrganizationFeaturesRequest]) (_ *connect.Response[feature.SetOrganizationFeaturesResponse], err error) { return nil, status.Errorf(codes.Unimplemented, "method SetOrganizationFeatures not implemented") } -func (s *Server) ResetOrganizationFeatures(ctx context.Context, req *feature.ResetOrganizationFeaturesRequest) (_ *feature.ResetOrganizationFeaturesResponse, err error) { +func (s *Server) ResetOrganizationFeatures(ctx context.Context, req *connect.Request[feature.ResetOrganizationFeaturesRequest]) (_ *connect.Response[feature.ResetOrganizationFeaturesResponse], err error) { return nil, status.Errorf(codes.Unimplemented, "method ResetOrganizationFeatures not implemented") } -func (s *Server) GetOrganizationFeatures(ctx context.Context, req *feature.GetOrganizationFeaturesRequest) (_ *feature.GetOrganizationFeaturesResponse, err error) { +func (s *Server) GetOrganizationFeatures(ctx context.Context, req *connect.Request[feature.GetOrganizationFeaturesRequest]) (_ *connect.Response[feature.GetOrganizationFeaturesResponse], err error) { return nil, status.Errorf(codes.Unimplemented, "method GetOrganizationFeatures not implemented") } -func (s *Server) SetUserFeatures(ctx context.Context, req *feature.SetUserFeatureRequest) (_ *feature.SetUserFeaturesResponse, err error) { +func (s *Server) SetUserFeatures(ctx context.Context, req *connect.Request[feature.SetUserFeatureRequest]) (_ *connect.Response[feature.SetUserFeaturesResponse], err error) { return nil, status.Errorf(codes.Unimplemented, "method SetUserFeatures not implemented") } -func (s *Server) ResetUserFeatures(ctx context.Context, req *feature.ResetUserFeaturesRequest) (_ *feature.ResetUserFeaturesResponse, err error) { +func (s *Server) ResetUserFeatures(ctx context.Context, req *connect.Request[feature.ResetUserFeaturesRequest]) (_ *connect.Response[feature.ResetUserFeaturesResponse], err error) { return nil, status.Errorf(codes.Unimplemented, "method ResetUserFeatures not implemented") } -func (s *Server) GetUserFeatures(ctx context.Context, req *feature.GetUserFeaturesRequest) (_ *feature.GetUserFeaturesResponse, err error) { +func (s *Server) GetUserFeatures(ctx context.Context, req *connect.Request[feature.GetUserFeaturesRequest]) (_ *connect.Response[feature.GetUserFeaturesResponse], err error) { return nil, status.Errorf(codes.Unimplemented, "method GetUserFeatures not implemented") } diff --git a/internal/api/grpc/feature/v2beta/integration_test/feature_test.go b/internal/api/grpc/feature/v2beta/integration_test/feature_test.go index cbd9f5f939..4e24bb2a4f 100644 --- a/internal/api/grpc/feature/v2beta/integration_test/feature_test.go +++ b/internal/api/grpc/feature/v2beta/integration_test/feature_test.go @@ -61,7 +61,7 @@ func TestServer_SetInstanceFeatures(t *testing.T) { args: args{ ctx: OrgCTX, req: &feature.SetInstanceFeaturesRequest{ - OidcTriggerIntrospectionProjections: gu.Ptr(true), + LoginDefaultOrg: gu.Ptr(true), }, }, wantErr: true, @@ -79,7 +79,7 @@ func TestServer_SetInstanceFeatures(t *testing.T) { args: args{ ctx: IamCTX, req: &feature.SetInstanceFeaturesRequest{ - OidcTriggerIntrospectionProjections: gu.Ptr(true), + LoginDefaultOrg: gu.Ptr(true), }, }, want: &feature.SetInstanceFeaturesResponse{ @@ -190,14 +190,6 @@ func TestServer_GetInstanceFeatures(t *testing.T) { Enabled: false, Source: feature.Source_SOURCE_UNSPECIFIED, }, - OidcTriggerIntrospectionProjections: &feature.FeatureFlag{ - Enabled: false, - Source: feature.Source_SOURCE_UNSPECIFIED, - }, - OidcLegacyIntrospection: &feature.FeatureFlag{ - Enabled: false, - Source: feature.Source_SOURCE_UNSPECIFIED, - }, UserSchema: &feature.FeatureFlag{ Enabled: false, Source: feature.Source_SOURCE_UNSPECIFIED, @@ -208,9 +200,8 @@ func TestServer_GetInstanceFeatures(t *testing.T) { name: "some features, no inheritance", prepare: func(t *testing.T) { _, err := Client.SetInstanceFeatures(IamCTX, &feature.SetInstanceFeaturesRequest{ - LoginDefaultOrg: gu.Ptr(true), - OidcTriggerIntrospectionProjections: gu.Ptr(false), - UserSchema: gu.Ptr(true), + LoginDefaultOrg: gu.Ptr(true), + UserSchema: gu.Ptr(true), }) require.NoError(t, err) }, @@ -223,10 +214,6 @@ func TestServer_GetInstanceFeatures(t *testing.T) { Enabled: true, Source: feature.Source_SOURCE_INSTANCE, }, - OidcTriggerIntrospectionProjections: &feature.FeatureFlag{ - Enabled: false, - Source: feature.Source_SOURCE_INSTANCE, - }, UserSchema: &feature.FeatureFlag{ Enabled: true, Source: feature.Source_SOURCE_INSTANCE, @@ -252,14 +239,6 @@ func TestServer_GetInstanceFeatures(t *testing.T) { Enabled: true, Source: feature.Source_SOURCE_INSTANCE, }, - OidcTriggerIntrospectionProjections: &feature.FeatureFlag{ - Enabled: false, - Source: feature.Source_SOURCE_UNSPECIFIED, - }, - OidcLegacyIntrospection: &feature.FeatureFlag{ - Enabled: false, - Source: feature.Source_SOURCE_UNSPECIFIED, - }, UserSchema: &feature.FeatureFlag{ Enabled: false, Source: feature.Source_SOURCE_UNSPECIFIED, @@ -284,8 +263,6 @@ func TestServer_GetInstanceFeatures(t *testing.T) { } require.NoError(t, err) assertFeatureFlag(t, tt.want.LoginDefaultOrg, got.LoginDefaultOrg) - assertFeatureFlag(t, tt.want.OidcTriggerIntrospectionProjections, got.OidcTriggerIntrospectionProjections) - assertFeatureFlag(t, tt.want.OidcLegacyIntrospection, got.OidcLegacyIntrospection) assertFeatureFlag(t, tt.want.UserSchema, got.UserSchema) }) } diff --git a/internal/api/grpc/feature/v2beta/server.go b/internal/api/grpc/feature/v2beta/server.go index 4208c4acfc..29877f77f9 100644 --- a/internal/api/grpc/feature/v2beta/server.go +++ b/internal/api/grpc/feature/v2beta/server.go @@ -1,17 +1,22 @@ package feature import ( - "google.golang.org/grpc" + "net/http" + + "connectrpc.com/connect" + "google.golang.org/protobuf/reflect/protoreflect" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/grpc/server" "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/query" feature "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta/featureconnect" ) +var _ featureconnect.FeatureServiceHandler = (*Server)(nil) + type Server struct { - feature.UnimplementedFeatureServiceServer command *command.Commands query *query.Queries } @@ -26,8 +31,12 @@ func CreateServer( } } -func (s *Server) RegisterServer(grpcServer *grpc.Server) { - feature.RegisterFeatureServiceServer(grpcServer, s) +func (s *Server) RegisterConnectServer(interceptors ...connect.Interceptor) (string, http.Handler) { + return featureconnect.NewFeatureServiceHandler(s, connect.WithInterceptors(interceptors...)) +} + +func (s *Server) FileDescriptor() protoreflect.FileDescriptor { + return feature.File_zitadel_feature_v2beta_feature_service_proto } func (s *Server) AppName() string { diff --git a/internal/api/grpc/filter/v2/converter.go b/internal/api/grpc/filter/v2/converter.go new file mode 100644 index 0000000000..98e8bdd4f8 --- /dev/null +++ b/internal/api/grpc/filter/v2/converter.go @@ -0,0 +1,73 @@ +package filter + +import ( + "fmt" + + "github.com/zitadel/zitadel/internal/config/systemdefaults" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/zerrors" + "github.com/zitadel/zitadel/pkg/grpc/filter/v2" +) + +func TextMethodPbToQuery(method filter.TextFilterMethod) query.TextComparison { + switch method { + case filter.TextFilterMethod_TEXT_FILTER_METHOD_EQUALS: + return query.TextEquals + case filter.TextFilterMethod_TEXT_FILTER_METHOD_EQUALS_IGNORE_CASE: + return query.TextEqualsIgnoreCase + case filter.TextFilterMethod_TEXT_FILTER_METHOD_STARTS_WITH: + return query.TextStartsWith + case filter.TextFilterMethod_TEXT_FILTER_METHOD_STARTS_WITH_IGNORE_CASE: + return query.TextStartsWithIgnoreCase + case filter.TextFilterMethod_TEXT_FILTER_METHOD_CONTAINS: + return query.TextContains + case filter.TextFilterMethod_TEXT_FILTER_METHOD_CONTAINS_IGNORE_CASE: + return query.TextContainsIgnoreCase + case filter.TextFilterMethod_TEXT_FILTER_METHOD_ENDS_WITH: + return query.TextEndsWith + case filter.TextFilterMethod_TEXT_FILTER_METHOD_ENDS_WITH_IGNORE_CASE: + return query.TextEndsWithIgnoreCase + default: + return -1 + } +} + +func TimestampMethodPbToQuery(method filter.TimestampFilterMethod) query.TimestampComparison { + switch method { + case filter.TimestampFilterMethod_TIMESTAMP_FILTER_METHOD_EQUALS: + return query.TimestampEquals + case filter.TimestampFilterMethod_TIMESTAMP_FILTER_METHOD_BEFORE: + return query.TimestampLess + case filter.TimestampFilterMethod_TIMESTAMP_FILTER_METHOD_AFTER: + return query.TimestampGreater + case filter.TimestampFilterMethod_TIMESTAMP_FILTER_METHOD_BEFORE_OR_EQUALS: + return query.TimestampLessOrEquals + case filter.TimestampFilterMethod_TIMESTAMP_FILTER_METHOD_AFTER_OR_EQUALS: + return query.TimestampGreaterOrEquals + default: + return -1 + } +} + +func PaginationPbToQuery(defaults systemdefaults.SystemDefaults, query *filter.PaginationRequest) (offset, limit uint64, asc bool, err error) { + limit = defaults.DefaultQueryLimit + if query == nil { + return 0, limit, asc, nil + } + offset = query.Offset + asc = query.Asc + if defaults.MaxQueryLimit > 0 && uint64(query.Limit) > defaults.MaxQueryLimit { + return 0, 0, false, zerrors.ThrowInvalidArgumentf(fmt.Errorf("given: %d, allowed: %d", query.Limit, defaults.MaxQueryLimit), "QUERY-4M0fs", "Errors.Query.LimitExceeded") + } + if query.Limit > 0 { + limit = uint64(query.Limit) + } + return offset, limit, asc, nil +} + +func QueryToPaginationPb(request query.SearchRequest, response query.SearchResponse) *filter.PaginationResponse { + return &filter.PaginationResponse{ + AppliedLimit: request.Limit, + TotalResult: response.Count, + } +} diff --git a/internal/api/grpc/filter/v2beta/converter.go b/internal/api/grpc/filter/v2beta/converter.go index e34f9dd9d7..e15895e9d9 100644 --- a/internal/api/grpc/filter/v2beta/converter.go +++ b/internal/api/grpc/filter/v2beta/converter.go @@ -32,6 +32,23 @@ func TextMethodPbToQuery(method filter.TextFilterMethod) query.TextComparison { } } +func TimestampMethodPbToQuery(method filter.TimestampFilterMethod) query.TimestampComparison { + switch method { + case filter.TimestampFilterMethod_TIMESTAMP_FILTER_METHOD_EQUALS: + return query.TimestampEquals + case filter.TimestampFilterMethod_TIMESTAMP_FILTER_METHOD_LESS: + return query.TimestampLess + case filter.TimestampFilterMethod_TIMESTAMP_FILTER_METHOD_GREATER: + return query.TimestampGreater + case filter.TimestampFilterMethod_TIMESTAMP_FILTER_METHOD_LESS_OR_EQUALS: + return query.TimestampLessOrEquals + case filter.TimestampFilterMethod_TIMESTAMP_FILTER_METHOD_GREATER_OR_EQUALS: + return query.TimestampGreaterOrEquals + default: + return -1 + } +} + func PaginationPbToQuery(defaults systemdefaults.SystemDefaults, query *filter.PaginationRequest) (offset, limit uint64, asc bool, err error) { limit = defaults.DefaultQueryLimit if query == nil { diff --git a/internal/api/grpc/gerrors/zitadel_errors.go b/internal/api/grpc/gerrors/zitadel_errors.go index d679054da6..b5d2893062 100644 --- a/internal/api/grpc/gerrors/zitadel_errors.go +++ b/internal/api/grpc/gerrors/zitadel_errors.go @@ -3,10 +3,12 @@ package gerrors import ( "errors" + "connectrpc.com/connect" "github.com/jackc/pgx/v5/pgconn" "github.com/zitadel/logging" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" "google.golang.org/protobuf/protoadapt" commandErrors "github.com/zitadel/zitadel/internal/command/errors" @@ -36,6 +38,30 @@ func ZITADELToGRPCError(err error) error { return s.Err() } +func ZITADELToConnectError(err error) error { + if err == nil { + return nil + } + connectError := new(connect.Error) + if errors.As(err, &connectError) { + return err + } + code, key, id, ok := ExtractZITADELError(err) + if !ok { + return status.Convert(err).Err() + } + msg := key + msg += " (" + id + ")" + + errorInfo := getErrorInfo(id, key, err) + + cErr := connect.NewError(connect.Code(code), errors.New(msg)) + if detail, detailErr := connect.NewErrorDetail(errorInfo.(proto.Message)); detailErr == nil { + cErr.AddDetail(detail) + } + return cErr +} + func ExtractZITADELError(err error) (c codes.Code, msg, id string, ok bool) { if err == nil { return codes.OK, "", "", false diff --git a/internal/api/grpc/idp/v2/query.go b/internal/api/grpc/idp/v2/query.go index 082a94d18f..587b1687b9 100644 --- a/internal/api/grpc/idp/v2/query.go +++ b/internal/api/grpc/idp/v2/query.go @@ -3,6 +3,7 @@ package idp import ( "context" + "connectrpc.com/connect" "github.com/crewjam/saml" "github.com/muhlemmer/gu" "google.golang.org/protobuf/types/known/durationpb" @@ -15,12 +16,12 @@ import ( idp_pb "github.com/zitadel/zitadel/pkg/grpc/idp/v2" ) -func (s *Server) GetIDPByID(ctx context.Context, req *idp_pb.GetIDPByIDRequest) (*idp_pb.GetIDPByIDResponse, error) { - idp, err := s.query.IDPTemplateByID(ctx, true, req.Id, false, s.checkPermission) +func (s *Server) GetIDPByID(ctx context.Context, req *connect.Request[idp_pb.GetIDPByIDRequest]) (*connect.Response[idp_pb.GetIDPByIDResponse], error) { + idp, err := s.query.IDPTemplateByID(ctx, true, req.Msg.GetId(), false, s.checkPermission) if err != nil { return nil, err } - return &idp_pb.GetIDPByIDResponse{Idp: idpToPb(idp)}, nil + return connect.NewResponse(&idp_pb.GetIDPByIDResponse{Idp: idpToPb(idp)}), nil } func idpToPb(idp *query.IDPTemplate) *idp_pb.IDP { diff --git a/internal/api/grpc/idp/v2/server.go b/internal/api/grpc/idp/v2/server.go index 246e980434..666c39294d 100644 --- a/internal/api/grpc/idp/v2/server.go +++ b/internal/api/grpc/idp/v2/server.go @@ -1,7 +1,10 @@ package idp import ( - "google.golang.org/grpc" + "net/http" + + "connectrpc.com/connect" + "google.golang.org/protobuf/reflect/protoreflect" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/grpc/server" @@ -9,12 +12,12 @@ import ( "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/pkg/grpc/idp/v2" + "github.com/zitadel/zitadel/pkg/grpc/idp/v2/idpconnect" ) -var _ idp.IdentityProviderServiceServer = (*Server)(nil) +var _ idpconnect.IdentityProviderServiceHandler = (*Server)(nil) type Server struct { - idp.UnimplementedIdentityProviderServiceServer command *command.Commands query *query.Queries @@ -35,8 +38,12 @@ func CreateServer( } } -func (s *Server) RegisterServer(grpcServer *grpc.Server) { - idp.RegisterIdentityProviderServiceServer(grpcServer, s) +func (s *Server) RegisterConnectServer(interceptors ...connect.Interceptor) (string, http.Handler) { + return idpconnect.NewIdentityProviderServiceHandler(s, connect.WithInterceptors(interceptors...)) +} + +func (s *Server) FileDescriptor() protoreflect.FileDescriptor { + return idp.File_zitadel_idp_v2_idp_service_proto } func (s *Server) AppName() string { diff --git a/internal/api/grpc/instance/converter.go b/internal/api/grpc/instance/converter.go index 4094da4a77..b894a064ff 100644 --- a/internal/api/grpc/instance/converter.go +++ b/internal/api/grpc/instance/converter.go @@ -28,7 +28,7 @@ func InstanceToPb(instance *query.Instance) *instance_pb.Instance { Name: instance.Name, Domains: DomainsToPb(instance.Domains), Version: build.Version(), - State: instance_pb.State_STATE_RUNNING, //TODO: change when delete is implemented + State: instance_pb.State_STATE_RUNNING, // TODO: change when delete is implemented } } @@ -44,7 +44,7 @@ func InstanceDetailToPb(instance *query.Instance) *instance_pb.InstanceDetail { Name: instance.Name, Domains: DomainsToPb(instance.Domains), Version: build.Version(), - State: instance_pb.State_STATE_RUNNING, //TODO: change when delete is implemented + State: instance_pb.State_STATE_RUNNING, // TODO: change when delete is implemented } } diff --git a/internal/api/grpc/instance/v2beta/converter.go b/internal/api/grpc/instance/v2beta/converter.go new file mode 100644 index 0000000000..8bff682606 --- /dev/null +++ b/internal/api/grpc/instance/v2beta/converter.go @@ -0,0 +1,246 @@ +package instance + +import ( + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/cmd/build" + filter "github.com/zitadel/zitadel/internal/api/grpc/filter/v2beta" + "github.com/zitadel/zitadel/internal/api/grpc/object/v2" + "github.com/zitadel/zitadel/internal/config/systemdefaults" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/zerrors" + instance "github.com/zitadel/zitadel/pkg/grpc/instance/v2beta" +) + +func InstancesToPb(instances []*query.Instance) []*instance.Instance { + list := []*instance.Instance{} + for _, instance := range instances { + list = append(list, ToProtoObject(instance)) + } + return list +} + +func ToProtoObject(inst *query.Instance) *instance.Instance { + return &instance.Instance{ + Id: inst.ID, + Name: inst.Name, + Domains: DomainsToPb(inst.Domains), + Version: build.Version(), + ChangeDate: timestamppb.New(inst.ChangeDate), + CreationDate: timestamppb.New(inst.CreationDate), + } +} + +func DomainsToPb(domains []*query.InstanceDomain) []*instance.Domain { + d := []*instance.Domain{} + for _, dm := range domains { + pbDomain := DomainToPb(dm) + d = append(d, pbDomain) + } + return d +} + +func DomainToPb(d *query.InstanceDomain) *instance.Domain { + return &instance.Domain{ + Domain: d.Domain, + Primary: d.IsPrimary, + Generated: d.IsGenerated, + InstanceId: d.InstanceID, + CreationDate: timestamppb.New(d.CreationDate), + } +} + +func ListInstancesRequestToModel(req *instance.ListInstancesRequest, sysDefaults systemdefaults.SystemDefaults) (*query.InstanceSearchQueries, error) { + offset, limit, asc, err := filter.PaginationPbToQuery(sysDefaults, req.GetPagination()) + if err != nil { + return nil, err + } + + queries, err := instanceQueriesToModel(req.GetQueries()) + if err != nil { + return nil, err + } + + return &query.InstanceSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + SortingColumn: fieldNameToInstanceColumn(req.GetSortingColumn()), + }, + Queries: queries, + }, nil + +} + +func fieldNameToInstanceColumn(fieldName instance.FieldName) query.Column { + switch fieldName { + case instance.FieldName_FIELD_NAME_ID: + return query.InstanceColumnID + case instance.FieldName_FIELD_NAME_NAME: + return query.InstanceColumnName + case instance.FieldName_FIELD_NAME_CREATION_DATE: + return query.InstanceColumnCreationDate + case instance.FieldName_FIELD_NAME_UNSPECIFIED: + fallthrough + default: + return query.Column{} + } +} + +func instanceQueriesToModel(queries []*instance.Query) (_ []query.SearchQuery, err error) { + q := []query.SearchQuery{} + for _, query := range queries { + model, err := instanceQueryToModel(query) + if err != nil { + return nil, err + } + q = append(q, model) + } + return q, nil +} + +func instanceQueryToModel(searchQuery *instance.Query) (query.SearchQuery, error) { + switch q := searchQuery.GetQuery().(type) { + case *instance.Query_IdQuery: + return query.NewInstanceIDsListSearchQuery(q.IdQuery.GetIds()...) + case *instance.Query_DomainQuery: + return query.NewInstanceDomainsListSearchQuery(q.DomainQuery.GetDomains()...) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "INST-3m0se", "List.Query.Invalid") + } +} + +func ListCustomDomainsRequestToModel(req *instance.ListCustomDomainsRequest, defaults systemdefaults.SystemDefaults) (*query.InstanceDomainSearchQueries, error) { + offset, limit, asc, err := filter.PaginationPbToQuery(defaults, req.GetPagination()) + if err != nil { + return nil, err + } + + queries, err := domainQueriesToModel(req.GetQueries()) + if err != nil { + return nil, err + } + + return &query.InstanceDomainSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + SortingColumn: fieldNameToInstanceDomainColumn(req.GetSortingColumn()), + }, + Queries: queries, + }, nil +} + +func fieldNameToInstanceDomainColumn(fieldName instance.DomainFieldName) query.Column { + switch fieldName { + case instance.DomainFieldName_DOMAIN_FIELD_NAME_DOMAIN: + return query.InstanceDomainDomainCol + case instance.DomainFieldName_DOMAIN_FIELD_NAME_GENERATED: + return query.InstanceDomainIsGeneratedCol + case instance.DomainFieldName_DOMAIN_FIELD_NAME_PRIMARY: + return query.InstanceDomainIsPrimaryCol + case instance.DomainFieldName_DOMAIN_FIELD_NAME_CREATION_DATE: + return query.InstanceDomainCreationDateCol + case instance.DomainFieldName_DOMAIN_FIELD_NAME_UNSPECIFIED: + fallthrough + default: + return query.Column{} + } +} + +func domainQueriesToModel(queries []*instance.DomainSearchQuery) (_ []query.SearchQuery, err error) { + q := make([]query.SearchQuery, len(queries)) + for i, query := range queries { + q[i], err = domainQueryToModel(query) + if err != nil { + return nil, err + } + } + return q, nil +} + +func domainQueryToModel(searchQuery *instance.DomainSearchQuery) (query.SearchQuery, error) { + switch q := searchQuery.GetQuery().(type) { + case *instance.DomainSearchQuery_DomainQuery: + return query.NewInstanceDomainDomainSearchQuery(object.TextMethodToQuery(q.DomainQuery.GetMethod()), q.DomainQuery.GetDomain()) + case *instance.DomainSearchQuery_GeneratedQuery: + return query.NewInstanceDomainGeneratedSearchQuery(q.GeneratedQuery.GetGenerated()) + case *instance.DomainSearchQuery_PrimaryQuery: + return query.NewInstanceDomainPrimarySearchQuery(q.PrimaryQuery.GetPrimary()) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "INST-Ags42", "List.Query.Invalid") + } +} + +func ListTrustedDomainsRequestToModel(req *instance.ListTrustedDomainsRequest, defaults systemdefaults.SystemDefaults) (*query.InstanceTrustedDomainSearchQueries, error) { + offset, limit, asc, err := filter.PaginationPbToQuery(defaults, req.GetPagination()) + if err != nil { + return nil, err + } + + queries, err := trustedDomainQueriesToModel(req.GetQueries()) + if err != nil { + return nil, err + } + + return &query.InstanceTrustedDomainSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + SortingColumn: fieldNameToInstanceTrustedDomainColumn(req.GetSortingColumn()), + }, + Queries: queries, + }, nil +} + +func trustedDomainQueriesToModel(queries []*instance.TrustedDomainSearchQuery) (_ []query.SearchQuery, err error) { + q := make([]query.SearchQuery, len(queries)) + for i, query := range queries { + q[i], err = trustedDomainQueryToModel(query) + if err != nil { + return nil, err + } + } + return q, nil +} + +func trustedDomainQueryToModel(searchQuery *instance.TrustedDomainSearchQuery) (query.SearchQuery, error) { + switch q := searchQuery.GetQuery().(type) { + case *instance.TrustedDomainSearchQuery_DomainQuery: + return query.NewInstanceTrustedDomainDomainSearchQuery(object.TextMethodToQuery(q.DomainQuery.GetMethod()), q.DomainQuery.GetDomain()) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "INST-Ags42", "List.Query.Invalid") + } +} + +func trustedDomainsToPb(domains []*query.InstanceTrustedDomain) []*instance.TrustedDomain { + d := make([]*instance.TrustedDomain, len(domains)) + for i, domain := range domains { + d[i] = trustedDomainToPb(domain) + } + return d +} + +func trustedDomainToPb(d *query.InstanceTrustedDomain) *instance.TrustedDomain { + return &instance.TrustedDomain{ + Domain: d.Domain, + InstanceId: d.InstanceID, + CreationDate: timestamppb.New(d.CreationDate), + } +} + +func fieldNameToInstanceTrustedDomainColumn(fieldName instance.TrustedDomainFieldName) query.Column { + switch fieldName { + case instance.TrustedDomainFieldName_TRUSTED_DOMAIN_FIELD_NAME_DOMAIN: + return query.InstanceTrustedDomainDomainCol + case instance.TrustedDomainFieldName_TRUSTED_DOMAIN_FIELD_NAME_CREATION_DATE: + return query.InstanceTrustedDomainCreationDateCol + case instance.TrustedDomainFieldName_TRUSTED_DOMAIN_FIELD_NAME_UNSPECIFIED: + fallthrough + default: + return query.Column{} + } +} diff --git a/internal/api/grpc/instance/v2beta/converter_test.go b/internal/api/grpc/instance/v2beta/converter_test.go new file mode 100644 index 0000000000..150678010c --- /dev/null +++ b/internal/api/grpc/instance/v2beta/converter_test.go @@ -0,0 +1,390 @@ +package instance + +import ( + "errors" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/cmd/build" + "github.com/zitadel/zitadel/internal/config/systemdefaults" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/zerrors" + filter "github.com/zitadel/zitadel/pkg/grpc/filter/v2beta" + instance "github.com/zitadel/zitadel/pkg/grpc/instance/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/object/v2" +) + +func Test_InstancesToPb(t *testing.T) { + instances := []*query.Instance{ + { + ID: "instance1", + Name: "Instance One", + Domains: []*query.InstanceDomain{ + { + Domain: "example.com", + IsPrimary: true, + IsGenerated: false, + Sequence: 1, + CreationDate: time.Unix(123, 0), + ChangeDate: time.Unix(124, 0), + InstanceID: "instance1", + }, + }, + Sequence: 1, + CreationDate: time.Unix(123, 0), + ChangeDate: time.Unix(124, 0), + }, + } + + want := []*instance.Instance{ + { + Id: "instance1", + Name: "Instance One", + Domains: []*instance.Domain{ + { + Domain: "example.com", + Primary: true, + Generated: false, + InstanceId: "instance1", + CreationDate: ×tamppb.Timestamp{Seconds: 123}, + }, + }, + Version: build.Version(), + ChangeDate: ×tamppb.Timestamp{Seconds: 124}, + CreationDate: ×tamppb.Timestamp{Seconds: 123}, + }, + } + + got := InstancesToPb(instances) + assert.Equal(t, want, got) +} + +func Test_ListInstancesRequestToModel(t *testing.T) { + t.Parallel() + + searchInstanceByID, err := query.NewInstanceIDsListSearchQuery("instance1", "instance2") + require.Nil(t, err) + + tt := []struct { + testName string + inputRequest *instance.ListInstancesRequest + maxQueryLimit uint64 + expectedResult *query.InstanceSearchQueries + expectedError error + }{ + { + testName: "when query limit exceeds max query limit should return invalid argument error", + maxQueryLimit: 1, + inputRequest: &instance.ListInstancesRequest{ + Pagination: &filter.PaginationRequest{Limit: 10, Offset: 0, Asc: true}, + SortingColumn: instance.FieldName_FIELD_NAME_ID.Enum(), + Queries: []*instance.Query{{Query: &instance.Query_IdQuery{IdQuery: &instance.IdsQuery{Ids: []string{"instance1", "instance2"}}}}}, + }, + expectedError: zerrors.ThrowInvalidArgumentf(errors.New("given: 10, allowed: 1"), "QUERY-4M0fs", "Errors.Query.LimitExceeded"), + }, + { + testName: "when valid request should return instance search query model", + inputRequest: &instance.ListInstancesRequest{ + Pagination: &filter.PaginationRequest{Limit: 10, Offset: 0, Asc: true}, + SortingColumn: instance.FieldName_FIELD_NAME_ID.Enum(), + Queries: []*instance.Query{{Query: &instance.Query_IdQuery{IdQuery: &instance.IdsQuery{Ids: []string{"instance1", "instance2"}}}}}, + }, + expectedResult: &query.InstanceSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: 0, + Limit: 10, + Asc: true, + SortingColumn: query.InstanceColumnID, + }, + Queries: []query.SearchQuery{searchInstanceByID}, + }, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + sysDefaults := systemdefaults.SystemDefaults{MaxQueryLimit: tc.maxQueryLimit} + + got, err := ListInstancesRequestToModel(tc.inputRequest, sysDefaults) + assert.Equal(t, tc.expectedError, err) + assert.Equal(t, tc.expectedResult, got) + + }) + } +} + +func Test_fieldNameToInstanceColumn(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + fieldName instance.FieldName + want query.Column + }{ + { + name: "ID field", + fieldName: instance.FieldName_FIELD_NAME_ID, + want: query.InstanceColumnID, + }, + { + name: "Name field", + fieldName: instance.FieldName_FIELD_NAME_NAME, + want: query.InstanceColumnName, + }, + { + name: "Creation Date field", + fieldName: instance.FieldName_FIELD_NAME_CREATION_DATE, + want: query.InstanceColumnCreationDate, + }, + { + name: "Unknown field", + fieldName: instance.FieldName(99), + want: query.Column{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got := fieldNameToInstanceColumn(tt.fieldName) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_instanceQueryToModel(t *testing.T) { + t.Parallel() + + searchInstanceByID, err := query.NewInstanceIDsListSearchQuery("instance1") + require.Nil(t, err) + + searchInstanceByDomain, err := query.NewInstanceDomainsListSearchQuery("example.com") + require.Nil(t, err) + + tests := []struct { + name string + searchQuery *instance.Query + want query.SearchQuery + wantErr bool + }{ + { + name: "ID Query", + searchQuery: &instance.Query{ + Query: &instance.Query_IdQuery{ + IdQuery: &instance.IdsQuery{ + Ids: []string{"instance1"}, + }, + }, + }, + want: searchInstanceByID, + wantErr: false, + }, + { + name: "Domain Query", + searchQuery: &instance.Query{ + Query: &instance.Query_DomainQuery{ + DomainQuery: &instance.DomainsQuery{ + Domains: []string{"example.com"}, + }, + }, + }, + want: searchInstanceByDomain, + wantErr: false, + }, + { + name: "Invalid Query", + searchQuery: &instance.Query{ + Query: nil, + }, + want: nil, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got, err := instanceQueryToModel(tt.searchQuery) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.want, got) + } + }) + } +} + +func Test_ListCustomDomainsRequestToModel(t *testing.T) { + t.Parallel() + + querySearchRes, err := query.NewInstanceDomainDomainSearchQuery(query.TextEquals, "example.com") + require.Nil(t, err) + + queryGeneratedRes, err := query.NewInstanceDomainGeneratedSearchQuery(false) + require.Nil(t, err) + + tests := []struct { + name string + inputRequest *instance.ListCustomDomainsRequest + maxQueryLimit uint64 + expectedResult *query.InstanceDomainSearchQueries + expectedError error + }{ + { + name: "when query limit exceeds max query limit should return invalid argument error", + inputRequest: &instance.ListCustomDomainsRequest{ + Pagination: &filter.PaginationRequest{Limit: 10, Offset: 0, Asc: true}, + SortingColumn: instance.DomainFieldName_DOMAIN_FIELD_NAME_DOMAIN, + Queries: []*instance.DomainSearchQuery{ + { + Query: &instance.DomainSearchQuery_DomainQuery{ + DomainQuery: &instance.DomainQuery{ + Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS, + Domain: "example.com", + }, + }, + }, + }, + }, + maxQueryLimit: 1, + expectedError: zerrors.ThrowInvalidArgumentf(errors.New("given: 10, allowed: 1"), "QUERY-4M0fs", "Errors.Query.LimitExceeded"), + }, + { + name: "when valid request should return domain search query model", + inputRequest: &instance.ListCustomDomainsRequest{ + Pagination: &filter.PaginationRequest{Limit: 10, Offset: 0, Asc: true}, + SortingColumn: instance.DomainFieldName_DOMAIN_FIELD_NAME_PRIMARY, + Queries: []*instance.DomainSearchQuery{ + { + Query: &instance.DomainSearchQuery_DomainQuery{ + DomainQuery: &instance.DomainQuery{Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS, Domain: "example.com"}}, + }, + { + Query: &instance.DomainSearchQuery_GeneratedQuery{ + GeneratedQuery: &instance.DomainGeneratedQuery{Generated: false}}, + }, + }, + }, + maxQueryLimit: 100, + expectedResult: &query.InstanceDomainSearchQueries{ + SearchRequest: query.SearchRequest{Offset: 0, Limit: 10, Asc: true, SortingColumn: query.InstanceDomainIsPrimaryCol}, + Queries: []query.SearchQuery{ + querySearchRes, + queryGeneratedRes, + }, + }, + expectedError: nil, + }, + { + name: "when invalid query should return error", + inputRequest: &instance.ListCustomDomainsRequest{ + Pagination: &filter.PaginationRequest{Limit: 10, Offset: 0, Asc: true}, + SortingColumn: instance.DomainFieldName_DOMAIN_FIELD_NAME_GENERATED, + Queries: []*instance.DomainSearchQuery{ + { + Query: nil, + }, + }, + }, + maxQueryLimit: 100, + expectedResult: nil, + expectedError: zerrors.ThrowInvalidArgument(nil, "INST-Ags42", "List.Query.Invalid"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + sysDefaults := systemdefaults.SystemDefaults{MaxQueryLimit: tt.maxQueryLimit} + + got, err := ListCustomDomainsRequestToModel(tt.inputRequest, sysDefaults) + assert.Equal(t, tt.expectedError, err) + assert.Equal(t, tt.expectedResult, got) + }) + } +} + +func Test_ListTrustedDomainsRequestToModel(t *testing.T) { + t.Parallel() + + querySearchRes, err := query.NewInstanceTrustedDomainDomainSearchQuery(query.TextEquals, "example.com") + require.Nil(t, err) + + tests := []struct { + name string + inputRequest *instance.ListTrustedDomainsRequest + maxQueryLimit uint64 + expectedResult *query.InstanceTrustedDomainSearchQueries + expectedError error + }{ + { + name: "when query limit exceeds max query limit should return invalid argument error", + inputRequest: &instance.ListTrustedDomainsRequest{ + Pagination: &filter.PaginationRequest{Limit: 10, Offset: 0, Asc: true}, + SortingColumn: instance.TrustedDomainFieldName_TRUSTED_DOMAIN_FIELD_NAME_DOMAIN, + Queries: []*instance.TrustedDomainSearchQuery{ + { + Query: &instance.TrustedDomainSearchQuery_DomainQuery{ + DomainQuery: &instance.DomainQuery{ + Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS, + Domain: "example.com", + }, + }, + }, + }, + }, + maxQueryLimit: 1, + expectedError: zerrors.ThrowInvalidArgumentf(errors.New("given: 10, allowed: 1"), "QUERY-4M0fs", "Errors.Query.LimitExceeded"), + }, + { + name: "when valid request should return domain search query model", + inputRequest: &instance.ListTrustedDomainsRequest{ + Pagination: &filter.PaginationRequest{Limit: 10, Offset: 0, Asc: true}, + SortingColumn: instance.TrustedDomainFieldName_TRUSTED_DOMAIN_FIELD_NAME_CREATION_DATE, + Queries: []*instance.TrustedDomainSearchQuery{ + { + Query: &instance.TrustedDomainSearchQuery_DomainQuery{ + DomainQuery: &instance.DomainQuery{Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS, Domain: "example.com"}}, + }, + }, + }, + maxQueryLimit: 100, + expectedResult: &query.InstanceTrustedDomainSearchQueries{ + SearchRequest: query.SearchRequest{Offset: 0, Limit: 10, Asc: true, SortingColumn: query.InstanceTrustedDomainCreationDateCol}, + Queries: []query.SearchQuery{querySearchRes}, + }, + expectedError: nil, + }, + { + name: "when invalid query should return error", + inputRequest: &instance.ListTrustedDomainsRequest{ + Pagination: &filter.PaginationRequest{Limit: 10, Offset: 0, Asc: true}, + Queries: []*instance.TrustedDomainSearchQuery{ + { + Query: nil, + }, + }, + }, + maxQueryLimit: 100, + expectedResult: nil, + expectedError: zerrors.ThrowInvalidArgument(nil, "INST-Ags42", "List.Query.Invalid"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + sysDefaults := systemdefaults.SystemDefaults{MaxQueryLimit: tt.maxQueryLimit} + + got, err := ListTrustedDomainsRequestToModel(tt.inputRequest, sysDefaults) + assert.Equal(t, tt.expectedError, err) + assert.Equal(t, tt.expectedResult, got) + }) + } +} diff --git a/internal/api/grpc/instance/v2beta/domain.go b/internal/api/grpc/instance/v2beta/domain.go new file mode 100644 index 0000000000..380ebff5a7 --- /dev/null +++ b/internal/api/grpc/instance/v2beta/domain.go @@ -0,0 +1,51 @@ +package instance + +import ( + "context" + + "connectrpc.com/connect" + "google.golang.org/protobuf/types/known/timestamppb" + + instance "github.com/zitadel/zitadel/pkg/grpc/instance/v2beta" +) + +func (s *Server) AddCustomDomain(ctx context.Context, req *connect.Request[instance.AddCustomDomainRequest]) (*connect.Response[instance.AddCustomDomainResponse], error) { + details, err := s.command.AddInstanceDomain(ctx, req.Msg.GetDomain()) + if err != nil { + return nil, err + } + return connect.NewResponse(&instance.AddCustomDomainResponse{ + CreationDate: timestamppb.New(details.CreationDate), + }), nil +} + +func (s *Server) RemoveCustomDomain(ctx context.Context, req *connect.Request[instance.RemoveCustomDomainRequest]) (*connect.Response[instance.RemoveCustomDomainResponse], error) { + details, err := s.command.RemoveInstanceDomain(ctx, req.Msg.GetDomain()) + if err != nil { + return nil, err + } + return connect.NewResponse(&instance.RemoveCustomDomainResponse{ + DeletionDate: timestamppb.New(details.EventDate), + }), nil +} + +func (s *Server) AddTrustedDomain(ctx context.Context, req *connect.Request[instance.AddTrustedDomainRequest]) (*connect.Response[instance.AddTrustedDomainResponse], error) { + details, err := s.command.AddTrustedDomain(ctx, req.Msg.GetDomain()) + if err != nil { + return nil, err + } + return connect.NewResponse(&instance.AddTrustedDomainResponse{ + CreationDate: timestamppb.New(details.CreationDate), + }), nil +} + +func (s *Server) RemoveTrustedDomain(ctx context.Context, req *connect.Request[instance.RemoveTrustedDomainRequest]) (*connect.Response[instance.RemoveTrustedDomainResponse], error) { + details, err := s.command.RemoveTrustedDomain(ctx, req.Msg.GetDomain()) + if err != nil { + return nil, err + } + + return connect.NewResponse(&instance.RemoveTrustedDomainResponse{ + DeletionDate: timestamppb.New(details.EventDate), + }), nil +} diff --git a/internal/api/grpc/instance/v2beta/instance.go b/internal/api/grpc/instance/v2beta/instance.go new file mode 100644 index 0000000000..b3f2d6e478 --- /dev/null +++ b/internal/api/grpc/instance/v2beta/instance.go @@ -0,0 +1,33 @@ +package instance + +import ( + "context" + + "connectrpc.com/connect" + "google.golang.org/protobuf/types/known/timestamppb" + + instance "github.com/zitadel/zitadel/pkg/grpc/instance/v2beta" +) + +func (s *Server) DeleteInstance(ctx context.Context, request *connect.Request[instance.DeleteInstanceRequest]) (*connect.Response[instance.DeleteInstanceResponse], error) { + obj, err := s.command.RemoveInstance(ctx, request.Msg.GetInstanceId()) + if err != nil { + return nil, err + } + + return connect.NewResponse(&instance.DeleteInstanceResponse{ + DeletionDate: timestamppb.New(obj.EventDate), + }), nil + +} + +func (s *Server) UpdateInstance(ctx context.Context, request *connect.Request[instance.UpdateInstanceRequest]) (*connect.Response[instance.UpdateInstanceResponse], error) { + obj, err := s.command.UpdateInstance(ctx, request.Msg.GetInstanceName()) + if err != nil { + return nil, err + } + + return connect.NewResponse(&instance.UpdateInstanceResponse{ + ChangeDate: timestamppb.New(obj.EventDate), + }), nil +} diff --git a/internal/api/grpc/instance/v2beta/integration_test/domain_test.go b/internal/api/grpc/instance/v2beta/integration_test/domain_test.go new file mode 100644 index 0000000000..a0e2011cfc --- /dev/null +++ b/internal/api/grpc/instance/v2beta/integration_test/domain_test.go @@ -0,0 +1,350 @@ +//go:build integration + +package instance_test + +import ( + "context" + "strings" + "testing" + "time" + + "github.com/brianvoe/gofakeit/v6" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/zitadel/zitadel/internal/integration" + instance "github.com/zitadel/zitadel/pkg/grpc/instance/v2beta" +) + +func TestAddCustomDomain(t *testing.T) { + t.Parallel() + + // Given + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) + defer cancel() + + ctxWithSysAuthZ := integration.WithSystemAuthorization(ctx) + + inst := integration.NewInstance(ctxWithSysAuthZ) + iamOwnerCtx := inst.WithAuthorization(context.Background(), integration.UserTypeIAMOwner) + + t.Cleanup(func() { + inst.Client.InstanceV2Beta.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: inst.ID()}) + }) + + tt := []struct { + testName string + inputContext context.Context + inputRequest *instance.AddCustomDomainRequest + expectedErrorMsg string + expectedErrorCode codes.Code + }{ + { + testName: "when invalid context should return unauthN error", + inputRequest: &instance.AddCustomDomainRequest{ + InstanceId: inst.ID(), + Domain: gofakeit.DomainName(), + }, + inputContext: context.Background(), + expectedErrorCode: codes.Unauthenticated, + expectedErrorMsg: "auth header missing", + }, + { + testName: "when unauthZ context should return unauthZ error", + inputRequest: &instance.AddCustomDomainRequest{ + InstanceId: inst.ID(), + Domain: gofakeit.DomainName(), + }, + inputContext: iamOwnerCtx, + expectedErrorCode: codes.PermissionDenied, + expectedErrorMsg: "No matching permissions found (AUTH-5mWD2)", + }, + { + testName: "when invalid domain should return invalid argument error", + inputRequest: &instance.AddCustomDomainRequest{ + InstanceId: inst.ID(), + Domain: " ", + }, + inputContext: ctxWithSysAuthZ, + expectedErrorCode: codes.InvalidArgument, + expectedErrorMsg: "Errors.Invalid.Argument (INST-28nlD)", + }, + { + testName: "when valid request should return successful response", + inputRequest: &instance.AddCustomDomainRequest{ + InstanceId: inst.ID(), + Domain: " " + gofakeit.DomainName(), + }, + inputContext: ctxWithSysAuthZ, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Cleanup(func() { + if tc.expectedErrorMsg == "" { + inst.Client.InstanceV2Beta.RemoveCustomDomain(ctxWithSysAuthZ, &instance.RemoveCustomDomainRequest{Domain: strings.TrimSpace(tc.inputRequest.Domain)}) + } + }) + + // Test + res, err := inst.Client.InstanceV2Beta.AddCustomDomain(tc.inputContext, tc.inputRequest) + + // Verify + assert.Equal(t, tc.expectedErrorCode, status.Code(err)) + assert.Equal(t, tc.expectedErrorMsg, status.Convert(err).Message()) + + if tc.expectedErrorMsg == "" { + assert.NotNil(t, res) + assert.NotEmpty(t, res.GetCreationDate()) + } + }) + } +} + +func TestRemoveCustomDomain(t *testing.T) { + t.Parallel() + + // Given + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) + defer cancel() + + ctxWithSysAuthZ := integration.WithSystemAuthorization(ctx) + inst := integration.NewInstance(ctxWithSysAuthZ) + iamOwnerCtx := inst.WithAuthorization(context.Background(), integration.UserTypeIAMOwner) + + customDomain := gofakeit.DomainName() + + _, err := inst.Client.InstanceV2Beta.AddCustomDomain(ctxWithSysAuthZ, &instance.AddCustomDomainRequest{InstanceId: inst.ID(), Domain: customDomain}) + require.Nil(t, err) + + t.Cleanup(func() { + inst.Client.InstanceV2Beta.RemoveCustomDomain(ctxWithSysAuthZ, &instance.RemoveCustomDomainRequest{InstanceId: inst.ID(), Domain: customDomain}) + inst.Client.InstanceV2Beta.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: inst.ID()}) + }) + + tt := []struct { + testName string + inputContext context.Context + inputRequest *instance.RemoveCustomDomainRequest + expectedErrorMsg string + expectedErrorCode codes.Code + }{ + { + testName: "when invalid context should return unauthN error", + inputRequest: &instance.RemoveCustomDomainRequest{ + InstanceId: inst.ID(), + Domain: "custom1", + }, + inputContext: context.Background(), + expectedErrorCode: codes.Unauthenticated, + expectedErrorMsg: "auth header missing", + }, + { + testName: "when unauthZ context should return unauthZ error", + inputRequest: &instance.RemoveCustomDomainRequest{ + InstanceId: inst.ID(), + Domain: "custom1", + }, + inputContext: iamOwnerCtx, + expectedErrorCode: codes.PermissionDenied, + expectedErrorMsg: "No matching permissions found (AUTH-5mWD2)", + }, + { + testName: "when invalid domain should return invalid argument error", + inputRequest: &instance.RemoveCustomDomainRequest{ + InstanceId: inst.ID(), + Domain: " ", + }, + inputContext: ctxWithSysAuthZ, + expectedErrorCode: codes.InvalidArgument, + expectedErrorMsg: "Errors.Invalid.Argument (INST-39nls)", + }, + { + testName: "when valid request should return successful response", + inputRequest: &instance.RemoveCustomDomainRequest{ + InstanceId: inst.ID(), + Domain: " " + customDomain, + }, + inputContext: ctxWithSysAuthZ, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + // Test + res, err := inst.Client.InstanceV2Beta.RemoveCustomDomain(tc.inputContext, tc.inputRequest) + + // Verify + assert.Equal(t, tc.expectedErrorCode, status.Code(err)) + assert.Equal(t, tc.expectedErrorMsg, status.Convert(err).Message()) + + if tc.expectedErrorMsg == "" { + assert.NotNil(t, res) + assert.NotEmpty(t, res.GetDeletionDate()) + } + }) + } +} + +func TestAddTrustedDomain(t *testing.T) { + t.Parallel() + + // Given + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) + defer cancel() + + ctxWithSysAuthZ := integration.WithSystemAuthorization(ctx) + inst := integration.NewInstance(ctxWithSysAuthZ) + orgOwnerCtx := inst.WithAuthorization(context.Background(), integration.UserTypeOrgOwner) + + t.Cleanup(func() { + inst.Client.InstanceV2Beta.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: inst.ID()}) + }) + + tt := []struct { + testName string + inputContext context.Context + inputRequest *instance.AddTrustedDomainRequest + expectedErrorMsg string + expectedErrorCode codes.Code + }{ + { + testName: "when invalid context should return unauthN error", + inputRequest: &instance.AddTrustedDomainRequest{ + InstanceId: inst.ID(), + Domain: "trusted1", + }, + inputContext: context.Background(), + expectedErrorCode: codes.Unauthenticated, + expectedErrorMsg: "auth header missing", + }, + { + testName: "when unauthZ context should return unauthZ error", + inputRequest: &instance.AddTrustedDomainRequest{ + InstanceId: inst.ID(), + Domain: "trusted1", + }, + inputContext: orgOwnerCtx, + expectedErrorCode: codes.PermissionDenied, + expectedErrorMsg: "No matching permissions found (AUTH-5mWD2)", + }, + { + testName: "when invalid domain should return invalid argument error", + inputRequest: &instance.AddTrustedDomainRequest{ + InstanceId: inst.ID(), + Domain: " ", + }, + inputContext: ctxWithSysAuthZ, + expectedErrorCode: codes.InvalidArgument, + expectedErrorMsg: "Errors.Invalid.Argument (COMMA-Stk21)", + }, + { + testName: "when valid request should return successful response", + inputRequest: &instance.AddTrustedDomainRequest{ + InstanceId: inst.ID(), + Domain: " " + gofakeit.DomainName(), + }, + inputContext: ctxWithSysAuthZ, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Cleanup(func() { + if tc.expectedErrorMsg == "" { + inst.Client.InstanceV2Beta.RemoveTrustedDomain(ctxWithSysAuthZ, &instance.RemoveTrustedDomainRequest{Domain: strings.TrimSpace(tc.inputRequest.Domain)}) + } + }) + + // Test + res, err := inst.Client.InstanceV2Beta.AddTrustedDomain(tc.inputContext, tc.inputRequest) + + // Verify + assert.Equal(t, tc.expectedErrorCode, status.Code(err)) + assert.Equal(t, tc.expectedErrorMsg, status.Convert(err).Message()) + + if tc.expectedErrorMsg == "" { + assert.NotNil(t, res) + assert.NotEmpty(t, res.GetCreationDate()) + } + }) + } +} + +func TestRemoveTrustedDomain(t *testing.T) { + t.Parallel() + + // Given + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) + defer cancel() + + ctxWithSysAuthZ := integration.WithSystemAuthorization(ctx) + inst := integration.NewInstance(ctxWithSysAuthZ) + orgOwnerCtx := inst.WithAuthorization(context.Background(), integration.UserTypeOrgOwner) + + trustedDomain := gofakeit.DomainName() + + _, err := inst.Client.InstanceV2Beta.AddTrustedDomain(ctxWithSysAuthZ, &instance.AddTrustedDomainRequest{InstanceId: inst.ID(), Domain: trustedDomain}) + require.Nil(t, err) + + t.Cleanup(func() { + inst.Client.InstanceV2Beta.RemoveTrustedDomain(ctxWithSysAuthZ, &instance.RemoveTrustedDomainRequest{InstanceId: inst.ID(), Domain: trustedDomain}) + inst.Client.InstanceV2Beta.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: inst.ID()}) + }) + + tt := []struct { + testName string + inputContext context.Context + inputRequest *instance.RemoveTrustedDomainRequest + expectedErrorMsg string + expectedErrorCode codes.Code + }{ + { + testName: "when invalid context should return unauthN error", + inputRequest: &instance.RemoveTrustedDomainRequest{ + InstanceId: inst.ID(), + Domain: "trusted1", + }, + inputContext: context.Background(), + expectedErrorCode: codes.Unauthenticated, + expectedErrorMsg: "auth header missing", + }, + { + testName: "when unauthZ context should return unauthZ error", + inputRequest: &instance.RemoveTrustedDomainRequest{ + InstanceId: inst.ID(), + Domain: "trusted1", + }, + inputContext: orgOwnerCtx, + expectedErrorCode: codes.PermissionDenied, + expectedErrorMsg: "No matching permissions found (AUTH-5mWD2)", + }, + { + testName: "when valid request should return successful response", + inputRequest: &instance.RemoveTrustedDomainRequest{ + InstanceId: inst.ID(), + Domain: " " + trustedDomain, + }, + inputContext: ctxWithSysAuthZ, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + // Test + res, err := inst.Client.InstanceV2Beta.RemoveTrustedDomain(tc.inputContext, tc.inputRequest) + + // Verify + assert.Equal(t, tc.expectedErrorCode, status.Code(err)) + assert.Equal(t, tc.expectedErrorMsg, status.Convert(err).Message()) + + if tc.expectedErrorMsg == "" { + require.NotNil(t, res) + require.NotEmpty(t, res.GetDeletionDate()) + } + }) + } +} diff --git a/internal/api/grpc/instance/v2beta/integration_test/instance_test.go b/internal/api/grpc/instance/v2beta/integration_test/instance_test.go new file mode 100644 index 0000000000..ae277c6d13 --- /dev/null +++ b/internal/api/grpc/instance/v2beta/integration_test/instance_test.go @@ -0,0 +1,163 @@ +//go:build integration + +package instance_test + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/zitadel/zitadel/internal/integration" + instance "github.com/zitadel/zitadel/pkg/grpc/instance/v2beta" +) + +func TestDeleteInstace(t *testing.T) { + t.Parallel() + + // Given + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) + defer cancel() + + ctxWithSysAuthZ := integration.WithSystemAuthorization(ctx) + + inst := integration.NewInstance(ctxWithSysAuthZ) + + t.Cleanup(func() { + inst.Client.InstanceV2Beta.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: inst.ID()}) + }) + + tt := []struct { + testName string + inputRequest *instance.DeleteInstanceRequest + inputContext context.Context + expectedErrorMsg string + expectedErrorCode codes.Code + expectedInstanceID string + }{ + { + testName: "when invalid context should return unauthN error", + inputRequest: &instance.DeleteInstanceRequest{ + InstanceId: " ", + }, + inputContext: context.Background(), + expectedErrorCode: codes.Unauthenticated, + expectedErrorMsg: "auth header missing", + }, + { + testName: "when invalid input should return invalid argument error", + inputRequest: &instance.DeleteInstanceRequest{ + InstanceId: inst.ID() + "invalid", + }, + inputContext: ctxWithSysAuthZ, + expectedErrorCode: codes.NotFound, + expectedErrorMsg: "Instance not found (COMMA-AE3GS)", + }, + { + testName: "when delete succeeds should return deletion date", + inputRequest: &instance.DeleteInstanceRequest{ + InstanceId: inst.ID(), + }, + inputContext: ctxWithSysAuthZ, + expectedInstanceID: inst.ID(), + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + // Test + res, err := inst.Client.InstanceV2Beta.DeleteInstance(tc.inputContext, tc.inputRequest) + + // Verify + assert.Equal(t, tc.expectedErrorCode, status.Code(err)) + assert.Equal(t, tc.expectedErrorMsg, status.Convert(err).Message()) + if tc.expectedErrorMsg == "" { + require.NotNil(t, res) + require.NotEmpty(t, res.GetDeletionDate()) + } + }) + } +} + +func TestUpdateInstace(t *testing.T) { + t.Parallel() + + // Given + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) + defer cancel() + + ctxWithSysAuthZ := integration.WithSystemAuthorization(ctx) + + inst := integration.NewInstance(ctxWithSysAuthZ) + orgOwnerCtx := inst.WithAuthorization(context.Background(), integration.UserTypeOrgOwner) + + t.Cleanup(func() { + inst.Client.InstanceV2Beta.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: inst.ID()}) + }) + + tt := []struct { + testName string + inputRequest *instance.UpdateInstanceRequest + inputContext context.Context + expectedErrorMsg string + expectedErrorCode codes.Code + expectedNewName string + }{ + { + testName: "when invalid context should return unauthN error", + inputRequest: &instance.UpdateInstanceRequest{ + InstanceId: inst.ID(), + InstanceName: " ", + }, + inputContext: context.Background(), + expectedErrorCode: codes.Unauthenticated, + expectedErrorMsg: "auth header missing", + }, + { + testName: "when unauthZ context should return unauthZ error", + inputRequest: &instance.UpdateInstanceRequest{ + InstanceId: inst.ID(), + InstanceName: " ", + }, + inputContext: orgOwnerCtx, + expectedErrorCode: codes.PermissionDenied, + expectedErrorMsg: "No matching permissions found (AUTH-5mWD2)", + }, + { + testName: "when update succeeds should change instance name", + inputRequest: &instance.UpdateInstanceRequest{ + InstanceId: inst.ID(), + InstanceName: "an-updated-name", + }, + inputContext: ctxWithSysAuthZ, + expectedNewName: "an-updated-name", + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + // Test + res, err := inst.Client.InstanceV2Beta.UpdateInstance(tc.inputContext, tc.inputRequest) + + // Verify + assert.Equal(t, tc.expectedErrorCode, status.Code(err)) + assert.Equal(t, tc.expectedErrorMsg, status.Convert(err).Message()) + if tc.expectedErrorMsg == "" { + + require.NotNil(t, res) + assert.NotEmpty(t, res.GetChangeDate()) + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tc.inputContext, 20*time.Second) + require.EventuallyWithT(t, func(tt *assert.CollectT) { + retrievedInstance, err := inst.Client.InstanceV2Beta.GetInstance(tc.inputContext, &instance.GetInstanceRequest{InstanceId: inst.ID()}) + require.Nil(tt, err) + assert.Equal(tt, tc.expectedNewName, retrievedInstance.GetInstance().GetName()) + }, retryDuration, tick, "timeout waiting for expected execution result") + } + }) + } +} diff --git a/internal/api/grpc/instance/v2beta/integration_test/query_test.go b/internal/api/grpc/instance/v2beta/integration_test/query_test.go new file mode 100644 index 0000000000..e59a16a932 --- /dev/null +++ b/internal/api/grpc/instance/v2beta/integration_test/query_test.go @@ -0,0 +1,370 @@ +//go:build integration + +package instance_test + +import ( + "context" + "slices" + "testing" + "time" + + "github.com/brianvoe/gofakeit/v6" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/zitadel/zitadel/internal/integration" + filter "github.com/zitadel/zitadel/pkg/grpc/filter/v2beta" + instance "github.com/zitadel/zitadel/pkg/grpc/instance/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/object/v2" +) + +func TestGetInstance(t *testing.T) { + t.Parallel() + + // Given + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) + defer cancel() + + ctxWithSysAuthZ := integration.WithSystemAuthorization(ctx) + inst := integration.NewInstance(ctxWithSysAuthZ) + orgOwnerCtx := inst.WithAuthorization(context.Background(), integration.UserTypeOrgOwner) + + t.Cleanup(func() { + inst.Client.InstanceV2Beta.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: inst.ID()}) + }) + + tt := []struct { + testName string + inputContext context.Context + expectedInstanceID string + expectedErrorMsg string + expectedErrorCode codes.Code + }{ + { + testName: "when unauthN context should return unauthN error", + inputContext: context.Background(), + expectedErrorCode: codes.Unauthenticated, + expectedErrorMsg: "auth header missing", + }, + { + testName: "when unauthZ context should return unauthZ error", + inputContext: orgOwnerCtx, + expectedErrorCode: codes.PermissionDenied, + expectedErrorMsg: "No matching permissions found (AUTH-5mWD2)", + }, + { + testName: "when request succeeds should return matching instance", + inputContext: ctxWithSysAuthZ, + expectedInstanceID: inst.ID(), + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + // Test + res, err := inst.Client.InstanceV2Beta.GetInstance(tc.inputContext, &instance.GetInstanceRequest{InstanceId: inst.ID()}) + + // Verify + assert.Equal(t, tc.expectedErrorCode, status.Code(err)) + assert.Equal(t, tc.expectedErrorMsg, status.Convert(err).Message()) + + if tc.expectedErrorMsg == "" { + require.NoError(t, err) + assert.Equal(t, tc.expectedInstanceID, res.GetInstance().GetId()) + } + }) + } +} + +func TestListInstances(t *testing.T) { + t.Parallel() + + // Given + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) + defer cancel() + + ctxWithSysAuthZ := integration.WithSystemAuthorization(ctx) + + instances := make([]*integration.Instance, 2) + inst := integration.NewInstance(ctxWithSysAuthZ) + inst2 := integration.NewInstance(ctxWithSysAuthZ) + instances[0], instances[1] = inst, inst2 + + t.Cleanup(func() { + inst.Client.InstanceV2Beta.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: inst.ID()}) + inst.Client.InstanceV2Beta.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: inst2.ID()}) + }) + + // Sort in descending order + slices.SortFunc(instances, func(i1, i2 *integration.Instance) int { + res := i1.Instance.Details.CreationDate.AsTime().Compare(i2.Instance.Details.CreationDate.AsTime()) + if res == 0 { + return res + } + return -res + }) + + orgOwnerCtx := inst.WithAuthorization(context.Background(), integration.UserTypeOrgOwner) + + tt := []struct { + testName string + inputRequest *instance.ListInstancesRequest + inputContext context.Context + expectedErrorMsg string + expectedErrorCode codes.Code + expectedInstances []string + }{ + { + testName: "when invalid context should return unauthN error", + inputRequest: &instance.ListInstancesRequest{ + Pagination: &filter.PaginationRequest{Offset: 0, Limit: 10}, + }, + inputContext: context.Background(), + expectedErrorCode: codes.Unauthenticated, + expectedErrorMsg: "auth header missing", + }, + { + testName: "when unauthZ context should return unauthZ error", + inputRequest: &instance.ListInstancesRequest{ + Pagination: &filter.PaginationRequest{Offset: 0, Limit: 10}, + }, + inputContext: orgOwnerCtx, + expectedErrorCode: codes.PermissionDenied, + expectedErrorMsg: "No matching permissions found (AUTH-5mWD2)", + }, + { + testName: "when valid request with filter should return paginated response", + inputRequest: &instance.ListInstancesRequest{ + Pagination: &filter.PaginationRequest{Offset: 0, Limit: 10}, + SortingColumn: instance.FieldName_FIELD_NAME_CREATION_DATE.Enum(), + Queries: []*instance.Query{ + { + Query: &instance.Query_IdQuery{ + IdQuery: &instance.IdsQuery{ + Ids: []string{inst.ID(), inst2.ID()}, + }, + }, + }, + }, + }, + inputContext: ctxWithSysAuthZ, + expectedInstances: []string{inst2.ID(), inst.ID()}, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + // Test + res, err := inst.Client.InstanceV2Beta.ListInstances(tc.inputContext, tc.inputRequest) + + // Verify + assert.Equal(t, tc.expectedErrorCode, status.Code(err)) + assert.Equal(t, tc.expectedErrorMsg, status.Convert(err).Message()) + + if tc.expectedErrorMsg == "" { + require.NotNil(t, res) + + require.Len(t, res.GetInstances(), len(tc.expectedInstances)) + + for i, ins := range res.GetInstances() { + assert.Equal(t, tc.expectedInstances[i], ins.GetId()) + } + } + }) + } +} + +func TestListCustomDomains(t *testing.T) { + t.Parallel() + + // Given + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) + defer cancel() + + ctxWithSysAuthZ := integration.WithSystemAuthorization(ctx) + inst := integration.NewInstance(ctxWithSysAuthZ) + + orgOwnerCtx := inst.WithAuthorization(context.Background(), integration.UserTypeOrgOwner) + d1, d2 := "custom."+gofakeit.DomainName(), "custom."+gofakeit.DomainName() + + _, err := inst.Client.InstanceV2Beta.AddCustomDomain(ctxWithSysAuthZ, &instance.AddCustomDomainRequest{InstanceId: inst.ID(), Domain: d1}) + require.Nil(t, err) + _, err = inst.Client.InstanceV2Beta.AddCustomDomain(ctxWithSysAuthZ, &instance.AddCustomDomainRequest{InstanceId: inst.ID(), Domain: d2}) + require.Nil(t, err) + + t.Cleanup(func() { + inst.Client.InstanceV2Beta.RemoveCustomDomain(ctxWithSysAuthZ, &instance.RemoveCustomDomainRequest{InstanceId: inst.ID(), Domain: d1}) + inst.Client.InstanceV2Beta.RemoveCustomDomain(ctxWithSysAuthZ, &instance.RemoveCustomDomainRequest{InstanceId: inst.ID(), Domain: d2}) + inst.Client.InstanceV2Beta.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: inst.ID()}) + }) + + tt := []struct { + testName string + inputRequest *instance.ListCustomDomainsRequest + inputContext context.Context + expectedErrorMsg string + expectedErrorCode codes.Code + expectedDomains []string + }{ + { + testName: "when invalid context should return unauthN error", + inputRequest: &instance.ListCustomDomainsRequest{ + InstanceId: inst.ID(), + Pagination: &filter.PaginationRequest{Offset: 0, Limit: 10}, + }, + inputContext: context.Background(), + expectedErrorCode: codes.Unauthenticated, + expectedErrorMsg: "auth header missing"}, + { + testName: "when unauthZ context should return unauthZ error", + inputRequest: &instance.ListCustomDomainsRequest{ + InstanceId: inst.ID(), + Pagination: &filter.PaginationRequest{Offset: 0, Limit: 10}, + SortingColumn: instance.DomainFieldName_DOMAIN_FIELD_NAME_CREATION_DATE, + Queries: []*instance.DomainSearchQuery{ + { + Query: &instance.DomainSearchQuery_DomainQuery{ + DomainQuery: &instance.DomainQuery{Domain: "custom", Method: object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS}, + }, + }, + }, + }, + inputContext: orgOwnerCtx, + expectedErrorCode: codes.PermissionDenied, + expectedErrorMsg: "No matching permissions found (AUTH-5mWD2)", + }, + { + testName: "when valid request with filter should return paginated response", + inputRequest: &instance.ListCustomDomainsRequest{ + InstanceId: inst.ID(), + Pagination: &filter.PaginationRequest{Offset: 0, Limit: 10}, + SortingColumn: instance.DomainFieldName_DOMAIN_FIELD_NAME_CREATION_DATE, + Queries: []*instance.DomainSearchQuery{ + { + Query: &instance.DomainSearchQuery_DomainQuery{ + DomainQuery: &instance.DomainQuery{Domain: "custom", Method: object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS}, + }, + }, + }, + }, + inputContext: ctxWithSysAuthZ, + expectedDomains: []string{d1, d2}, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + // Test + res, err := inst.Client.InstanceV2Beta.ListCustomDomains(tc.inputContext, tc.inputRequest) + + // Verify + assert.Equal(t, tc.expectedErrorCode, status.Code(err)) + assert.Equal(t, tc.expectedErrorMsg, status.Convert(err).Message()) + + if tc.expectedErrorMsg == "" { + domains := []string{} + for _, d := range res.GetDomains() { + domains = append(domains, d.GetDomain()) + } + + assert.Subset(t, domains, tc.expectedDomains) + } + }) + } +} + +func TestListTrustedDomains(t *testing.T) { + t.Parallel() + + // Given + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) + defer cancel() + + ctxWithSysAuthZ := integration.WithSystemAuthorization(ctx) + inst := integration.NewInstance(ctxWithSysAuthZ) + + orgOwnerCtx := inst.WithAuthorization(context.Background(), integration.UserTypeOrgOwner) + d1, d2 := "trusted."+gofakeit.DomainName(), "trusted."+gofakeit.DomainName() + + _, err := inst.Client.InstanceV2Beta.AddTrustedDomain(ctxWithSysAuthZ, &instance.AddTrustedDomainRequest{InstanceId: inst.ID(), Domain: d1}) + require.Nil(t, err) + _, err = inst.Client.InstanceV2Beta.AddTrustedDomain(ctxWithSysAuthZ, &instance.AddTrustedDomainRequest{InstanceId: inst.ID(), Domain: d2}) + require.Nil(t, err) + + t.Cleanup(func() { + inst.Client.InstanceV2Beta.RemoveTrustedDomain(ctxWithSysAuthZ, &instance.RemoveTrustedDomainRequest{InstanceId: inst.ID(), Domain: d1}) + inst.Client.InstanceV2Beta.RemoveTrustedDomain(ctxWithSysAuthZ, &instance.RemoveTrustedDomainRequest{InstanceId: inst.ID(), Domain: d2}) + inst.Client.InstanceV2Beta.DeleteInstance(ctxWithSysAuthZ, &instance.DeleteInstanceRequest{InstanceId: inst.ID()}) + }) + + tt := []struct { + testName string + inputRequest *instance.ListTrustedDomainsRequest + inputContext context.Context + expectedErrorMsg string + expectedErrorCode codes.Code + expectedDomains []string + }{ + { + testName: "when invalid context should return unauthN error", + inputRequest: &instance.ListTrustedDomainsRequest{ + InstanceId: inst.ID(), + Pagination: &filter.PaginationRequest{Offset: 0, Limit: 10}, + }, + inputContext: context.Background(), + expectedErrorCode: codes.Unauthenticated, + expectedErrorMsg: "auth header missing", + }, + { + testName: "when unauthZ context should return unauthZ error", + inputRequest: &instance.ListTrustedDomainsRequest{ + InstanceId: inst.ID(), + Pagination: &filter.PaginationRequest{Offset: 0, Limit: 10}, + }, + inputContext: orgOwnerCtx, + expectedErrorCode: codes.PermissionDenied, + expectedErrorMsg: "No matching permissions found (AUTH-5mWD2)", + }, + { + testName: "when valid request with filter should return paginated response", + inputRequest: &instance.ListTrustedDomainsRequest{ + InstanceId: inst.ID(), + Pagination: &filter.PaginationRequest{Offset: 0, Limit: 10}, + SortingColumn: instance.TrustedDomainFieldName_TRUSTED_DOMAIN_FIELD_NAME_CREATION_DATE, + Queries: []*instance.TrustedDomainSearchQuery{ + { + Query: &instance.TrustedDomainSearchQuery_DomainQuery{ + DomainQuery: &instance.DomainQuery{Domain: "trusted", Method: object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS}, + }, + }, + }, + }, + inputContext: ctxWithSysAuthZ, + expectedDomains: []string{d1, d2}, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + // Test + res, err := inst.Client.InstanceV2Beta.ListTrustedDomains(tc.inputContext, tc.inputRequest) + + // Verify + assert.Equal(t, tc.expectedErrorCode, status.Code(err)) + assert.Equal(t, tc.expectedErrorMsg, status.Convert(err).Message()) + + if tc.expectedErrorMsg == "" { + require.NotNil(t, res) + + domains := []string{} + for _, d := range res.GetTrustedDomain() { + domains = append(domains, d.GetDomain()) + } + + assert.Subset(t, domains, tc.expectedDomains) + } + }) + } +} diff --git a/internal/api/grpc/instance/v2beta/query.go b/internal/api/grpc/instance/v2beta/query.go new file mode 100644 index 0000000000..10716ffda0 --- /dev/null +++ b/internal/api/grpc/instance/v2beta/query.go @@ -0,0 +1,72 @@ +package instance + +import ( + "context" + + "connectrpc.com/connect" + + filter "github.com/zitadel/zitadel/internal/api/grpc/filter/v2beta" + instance "github.com/zitadel/zitadel/pkg/grpc/instance/v2beta" +) + +func (s *Server) GetInstance(ctx context.Context, _ *connect.Request[instance.GetInstanceRequest]) (*connect.Response[instance.GetInstanceResponse], error) { + inst, err := s.query.Instance(ctx, true) + if err != nil { + return nil, err + } + + return connect.NewResponse(&instance.GetInstanceResponse{ + Instance: ToProtoObject(inst), + }), nil +} + +func (s *Server) ListInstances(ctx context.Context, req *connect.Request[instance.ListInstancesRequest]) (*connect.Response[instance.ListInstancesResponse], error) { + queries, err := ListInstancesRequestToModel(req.Msg, s.systemDefaults) + if err != nil { + return nil, err + } + + instances, err := s.query.SearchInstances(ctx, queries) + if err != nil { + return nil, err + } + + return connect.NewResponse(&instance.ListInstancesResponse{ + Instances: InstancesToPb(instances.Instances), + Pagination: filter.QueryToPaginationPb(queries.SearchRequest, instances.SearchResponse), + }), nil +} + +func (s *Server) ListCustomDomains(ctx context.Context, req *connect.Request[instance.ListCustomDomainsRequest]) (*connect.Response[instance.ListCustomDomainsResponse], error) { + queries, err := ListCustomDomainsRequestToModel(req.Msg, s.systemDefaults) + if err != nil { + return nil, err + } + + domains, err := s.query.SearchInstanceDomains(ctx, queries) + if err != nil { + return nil, err + } + + return connect.NewResponse(&instance.ListCustomDomainsResponse{ + Domains: DomainsToPb(domains.Domains), + Pagination: filter.QueryToPaginationPb(queries.SearchRequest, domains.SearchResponse), + }), nil +} + +func (s *Server) ListTrustedDomains(ctx context.Context, req *connect.Request[instance.ListTrustedDomainsRequest]) (*connect.Response[instance.ListTrustedDomainsResponse], error) { + queries, err := ListTrustedDomainsRequestToModel(req.Msg, s.systemDefaults) + if err != nil { + return nil, err + } + + domains, err := s.query.SearchInstanceTrustedDomains(ctx, queries) + if err != nil { + return nil, err + } + + return connect.NewResponse(&instance.ListTrustedDomainsResponse{ + TrustedDomain: trustedDomainsToPb(domains.Domains), + Pagination: filter.QueryToPaginationPb(queries.SearchRequest, domains.SearchResponse), + }), nil +} diff --git a/internal/api/grpc/instance/v2beta/server.go b/internal/api/grpc/instance/v2beta/server.go new file mode 100644 index 0000000000..1fb3513dd6 --- /dev/null +++ b/internal/api/grpc/instance/v2beta/server.go @@ -0,0 +1,67 @@ +package instance + +import ( + "net/http" + + "connectrpc.com/connect" + "google.golang.org/protobuf/reflect/protoreflect" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/api/grpc/server" + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/config/systemdefaults" + "github.com/zitadel/zitadel/internal/query" + instance "github.com/zitadel/zitadel/pkg/grpc/instance/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/instance/v2beta/instanceconnect" +) + +var _ instanceconnect.InstanceServiceHandler = (*Server)(nil) + +type Server struct { + command *command.Commands + query *query.Queries + systemDefaults systemdefaults.SystemDefaults + defaultInstance command.InstanceSetup + externalDomain string +} + +type Config struct{} + +func CreateServer( + command *command.Commands, + query *query.Queries, + database string, + defaultInstance command.InstanceSetup, + externalDomain string, +) *Server { + return &Server{ + command: command, + query: query, + defaultInstance: defaultInstance, + externalDomain: externalDomain, + } +} + +func (s *Server) RegisterConnectServer(interceptors ...connect.Interceptor) (string, http.Handler) { + return instanceconnect.NewInstanceServiceHandler(s, connect.WithInterceptors(interceptors...)) +} + +func (s *Server) FileDescriptor() protoreflect.FileDescriptor { + return instance.File_zitadel_instance_v2beta_instance_service_proto +} + +func (s *Server) AppName() string { + return instance.InstanceService_ServiceDesc.ServiceName +} + +func (s *Server) MethodPrefix() string { + return instance.InstanceService_ServiceDesc.ServiceName +} + +func (s *Server) AuthMethods() authz.MethodMapping { + return instance.InstanceService_AuthMethods +} + +func (s *Server) RegisterGateway() server.RegisterGatewayFunc { + return instance.RegisterInstanceServiceHandler +} diff --git a/internal/api/grpc/internal_permission/v2beta/administrator.go b/internal/api/grpc/internal_permission/v2beta/administrator.go new file mode 100644 index 0000000000..86ee7d9454 --- /dev/null +++ b/internal/api/grpc/internal_permission/v2beta/administrator.go @@ -0,0 +1,220 @@ +package internal_permission + +import ( + "context" + + "connectrpc.com/connect" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/zerrors" + internal_permission "github.com/zitadel/zitadel/pkg/grpc/internal_permission/v2beta" +) + +func (s *Server) CreateAdministrator(ctx context.Context, req *connect.Request[internal_permission.CreateAdministratorRequest]) (*connect.Response[internal_permission.CreateAdministratorResponse], error) { + var creationDate *timestamppb.Timestamp + + switch resource := req.Msg.GetResource().GetResource().(type) { + case *internal_permission.ResourceType_Instance: + if resource.Instance { + member, err := s.command.AddInstanceMember(ctx, createAdministratorInstanceToCommand(authz.GetInstance(ctx).InstanceID(), req.Msg.UserId, req.Msg.Roles)) + if err != nil { + return nil, err + } + if !member.EventDate.IsZero() { + creationDate = timestamppb.New(member.EventDate) + } + } + case *internal_permission.ResourceType_OrganizationId: + member, err := s.command.AddOrgMember(ctx, createAdministratorOrganizationToCommand(resource, req.Msg.UserId, req.Msg.Roles)) + if err != nil { + return nil, err + } + if !member.EventDate.IsZero() { + creationDate = timestamppb.New(member.EventDate) + } + case *internal_permission.ResourceType_ProjectId: + member, err := s.command.AddProjectMember(ctx, createAdministratorProjectToCommand(resource, req.Msg.UserId, req.Msg.Roles)) + if err != nil { + return nil, err + } + if !member.EventDate.IsZero() { + creationDate = timestamppb.New(member.EventDate) + } + case *internal_permission.ResourceType_ProjectGrant_: + member, err := s.command.AddProjectGrantMember(ctx, createAdministratorProjectGrantToCommand(resource, req.Msg.UserId, req.Msg.Roles)) + if err != nil { + return nil, err + } + if !member.EventDate.IsZero() { + creationDate = timestamppb.New(member.EventDate) + } + default: + return nil, zerrors.ThrowInvalidArgument(nil, "ADMIN-IbPp47HDP5", "Errors.Invalid.Argument") + } + + return connect.NewResponse(&internal_permission.CreateAdministratorResponse{ + CreationDate: creationDate, + }), nil +} + +func createAdministratorInstanceToCommand(instanceID, userID string, roles []string) *command.AddInstanceMember { + return &command.AddInstanceMember{ + InstanceID: instanceID, + UserID: userID, + Roles: roles, + } +} + +func createAdministratorOrganizationToCommand(req *internal_permission.ResourceType_OrganizationId, userID string, roles []string) *command.AddOrgMember { + return &command.AddOrgMember{ + OrgID: req.OrganizationId, + UserID: userID, + Roles: roles, + } +} + +func createAdministratorProjectToCommand(req *internal_permission.ResourceType_ProjectId, userID string, roles []string) *command.AddProjectMember { + return &command.AddProjectMember{ + ProjectID: req.ProjectId, + UserID: userID, + Roles: roles, + } +} + +func createAdministratorProjectGrantToCommand(req *internal_permission.ResourceType_ProjectGrant_, userID string, roles []string) *command.AddProjectGrantMember { + return &command.AddProjectGrantMember{ + GrantID: req.ProjectGrant.ProjectGrantId, + ProjectID: req.ProjectGrant.ProjectId, + UserID: userID, + Roles: roles, + } +} + +func (s *Server) UpdateAdministrator(ctx context.Context, req *connect.Request[internal_permission.UpdateAdministratorRequest]) (*connect.Response[internal_permission.UpdateAdministratorResponse], error) { + var changeDate *timestamppb.Timestamp + + switch resource := req.Msg.GetResource().GetResource().(type) { + case *internal_permission.ResourceType_Instance: + if resource.Instance { + member, err := s.command.ChangeInstanceMember(ctx, updateAdministratorInstanceToCommand(authz.GetInstance(ctx).InstanceID(), req.Msg.UserId, req.Msg.Roles)) + if err != nil { + return nil, err + } + if !member.EventDate.IsZero() { + changeDate = timestamppb.New(member.EventDate) + } + } + case *internal_permission.ResourceType_OrganizationId: + member, err := s.command.ChangeOrgMember(ctx, updateAdministratorOrganizationToCommand(resource, req.Msg.UserId, req.Msg.Roles)) + if err != nil { + return nil, err + } + if !member.EventDate.IsZero() { + changeDate = timestamppb.New(member.EventDate) + } + case *internal_permission.ResourceType_ProjectId: + member, err := s.command.ChangeProjectMember(ctx, updateAdministratorProjectToCommand(resource, req.Msg.UserId, req.Msg.Roles)) + if err != nil { + return nil, err + } + if !member.EventDate.IsZero() { + changeDate = timestamppb.New(member.EventDate) + } + case *internal_permission.ResourceType_ProjectGrant_: + member, err := s.command.ChangeProjectGrantMember(ctx, updateAdministratorProjectGrantToCommand(resource, req.Msg.UserId, req.Msg.Roles)) + if err != nil { + return nil, err + } + if !member.EventDate.IsZero() { + changeDate = timestamppb.New(member.EventDate) + } + default: + return nil, zerrors.ThrowInvalidArgument(nil, "ADMIN-i0V2IbdloZ", "Errors.Invalid.Argument") + } + + return connect.NewResponse(&internal_permission.UpdateAdministratorResponse{ + ChangeDate: changeDate, + }), nil +} + +func updateAdministratorInstanceToCommand(instanceID, userID string, roles []string) *command.ChangeInstanceMember { + return &command.ChangeInstanceMember{ + InstanceID: instanceID, + UserID: userID, + Roles: roles, + } +} + +func updateAdministratorOrganizationToCommand(req *internal_permission.ResourceType_OrganizationId, userID string, roles []string) *command.ChangeOrgMember { + return &command.ChangeOrgMember{ + OrgID: req.OrganizationId, + UserID: userID, + Roles: roles, + } +} + +func updateAdministratorProjectToCommand(req *internal_permission.ResourceType_ProjectId, userID string, roles []string) *command.ChangeProjectMember { + return &command.ChangeProjectMember{ + ProjectID: req.ProjectId, + UserID: userID, + Roles: roles, + } +} + +func updateAdministratorProjectGrantToCommand(req *internal_permission.ResourceType_ProjectGrant_, userID string, roles []string) *command.ChangeProjectGrantMember { + return &command.ChangeProjectGrantMember{ + GrantID: req.ProjectGrant.ProjectGrantId, + ProjectID: req.ProjectGrant.ProjectId, + UserID: userID, + Roles: roles, + } +} + +func (s *Server) DeleteAdministrator(ctx context.Context, req *connect.Request[internal_permission.DeleteAdministratorRequest]) (*connect.Response[internal_permission.DeleteAdministratorResponse], error) { + var deletionDate *timestamppb.Timestamp + + switch resource := req.Msg.GetResource().GetResource().(type) { + case *internal_permission.ResourceType_Instance: + if resource.Instance { + member, err := s.command.RemoveInstanceMember(ctx, authz.GetInstance(ctx).InstanceID(), req.Msg.UserId) + if err != nil { + return nil, err + } + if !member.EventDate.IsZero() { + deletionDate = timestamppb.New(member.EventDate) + } + } + case *internal_permission.ResourceType_OrganizationId: + member, err := s.command.RemoveOrgMember(ctx, resource.OrganizationId, req.Msg.UserId) + if err != nil { + return nil, err + } + if !member.EventDate.IsZero() { + deletionDate = timestamppb.New(member.EventDate) + } + case *internal_permission.ResourceType_ProjectId: + member, err := s.command.RemoveProjectMember(ctx, resource.ProjectId, req.Msg.UserId, "") + if err != nil { + return nil, err + } + if !member.EventDate.IsZero() { + deletionDate = timestamppb.New(member.EventDate) + } + case *internal_permission.ResourceType_ProjectGrant_: + member, err := s.command.RemoveProjectGrantMember(ctx, resource.ProjectGrant.ProjectId, req.Msg.UserId, resource.ProjectGrant.ProjectGrantId) + if err != nil { + return nil, err + } + if !member.EventDate.IsZero() { + deletionDate = timestamppb.New(member.EventDate) + } + default: + return nil, zerrors.ThrowInvalidArgument(nil, "ADMIN-3UOjLtuohh", "Errors.Invalid.Argument") + } + + return connect.NewResponse(&internal_permission.DeleteAdministratorResponse{ + DeletionDate: deletionDate, + }), nil +} diff --git a/internal/api/grpc/internal_permission/v2beta/integration_test/administrator_test.go b/internal/api/grpc/internal_permission/v2beta/integration_test/administrator_test.go new file mode 100644 index 0000000000..4d8e1c057c --- /dev/null +++ b/internal/api/grpc/internal_permission/v2beta/integration_test/administrator_test.go @@ -0,0 +1,1848 @@ +//go:build integration + +package project_test + +import ( + "context" + "testing" + "time" + + "github.com/brianvoe/gofakeit/v6" + "github.com/stretchr/testify/assert" + + "github.com/zitadel/zitadel/internal/integration" + internal_permission "github.com/zitadel/zitadel/pkg/grpc/internal_permission/v2beta" +) + +func TestServer_CreateAdministrator(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + + type want struct { + creationDate bool + } + tests := []struct { + name string + ctx context.Context + prepare func(request *internal_permission.CreateAdministratorRequest) + req *internal_permission.CreateAdministratorRequest + want + wantErr bool + }{ + { + name: "empty user ID", + ctx: iamOwnerCtx, + req: &internal_permission.CreateAdministratorRequest{ + UserId: "", + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + Roles: []string{"IAM_OWNER"}, + }, + wantErr: true, + }, + { + name: "empty roles", + ctx: iamOwnerCtx, + req: &internal_permission.CreateAdministratorRequest{ + UserId: "notexisting", + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + Roles: []string{}, + }, + wantErr: true, + }, + { + name: "empty resource", + ctx: iamOwnerCtx, + req: &internal_permission.CreateAdministratorRequest{ + UserId: "notexisting", + Roles: []string{"IAM_OWNER"}, + }, + wantErr: true, + }, + { + name: "already existing, error", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateInstanceMembership(t, iamOwnerCtx, userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.CreateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + Roles: []string{"IAM_OWNER"}, + }, + wantErr: true, + }, + { + name: "instance, ok", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.CreateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + Roles: []string{"IAM_OWNER"}, + }, + want: want{ + creationDate: true, + }, + }, + { + name: "instance, not existing roles", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.CreateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + Roles: []string{"notexisting"}, + }, + wantErr: true, + }, + { + name: "org, not existing", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.CreateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_OrganizationId{ + OrganizationId: "notexisting", + }, + }, + Roles: []string{"ORG_OWNER"}, + }, + wantErr: true, + }, + { + name: "org, ok", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.CreateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_OrganizationId{ + OrganizationId: instance.DefaultOrg.GetId(), + }, + }, + Roles: []string{"ORG_OWNER"}, + }, + want: want{ + creationDate: true, + }, + }, + { + name: "org, no existing roles", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.CreateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_OrganizationId{ + OrganizationId: instance.DefaultOrg.GetId(), + }, + }, + Roles: []string{"notexisting"}, + }, + wantErr: true, + }, + { + name: "project, not existing", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.CreateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectId{ + ProjectId: "notexisting", + }, + }, + Roles: []string{"PROJECT_OWNER"}, + }, + wantErr: true, + }, + { + name: "project, ok", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + + request.UserId = userResp.GetId() + request.Resource = &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectId{ + ProjectId: projectResp.GetId(), + }, + } + }, + req: &internal_permission.CreateAdministratorRequest{ + Roles: []string{"PROJECT_OWNER"}, + }, + want: want{ + creationDate: true, + }, + }, + { + name: "project, not existing roles", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + + request.UserId = userResp.GetId() + request.Resource = &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectId{ + ProjectId: projectResp.GetId(), + }, + } + }, + req: &internal_permission.CreateAdministratorRequest{ + Roles: []string{"notexisting"}, + }, + wantErr: true, + }, + { + name: "project grant, not existing", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + request.UserId = userResp.GetId() + request.Resource = &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectGrant_{ + ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ + ProjectId: projectResp.GetId(), + ProjectGrantId: "notexisting", + }, + }, + } + }, + req: &internal_permission.CreateAdministratorRequest{ + Roles: []string{"PROJECT_GRANT_OWNER"}, + }, + wantErr: true, + }, + { + name: "project grant, ok", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.Company(), gofakeit.Email()) + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), orgResp.GetOrganizationId()) + + request.UserId = userResp.GetId() + request.Resource = &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectGrant_{ + ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ + ProjectId: projectResp.GetId(), + ProjectGrantId: orgResp.GetOrganizationId(), + }, + }, + } + }, + req: &internal_permission.CreateAdministratorRequest{ + Roles: []string{"PROJECT_GRANT_OWNER"}, + }, + want: want{ + creationDate: true, + }, + }, + { + name: "project grant, not existing roles", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.Company(), gofakeit.Email()) + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), orgResp.GetOrganizationId()) + + request.UserId = userResp.GetId() + request.Resource = &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectGrant_{ + ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ + ProjectId: projectResp.GetId(), + ProjectGrantId: orgResp.GetOrganizationId(), + }, + }, + } + }, + req: &internal_permission.CreateAdministratorRequest{ + Roles: []string{"notexisting"}, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.prepare != nil { + tt.prepare(tt.req) + } + creationDate := time.Now().UTC() + got, err := instance.Client.InternalPermissionv2Beta.CreateAdministrator(tt.ctx, tt.req) + changeDate := time.Now().UTC() + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertCreateAdministratorResponse(t, creationDate, changeDate, tt.want.creationDate, got) + }) + } +} + +func TestServer_CreateAdministrator_Permission(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + userProjectResp := instance.CreateMachineUser(iamOwnerCtx) + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + instance.CreateProjectMembership(t, iamOwnerCtx, projectResp.GetId(), userProjectResp.GetUserId()) + patProjectResp := instance.CreatePersonalAccessToken(iamOwnerCtx, userProjectResp.GetUserId()) + projectOwnerCtx := integration.WithAuthorizationToken(CTX, patProjectResp.Token) + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), orgResp.GetOrganizationId()) + userProjectGrantResp := instance.CreateMachineUser(iamOwnerCtx) + instance.CreateProjectGrantMembership(t, iamOwnerCtx, projectResp.GetId(), orgResp.GetOrganizationId(), userProjectGrantResp.GetUserId()) + patProjectGrantResp := instance.CreatePersonalAccessToken(iamOwnerCtx, userProjectGrantResp.GetUserId()) + projectGrantOwnerCtx := integration.WithAuthorizationToken(CTX, patProjectGrantResp.Token) + + type want struct { + creationDate bool + } + tests := []struct { + name string + ctx context.Context + prepare func(request *internal_permission.CreateAdministratorRequest) + req *internal_permission.CreateAdministratorRequest + want + wantErr bool + }{ + { + name: "unauthenticated", + ctx: CTX, + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.CreateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + Roles: []string{"IAM_OWNER"}, + }, + wantErr: true, + }, + { + name: "missing permission", + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.CreateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + Roles: []string{"IAM_OWNER"}, + }, + wantErr: true, + }, + { + name: "instance, missing permission, org owner", + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.CreateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + Roles: []string{"IAM_OWNER"}, + }, + wantErr: true, + }, + { + name: "instance, instance owner, ok", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.CreateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + Roles: []string{"IAM_OWNER"}, + }, + want: want{ + creationDate: true, + }, + }, + { + name: "org, instance owner, ok", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.CreateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_OrganizationId{ + OrganizationId: instance.DefaultOrg.GetId(), + }, + }, + Roles: []string{"ORG_OWNER"}, + }, + want: want{ + creationDate: true, + }, + }, + { + name: "org, org owner, ok", + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.CreateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_OrganizationId{ + OrganizationId: instance.DefaultOrg.GetId(), + }, + }, + Roles: []string{"ORG_OWNER"}, + }, + want: want{ + creationDate: true, + }, + }, + { + name: "org, missing permission", + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.CreateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_OrganizationId{ + OrganizationId: instance.DefaultOrg.GetId(), + }, + }, + Roles: []string{"ORG_OWNER"}, + }, + wantErr: true, + }, + { + name: "project, project owner, ok", + ctx: projectOwnerCtx, + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + + request.UserId = userResp.GetId() + }, + req: &internal_permission.CreateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectId{ + ProjectId: projectResp.GetId(), + }, + }, + Roles: []string{"PROJECT_OWNER"}, + }, + want: want{ + creationDate: true, + }, + }, + { + name: "project, missing permission", + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + + request.UserId = userResp.GetId() + }, + req: &internal_permission.CreateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectId{ + ProjectId: projectResp.GetId(), + }, + }, + Roles: []string{"PROJECT_OWNER"}, + }, + wantErr: true, + }, + { + name: "project grant, project grant owner, ok", + ctx: projectGrantOwnerCtx, + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + + request.UserId = userResp.GetId() + }, + req: &internal_permission.CreateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectGrant_{ + ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ + ProjectId: projectResp.GetId(), + ProjectGrantId: orgResp.GetOrganizationId(), + }, + }, + }, + Roles: []string{"PROJECT_GRANT_OWNER"}, + }, + want: want{ + creationDate: true, + }, + }, + { + name: "project grant, missing permission", + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + + request.UserId = userResp.GetId() + }, + req: &internal_permission.CreateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectGrant_{ + ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ + ProjectId: projectResp.GetId(), + ProjectGrantId: orgResp.GetOrganizationId(), + }, + }, + }, + Roles: []string{"PROJECT_GRANT_OWNER"}, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.prepare != nil { + tt.prepare(tt.req) + } + creationDate := time.Now().UTC() + got, err := instance.Client.InternalPermissionv2Beta.CreateAdministrator(tt.ctx, tt.req) + changeDate := time.Now().UTC() + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertCreateAdministratorResponse(t, creationDate, changeDate, tt.want.creationDate, got) + }) + } +} + +func assertCreateAdministratorResponse(t *testing.T, creationDate, changeDate time.Time, expectedCreationDate bool, actualResp *internal_permission.CreateAdministratorResponse) { + if expectedCreationDate { + if !changeDate.IsZero() { + assert.WithinRange(t, actualResp.GetCreationDate().AsTime(), creationDate, changeDate) + } else { + assert.WithinRange(t, actualResp.GetCreationDate().AsTime(), creationDate, time.Now().UTC()) + } + } else { + assert.Nil(t, actualResp.CreationDate) + } +} + +func TestServer_UpdateAdministrator(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + + type want struct { + change bool + changeDate bool + } + tests := []struct { + name string + ctx context.Context + prepare func(request *internal_permission.UpdateAdministratorRequest) + req *internal_permission.UpdateAdministratorRequest + want + wantErr bool + }{ + { + name: "empty user ID", + ctx: iamOwnerCtx, + req: &internal_permission.UpdateAdministratorRequest{ + UserId: "", + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + Roles: []string{"IAM_OWNER"}, + }, + wantErr: true, + }, + { + name: "empty roles", + ctx: iamOwnerCtx, + req: &internal_permission.UpdateAdministratorRequest{ + UserId: "notexisting", + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + Roles: []string{}, + }, + wantErr: true, + }, + { + name: "empty resource", + ctx: iamOwnerCtx, + req: &internal_permission.UpdateAdministratorRequest{ + UserId: "notexisting", + Roles: []string{"IAM_OWNER"}, + }, + wantErr: true, + }, + { + name: "instance, no change", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateInstanceMembership(t, iamOwnerCtx, userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.UpdateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + Roles: []string{"IAM_OWNER"}, + }, + want: want{ + change: false, + changeDate: true, + }, + }, + { + name: "instance, ok", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateInstanceMembership(t, iamOwnerCtx, userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.UpdateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + Roles: []string{"IAM_OWNER_VIEWER"}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + { + name: "instance, not existing roles", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateInstanceMembership(t, iamOwnerCtx, userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.UpdateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + Roles: []string{"notexisting"}, + }, + wantErr: true, + }, + { + name: "org, not existing", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.UpdateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_OrganizationId{ + OrganizationId: "notexisting", + }, + }, + Roles: []string{"ORG_OWNER"}, + }, + wantErr: true, + }, + { + name: "org, ok", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateOrgMembership(t, iamOwnerCtx, instance.DefaultOrg.Id, userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.UpdateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_OrganizationId{ + OrganizationId: instance.DefaultOrg.GetId(), + }, + }, + Roles: []string{"ORG_OWNER_VIEWER"}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + { + name: "org, no change", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateOrgMembership(t, iamOwnerCtx, instance.DefaultOrg.Id, userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.UpdateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_OrganizationId{ + OrganizationId: instance.DefaultOrg.GetId(), + }, + }, + Roles: []string{"ORG_OWNER"}, + }, + want: want{ + change: false, + changeDate: true, + }, + }, + { + name: "org, no existing roles", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateOrgMembership(t, iamOwnerCtx, instance.DefaultOrg.Id, userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.UpdateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_OrganizationId{ + OrganizationId: instance.DefaultOrg.GetId(), + }, + }, + Roles: []string{"notexisting"}, + }, + wantErr: true, + }, + { + name: "project, not existing", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.UpdateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectId{ + ProjectId: "notexisting", + }, + }, + Roles: []string{"PROJECT_OWNER"}, + }, + wantErr: true, + }, + { + name: "project, ok", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + instance.CreateProjectMembership(t, iamOwnerCtx, projectResp.GetId(), userResp.GetId()) + + request.UserId = userResp.GetId() + request.Resource = &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectId{ + ProjectId: projectResp.GetId(), + }, + } + }, + req: &internal_permission.UpdateAdministratorRequest{ + Roles: []string{"PROJECT_OWNER_VIEWER"}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + { + name: "project, no change", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + instance.CreateProjectMembership(t, iamOwnerCtx, projectResp.GetId(), userResp.GetId()) + + request.UserId = userResp.GetId() + request.Resource = &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectId{ + ProjectId: projectResp.GetId(), + }, + } + }, + req: &internal_permission.UpdateAdministratorRequest{ + Roles: []string{"PROJECT_OWNER"}, + }, + want: want{ + change: false, + changeDate: true, + }, + }, + { + name: "project, not existing roles", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + instance.CreateProjectMembership(t, iamOwnerCtx, projectResp.GetId(), userResp.GetId()) + + request.UserId = userResp.GetId() + request.Resource = &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectId{ + ProjectId: projectResp.GetId(), + }, + } + }, + req: &internal_permission.UpdateAdministratorRequest{ + Roles: []string{"notexisting"}, + }, + wantErr: true, + }, + { + name: "project grant, not existing", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.Company(), gofakeit.Email()) + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), orgResp.GetOrganizationId()) + + request.UserId = userResp.GetId() + request.Resource = &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectGrant_{ + ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ + ProjectId: projectResp.GetId(), + ProjectGrantId: "notexisting", + }, + }, + } + }, + req: &internal_permission.UpdateAdministratorRequest{ + Roles: []string{"PROJECT_GRANT_OWNER_VIEWER"}, + }, + wantErr: true, + }, + { + name: "project grant, ok", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.Company(), gofakeit.Email()) + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), orgResp.GetOrganizationId()) + instance.CreateProjectGrantMembership(t, iamOwnerCtx, projectResp.GetId(), orgResp.GetOrganizationId(), userResp.GetId()) + + request.UserId = userResp.GetId() + request.Resource = &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectGrant_{ + ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ + ProjectId: projectResp.GetId(), + ProjectGrantId: orgResp.GetOrganizationId(), + }, + }, + } + }, + req: &internal_permission.UpdateAdministratorRequest{ + Roles: []string{"PROJECT_GRANT_OWNER_VIEWER"}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + { + name: "project grant, no change", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.Company(), gofakeit.Email()) + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), orgResp.GetOrganizationId()) + instance.CreateProjectGrantMembership(t, iamOwnerCtx, projectResp.GetId(), orgResp.GetOrganizationId(), userResp.GetId()) + + request.UserId = userResp.GetId() + request.Resource = &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectGrant_{ + ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ + ProjectId: projectResp.GetId(), + ProjectGrantId: orgResp.GetOrganizationId(), + }, + }, + } + }, + req: &internal_permission.UpdateAdministratorRequest{ + Roles: []string{"PROJECT_GRANT_OWNER"}, + }, + want: want{ + change: false, + changeDate: true, + }, + }, + { + name: "project grant, not existing roles", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.Company(), gofakeit.Email()) + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), orgResp.GetOrganizationId()) + + request.UserId = userResp.GetId() + request.Resource = &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectGrant_{ + ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ + ProjectId: projectResp.GetId(), + ProjectGrantId: orgResp.GetOrganizationId(), + }, + }, + } + }, + req: &internal_permission.UpdateAdministratorRequest{ + Roles: []string{"notexisting"}, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creationDate := time.Now().UTC() + if tt.prepare != nil { + tt.prepare(tt.req) + } + got, err := instance.Client.InternalPermissionv2Beta.UpdateAdministrator(tt.ctx, tt.req) + if tt.wantErr { + assert.Error(t, err) + return + } + + changeDate := time.Time{} + if tt.want.change { + changeDate = time.Now().UTC() + } + assert.NoError(t, err) + assertUpdateAdministratorResponse(t, creationDate, changeDate, tt.want.changeDate, got) + }) + } +} + +func TestServer_UpdateAdministrator_Permission(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + userProjectResp := instance.CreateMachineUser(iamOwnerCtx) + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + instance.CreateProjectMembership(t, iamOwnerCtx, projectResp.GetId(), userProjectResp.GetUserId()) + patProjectResp := instance.CreatePersonalAccessToken(iamOwnerCtx, userProjectResp.GetUserId()) + projectOwnerCtx := integration.WithAuthorizationToken(CTX, patProjectResp.Token) + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), orgResp.GetOrganizationId()) + userProjectGrantResp := instance.CreateMachineUser(iamOwnerCtx) + instance.CreateProjectGrantMembership(t, iamOwnerCtx, projectResp.GetId(), orgResp.GetOrganizationId(), userProjectGrantResp.GetUserId()) + patProjectGrantResp := instance.CreatePersonalAccessToken(iamOwnerCtx, userProjectGrantResp.GetUserId()) + projectGrantOwnerCtx := integration.WithAuthorizationToken(CTX, patProjectGrantResp.Token) + + type want struct { + creationDate bool + } + tests := []struct { + name string + ctx context.Context + prepare func(request *internal_permission.UpdateAdministratorRequest) + req *internal_permission.UpdateAdministratorRequest + want + wantErr bool + }{ + { + name: "unauthenticated", + ctx: CTX, + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateInstanceMembership(t, iamOwnerCtx, userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.UpdateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + Roles: []string{"IAM_OWNER_VIEWER"}, + }, + wantErr: true, + }, + { + name: "missing permission", + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateInstanceMembership(t, iamOwnerCtx, userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.UpdateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + Roles: []string{"IAM_OWNER_VIEWER"}, + }, + wantErr: true, + }, + { + name: "instance, missing permission, org owner", + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateInstanceMembership(t, iamOwnerCtx, userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.UpdateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + Roles: []string{"IAM_OWNER_VIEWER"}, + }, + wantErr: true, + }, + { + name: "instance, instance owner, ok", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateInstanceMembership(t, iamOwnerCtx, userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.UpdateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + Roles: []string{"IAM_OWNER_VIEWER"}, + }, + want: want{ + creationDate: true, + }, + }, + { + name: "org, instance owner, ok", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateOrgMembership(t, iamOwnerCtx, instance.DefaultOrg.Id, userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.UpdateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_OrganizationId{ + OrganizationId: instance.DefaultOrg.GetId(), + }, + }, + Roles: []string{"ORG_OWNER_VIEWER"}, + }, + want: want{ + creationDate: true, + }, + }, + { + name: "org, org owner, ok", + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateOrgMembership(t, iamOwnerCtx, instance.DefaultOrg.Id, userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.UpdateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_OrganizationId{ + OrganizationId: instance.DefaultOrg.GetId(), + }, + }, + Roles: []string{"ORG_OWNER_VIEWER"}, + }, + want: want{ + creationDate: true, + }, + }, + { + name: "org, missing permission", + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateOrgMembership(t, iamOwnerCtx, instance.DefaultOrg.Id, userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.UpdateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_OrganizationId{ + OrganizationId: instance.DefaultOrg.GetId(), + }, + }, + Roles: []string{"ORG_OWNER_VIEWER"}, + }, + wantErr: true, + }, + { + name: "project, project owner, ok", + ctx: projectOwnerCtx, + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateProjectMembership(t, iamOwnerCtx, projectResp.GetId(), userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.UpdateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectId{ + ProjectId: projectResp.GetId(), + }, + }, + Roles: []string{"PROJECT_OWNER_VIEWER"}, + }, + want: want{ + creationDate: true, + }, + }, + { + name: "project, missing permission", + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateProjectMembership(t, iamOwnerCtx, projectResp.GetId(), userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.UpdateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectId{ + ProjectId: projectResp.GetId(), + }, + }, + Roles: []string{"PROJECT_OWNER_VIEWER"}, + }, + wantErr: true, + }, + { + name: "project grant, project grant owner, ok", + ctx: projectGrantOwnerCtx, + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateProjectGrantMembership(t, iamOwnerCtx, projectResp.GetId(), orgResp.GetOrganizationId(), userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.UpdateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectGrant_{ + ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ + ProjectId: projectResp.GetId(), + ProjectGrantId: orgResp.GetOrganizationId(), + }, + }, + }, + Roles: []string{"PROJECT_GRANT_OWNER_VIEWER"}, + }, + want: want{ + creationDate: true, + }, + }, + { + name: "project grant, missing permission", + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateProjectGrantMembership(t, iamOwnerCtx, projectResp.GetId(), orgResp.GetOrganizationId(), userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.UpdateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectGrant_{ + ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ + ProjectId: projectResp.GetId(), + ProjectGrantId: orgResp.GetOrganizationId(), + }, + }, + }, + Roles: []string{"PROJECT_GRANT_OWNER_VIEWER"}, + }, + wantErr: true, + }, + { + name: "project grant, project owner, error", + ctx: projectOwnerCtx, + prepare: func(request *internal_permission.UpdateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateProjectGrantMembership(t, iamOwnerCtx, projectResp.GetId(), orgResp.GetOrganizationId(), userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.UpdateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectGrant_{ + ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ + ProjectId: projectResp.GetId(), + ProjectGrantId: orgResp.GetOrganizationId(), + }, + }, + }, + Roles: []string{"PROJECT_GRANT_OWNER_VIEWER"}, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.prepare != nil { + tt.prepare(tt.req) + } + creationDate := time.Now().UTC() + got, err := instance.Client.InternalPermissionv2Beta.UpdateAdministrator(tt.ctx, tt.req) + changeDate := time.Now().UTC() + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertUpdateAdministratorResponse(t, creationDate, changeDate, tt.want.creationDate, got) + }) + } +} + +func assertUpdateAdministratorResponse(t *testing.T, creationDate, changeDate time.Time, expectedChangeDate bool, actualResp *internal_permission.UpdateAdministratorResponse) { + if expectedChangeDate { + if !changeDate.IsZero() { + assert.WithinRange(t, actualResp.GetChangeDate().AsTime(), creationDate, changeDate) + } else { + assert.WithinRange(t, actualResp.GetChangeDate().AsTime(), creationDate, time.Now().UTC()) + } + } else { + assert.Nil(t, actualResp.ChangeDate) + } +} + +func TestServer_DeleteAdministrator(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + + tests := []struct { + name string + ctx context.Context + prepare func(request *internal_permission.DeleteAdministratorRequest) (time.Time, time.Time) + req *internal_permission.DeleteAdministratorRequest + wantDeletionDate bool + wantErr bool + }{ + { + name: "empty user ID", + ctx: iamOwnerCtx, + req: &internal_permission.DeleteAdministratorRequest{ + UserId: "", + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + }, + wantErr: true, + }, + { + name: "instance, not existing", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.DeleteAdministratorRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + request.UserId = userResp.GetId() + return creationDate, time.Now().UTC() + }, + req: &internal_permission.DeleteAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + }, + wantDeletionDate: false, + }, + { + name: "instance, already removed", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.DeleteAdministratorRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateInstanceMembership(t, iamOwnerCtx, userResp.GetId()) + instance.DeleteInstanceMembership(t, iamOwnerCtx, userResp.GetId()) + request.UserId = userResp.GetId() + return creationDate, time.Now().UTC() + }, + req: &internal_permission.DeleteAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + }, + wantDeletionDate: true, + }, + { + name: "instance, ok", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.DeleteAdministratorRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateInstanceMembership(t, iamOwnerCtx, userResp.GetId()) + request.UserId = userResp.GetId() + return creationDate, time.Time{} + }, + req: &internal_permission.DeleteAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + }, + wantDeletionDate: true, + }, + { + name: "org, not existing", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.DeleteAdministratorRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + request.UserId = userResp.GetId() + return creationDate, time.Now().UTC() + }, + req: &internal_permission.DeleteAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_OrganizationId{ + OrganizationId: "notexisting", + }, + }, + }, + wantDeletionDate: false, + }, + { + name: "org, ok", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.DeleteAdministratorRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateOrgMembership(t, iamOwnerCtx, instance.DefaultOrg.Id, userResp.GetId()) + request.UserId = userResp.GetId() + return creationDate, time.Time{} + }, + req: &internal_permission.DeleteAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_OrganizationId{ + OrganizationId: instance.DefaultOrg.GetId(), + }, + }, + }, + wantDeletionDate: true, + }, + { + name: "org, already removed", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.DeleteAdministratorRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateOrgMembership(t, iamOwnerCtx, instance.DefaultOrg.Id, userResp.GetId()) + instance.DeleteOrgMembership(t, iamOwnerCtx, userResp.GetId()) + request.UserId = userResp.GetId() + return creationDate, time.Now().UTC() + }, + req: &internal_permission.DeleteAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_OrganizationId{ + OrganizationId: instance.DefaultOrg.GetId(), + }, + }, + }, + wantDeletionDate: true, + }, + { + name: "project, not existing", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.DeleteAdministratorRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + request.UserId = userResp.GetId() + return creationDate, time.Now().UTC() + }, + req: &internal_permission.DeleteAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectId{ + ProjectId: "notexisting", + }, + }, + }, + wantDeletionDate: false, + }, + { + name: "project, ok", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.DeleteAdministratorRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + instance.CreateProjectMembership(t, iamOwnerCtx, projectResp.GetId(), userResp.GetId()) + + request.UserId = userResp.GetId() + request.Resource = &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectId{ + ProjectId: projectResp.GetId(), + }, + } + return creationDate, time.Time{} + }, + req: &internal_permission.DeleteAdministratorRequest{}, + wantDeletionDate: true, + }, + { + name: "project, already removed", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.DeleteAdministratorRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + instance.CreateProjectMembership(t, iamOwnerCtx, projectResp.GetId(), userResp.GetId()) + instance.DeleteProjectMembership(t, iamOwnerCtx, projectResp.GetId(), userResp.GetId()) + + request.UserId = userResp.GetId() + request.Resource = &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectId{ + ProjectId: projectResp.GetId(), + }, + } + return creationDate, time.Now().UTC() + }, + req: &internal_permission.DeleteAdministratorRequest{}, + wantDeletionDate: true, + }, + { + name: "project grant, not existing", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.DeleteAdministratorRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.Company(), gofakeit.Email()) + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), orgResp.GetOrganizationId()) + + request.UserId = userResp.GetId() + request.Resource = &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectGrant_{ + ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ + ProjectId: projectResp.GetId(), + ProjectGrantId: "notexisting", + }, + }, + } + return creationDate, time.Time{} + }, + req: &internal_permission.DeleteAdministratorRequest{}, + wantDeletionDate: false, + }, + { + name: "project grant, ok", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.DeleteAdministratorRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.Company(), gofakeit.Email()) + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), orgResp.GetOrganizationId()) + instance.CreateProjectGrantMembership(t, iamOwnerCtx, projectResp.GetId(), orgResp.GetOrganizationId(), userResp.GetId()) + + request.UserId = userResp.GetId() + request.Resource = &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectGrant_{ + ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ + ProjectId: projectResp.GetId(), + ProjectGrantId: orgResp.GetOrganizationId(), + }, + }, + } + return creationDate, time.Time{} + }, + req: &internal_permission.DeleteAdministratorRequest{}, + wantDeletionDate: true, + }, + { + name: "project grant, already removed", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.DeleteAdministratorRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.Company(), gofakeit.Email()) + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), orgResp.GetOrganizationId()) + instance.CreateProjectGrantMembership(t, iamOwnerCtx, projectResp.GetId(), orgResp.GetOrganizationId(), userResp.GetId()) + instance.DeleteProjectGrantMembership(t, iamOwnerCtx, projectResp.GetId(), orgResp.GetOrganizationId(), userResp.GetId()) + + request.UserId = userResp.GetId() + request.Resource = &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectGrant_{ + ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ + ProjectId: projectResp.GetId(), + ProjectGrantId: orgResp.GetOrganizationId(), + }, + }, + } + return creationDate, time.Now().UTC() + }, + req: &internal_permission.DeleteAdministratorRequest{}, + wantDeletionDate: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var creationDate, deletionDate time.Time + if tt.prepare != nil { + creationDate, deletionDate = tt.prepare(tt.req) + } + got, err := instance.Client.InternalPermissionv2Beta.DeleteAdministrator(tt.ctx, tt.req) + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertDeleteAdministratorResponse(t, creationDate, deletionDate, tt.wantDeletionDate, got) + }) + } +} + +func TestServer_DeleteAdministrator_Permission(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + userProjectResp := instance.CreateMachineUser(iamOwnerCtx) + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + instance.CreateProjectMembership(t, iamOwnerCtx, projectResp.GetId(), userProjectResp.GetUserId()) + patProjectResp := instance.CreatePersonalAccessToken(iamOwnerCtx, userProjectResp.GetUserId()) + projectOwnerCtx := integration.WithAuthorizationToken(CTX, patProjectResp.Token) + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), orgResp.GetOrganizationId()) + userProjectGrantResp := instance.CreateMachineUser(iamOwnerCtx) + instance.CreateProjectGrantMembership(t, iamOwnerCtx, projectResp.GetId(), orgResp.GetOrganizationId(), userProjectGrantResp.GetUserId()) + patProjectGrantResp := instance.CreatePersonalAccessToken(iamOwnerCtx, userProjectGrantResp.GetUserId()) + projectGrantOwnerCtx := integration.WithAuthorizationToken(CTX, patProjectGrantResp.Token) + + type want struct { + creationDate bool + } + tests := []struct { + name string + ctx context.Context + prepare func(request *internal_permission.DeleteAdministratorRequest) + req *internal_permission.DeleteAdministratorRequest + want + wantErr bool + }{ + { + name: "unauthenticated", + ctx: CTX, + prepare: func(request *internal_permission.DeleteAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateInstanceMembership(t, iamOwnerCtx, userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.DeleteAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + }, + wantErr: true, + }, + { + name: "missing permission", + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + prepare: func(request *internal_permission.DeleteAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateInstanceMembership(t, iamOwnerCtx, userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.DeleteAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + }, + wantErr: true, + }, + { + name: "instance, missing permission, org owner", + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + prepare: func(request *internal_permission.DeleteAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateInstanceMembership(t, iamOwnerCtx, userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.DeleteAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + }, + wantErr: true, + }, + { + name: "instance, instance owner, ok", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.DeleteAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateInstanceMembership(t, iamOwnerCtx, userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.DeleteAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_Instance{ + Instance: true, + }, + }, + }, + want: want{ + creationDate: true, + }, + }, + { + name: "org, instance owner, ok", + ctx: iamOwnerCtx, + prepare: func(request *internal_permission.DeleteAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateOrgMembership(t, iamOwnerCtx, instance.DefaultOrg.Id, userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.DeleteAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_OrganizationId{ + OrganizationId: instance.DefaultOrg.GetId(), + }, + }, + }, + want: want{ + creationDate: true, + }, + }, + { + name: "org, org owner, ok", + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + prepare: func(request *internal_permission.DeleteAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateOrgMembership(t, iamOwnerCtx, instance.DefaultOrg.Id, userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.DeleteAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_OrganizationId{ + OrganizationId: instance.DefaultOrg.GetId(), + }, + }, + }, + want: want{ + creationDate: true, + }, + }, + { + name: "org, missing permission", + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + prepare: func(request *internal_permission.DeleteAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateOrgMembership(t, iamOwnerCtx, instance.DefaultOrg.Id, userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.DeleteAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_OrganizationId{ + OrganizationId: instance.DefaultOrg.GetId(), + }, + }, + }, + wantErr: true, + }, + { + name: "project, project owner, ok", + ctx: projectOwnerCtx, + prepare: func(request *internal_permission.DeleteAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateProjectMembership(t, iamOwnerCtx, projectResp.GetId(), userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.DeleteAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectId{ + ProjectId: projectResp.GetId(), + }, + }, + }, + want: want{ + creationDate: true, + }, + }, + { + name: "project, missing permission", + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + prepare: func(request *internal_permission.DeleteAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateProjectMembership(t, iamOwnerCtx, projectResp.GetId(), userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.DeleteAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectId{ + ProjectId: projectResp.GetId(), + }, + }, + }, + wantErr: true, + }, + { + name: "project grant, project grant owner, ok", + ctx: projectGrantOwnerCtx, + prepare: func(request *internal_permission.DeleteAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateProjectGrantMembership(t, iamOwnerCtx, projectResp.GetId(), orgResp.GetOrganizationId(), userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.DeleteAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectGrant_{ + ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ + ProjectId: projectResp.GetId(), + ProjectGrantId: orgResp.GetOrganizationId(), + }, + }, + }, + }, + want: want{ + creationDate: true, + }, + }, + { + name: "project grant, missing permission", + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + prepare: func(request *internal_permission.DeleteAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateProjectGrantMembership(t, iamOwnerCtx, projectResp.GetId(), orgResp.GetOrganizationId(), userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.DeleteAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectGrant_{ + ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ + ProjectId: projectResp.GetId(), + ProjectGrantId: orgResp.GetOrganizationId(), + }, + }, + }, + }, + wantErr: true, + }, + { + name: "project grant, project owner, error", + ctx: projectOwnerCtx, + prepare: func(request *internal_permission.DeleteAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, gofakeit.Email()) + instance.CreateProjectGrantMembership(t, iamOwnerCtx, projectResp.GetId(), orgResp.GetOrganizationId(), userResp.GetId()) + request.UserId = userResp.GetId() + }, + req: &internal_permission.DeleteAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectGrant_{ + ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ + ProjectId: projectResp.GetId(), + ProjectGrantId: orgResp.GetOrganizationId(), + }, + }, + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.prepare != nil { + tt.prepare(tt.req) + } + creationDate := time.Now().UTC() + got, err := instance.Client.InternalPermissionv2Beta.DeleteAdministrator(tt.ctx, tt.req) + changeDate := time.Now().UTC() + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertDeleteAdministratorResponse(t, creationDate, changeDate, tt.want.creationDate, got) + }) + } +} + +func assertDeleteAdministratorResponse(t *testing.T, creationDate, deletionDate time.Time, expectedDeletionDate bool, actualResp *internal_permission.DeleteAdministratorResponse) { + if expectedDeletionDate { + if !deletionDate.IsZero() { + assert.WithinRange(t, actualResp.GetDeletionDate().AsTime(), creationDate, deletionDate) + } else { + assert.WithinRange(t, actualResp.GetDeletionDate().AsTime(), creationDate, time.Now().UTC()) + } + } else { + assert.Nil(t, actualResp.DeletionDate) + } +} diff --git a/internal/api/grpc/internal_permission/v2beta/integration_test/query_test.go b/internal/api/grpc/internal_permission/v2beta/integration_test/query_test.go new file mode 100644 index 0000000000..84eea98992 --- /dev/null +++ b/internal/api/grpc/internal_permission/v2beta/integration_test/query_test.go @@ -0,0 +1,1173 @@ +//go:build integration + +package project_test + +import ( + "context" + "testing" + "time" + + "github.com/brianvoe/gofakeit/v6" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/internal/integration" + filter "github.com/zitadel/zitadel/pkg/grpc/filter/v2beta" + internal_permission "github.com/zitadel/zitadel/pkg/grpc/internal_permission/v2beta" +) + +func TestServer_ListAdministrators(t *testing.T) { + iamOwnerCtx := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + + projectName := gofakeit.AppName() + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), projectName, false, false) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.Company(), gofakeit.Email()) + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), orgResp.GetOrganizationId()) + + userProjectResp := instance.CreateMachineUser(iamOwnerCtx) + instance.CreateProjectMembership(t, iamOwnerCtx, projectResp.GetId(), userProjectResp.GetUserId()) + patProjectResp := instance.CreatePersonalAccessToken(iamOwnerCtx, userProjectResp.GetUserId()) + projectOwnerCtx := integration.WithAuthorizationToken(CTX, patProjectResp.Token) + + userProjectGrantResp := instance.CreateMachineUser(iamOwnerCtx) + instance.CreateProjectGrantMembership(t, iamOwnerCtx, projectResp.GetId(), orgResp.GetOrganizationId(), userProjectGrantResp.GetUserId()) + patProjectGrantResp := instance.CreatePersonalAccessToken(iamOwnerCtx, userProjectGrantResp.GetUserId()) + projectGrantOwnerCtx := integration.WithAuthorizationToken(CTX, patProjectGrantResp.Token) + + type args struct { + ctx context.Context + dep func(*internal_permission.ListAdministratorsRequest, *internal_permission.ListAdministratorsResponse) + req *internal_permission.ListAdministratorsRequest + } + tests := []struct { + name string + args args + want *internal_permission.ListAdministratorsResponse + wantErr bool + }{ + { + name: "list by id, unauthenticated", + args: args{ + ctx: CTX, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + admin := createInstanceAdministrator(iamOwnerCtx, instance, t) + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{admin.GetUser().GetId()}, + }, + } + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + wantErr: true, + }, + { + name: "list by id, no permission", + args: args{ + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeNoPermission), + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + admin := createInstanceAdministrator(iamOwnerCtx, instance, t) + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{admin.GetUser().GetId()}, + }, + } + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{}, + }, + }, + { + name: "list by id, missing permission", + args: args{ + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + admin := createInstanceAdministrator(iamOwnerCtx, instance, t) + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{admin.GetUser().GetId()}, + }, + } + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{}, + }, + }, + { + name: "list, not found", + args: args{ + ctx: iamOwnerCtx, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{ + { + Filter: &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{Ids: []string{"notexisting"}}, + }, + }, + }, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 0, + AppliedLimit: 100, + }, + }, + }, + { + name: "list single id, instance", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + admin := createInstanceAdministrator(iamOwnerCtx, instance, t) + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{admin.GetUser().GetId()}, + }, + } + response.Administrators[0] = admin + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, + }, + }, + }, + { + name: "list single id, instance", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + admin := createInstanceAdministrator(iamOwnerCtx, instance, t) + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{admin.GetUser().GetId()}, + }, + } + response.Administrators[0] = admin + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, + }, + }, + }, + { + name: "list multiple id, instance", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + admin1 := createInstanceAdministrator(iamOwnerCtx, instance, t) + admin2 := createInstanceAdministrator(iamOwnerCtx, instance, t) + admin3 := createInstanceAdministrator(iamOwnerCtx, instance, t) + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{admin1.GetUser().GetId(), admin2.GetUser().GetId(), admin3.GetUser().GetId()}, + }, + } + response.Administrators[0] = admin3 + response.Administrators[1] = admin2 + response.Administrators[2] = admin1 + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, {}, {}, + }, + }, + }, + { + name: "list single id, org", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + admin := createOrganizationAdministrator(iamOwnerCtx, instance, t) + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{admin.GetUser().GetId()}, + }, + } + response.Administrators[0] = admin + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, + }, + }, + }, + { + name: "list multiple id, org", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + admin1 := createOrganizationAdministrator(iamOwnerCtx, instance, t) + admin2 := createOrganizationAdministrator(iamOwnerCtx, instance, t) + admin3 := createOrganizationAdministrator(iamOwnerCtx, instance, t) + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{admin1.GetUser().GetId(), admin2.GetUser().GetId(), admin3.GetUser().GetId()}, + }, + } + response.Administrators[0] = admin3 + response.Administrators[1] = admin2 + response.Administrators[2] = admin1 + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, {}, {}, + }, + }, + }, + { + name: "list single id, project", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + admin := createProjectAdministrator(iamOwnerCtx, instance, t, instance.DefaultOrg.GetId(), projectResp.GetId(), projectName) + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{admin.GetUser().GetId()}, + }, + } + response.Administrators[0] = admin + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, + }, + }, + }, + { + name: "list multiple id, project", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + admin1 := createProjectAdministrator(iamOwnerCtx, instance, t, instance.DefaultOrg.GetId(), projectResp.GetId(), projectName) + admin2 := createProjectAdministrator(iamOwnerCtx, instance, t, instance.DefaultOrg.GetId(), projectResp.GetId(), projectName) + admin3 := createProjectAdministrator(iamOwnerCtx, instance, t, instance.DefaultOrg.GetId(), projectResp.GetId(), projectName) + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{admin1.GetUser().GetId(), admin2.GetUser().GetId(), admin3.GetUser().GetId()}, + }, + } + response.Administrators[0] = admin3 + response.Administrators[1] = admin2 + response.Administrators[2] = admin1 + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, {}, {}, + }, + }, + }, + { + name: "list single id, project grant", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + admin := createProjectGrantAdministrator(iamOwnerCtx, instance, t, instance.DefaultOrg.GetId(), projectResp.GetId(), projectName, orgResp.GetOrganizationId()) + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{admin.GetUser().GetId()}, + }, + } + response.Administrators[0] = admin + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, + }, + }, + }, + { + name: "list multiple id, project grant", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + admin1 := createProjectGrantAdministrator(iamOwnerCtx, instance, t, instance.DefaultOrg.GetId(), projectResp.GetId(), projectName, orgResp.GetOrganizationId()) + admin2 := createProjectGrantAdministrator(iamOwnerCtx, instance, t, instance.DefaultOrg.GetId(), projectResp.GetId(), projectName, orgResp.GetOrganizationId()) + admin3 := createProjectGrantAdministrator(iamOwnerCtx, instance, t, instance.DefaultOrg.GetId(), projectResp.GetId(), projectName, orgResp.GetOrganizationId()) + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{admin1.GetUser().GetId(), admin2.GetUser().GetId(), admin3.GetUser().GetId()}, + }, + } + response.Administrators[0] = admin3 + response.Administrators[1] = admin2 + response.Administrators[2] = admin1 + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, {}, {}, + }, + }, + }, + { + name: "list multiple id, instance owner", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + admin1 := createInstanceAdministrator(iamOwnerCtx, instance, t) + admin2 := createOrganizationAdministrator(iamOwnerCtx, instance, t) + admin3 := createProjectAdministrator(iamOwnerCtx, instance, t, instance.DefaultOrg.GetId(), projectResp.GetId(), projectName) + admin4 := createProjectGrantAdministrator(iamOwnerCtx, instance, t, instance.DefaultOrg.GetId(), projectResp.GetId(), projectName, orgResp.GetOrganizationId()) + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{admin1.GetUser().GetId(), admin2.GetUser().GetId(), admin3.GetUser().GetId(), admin4.GetUser().GetId()}, + }, + } + response.Administrators[0] = admin4 + response.Administrators[1] = admin3 + response.Administrators[2] = admin2 + response.Administrators[3] = admin1 + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 4, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, {}, {}, {}, + }, + }, + }, + { + name: "list multiple id, org owner", + args: args{ + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + admin1 := createInstanceAdministrator(iamOwnerCtx, instance, t) + admin2 := createOrganizationAdministrator(iamOwnerCtx, instance, t) + admin3 := createProjectAdministrator(iamOwnerCtx, instance, t, instance.DefaultOrg.GetId(), projectResp.GetId(), projectName) + admin4 := createProjectGrantAdministrator(iamOwnerCtx, instance, t, instance.DefaultOrg.GetId(), projectResp.GetId(), projectName, orgResp.GetOrganizationId()) + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{admin1.GetUser().GetId(), admin2.GetUser().GetId(), admin3.GetUser().GetId(), admin4.GetUser().GetId()}, + }, + } + response.Administrators[0] = admin4 + response.Administrators[1] = admin3 + response.Administrators[2] = admin2 + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 4, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, {}, {}, + }, + }, + }, + { + name: "list multiple id, project owner", + args: args{ + ctx: projectOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + admin1 := createInstanceAdministrator(iamOwnerCtx, instance, t) + admin2 := createOrganizationAdministrator(iamOwnerCtx, instance, t) + admin3 := createProjectAdministrator(iamOwnerCtx, instance, t, instance.DefaultOrg.GetId(), projectResp.GetId(), projectName) + admin4 := createProjectGrantAdministrator(iamOwnerCtx, instance, t, instance.DefaultOrg.GetId(), projectResp.GetId(), projectName, orgResp.GetOrganizationId()) + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{admin1.GetUser().GetId(), admin2.GetUser().GetId(), admin3.GetUser().GetId(), admin4.GetUser().GetId()}, + }, + } + response.Administrators[0] = admin4 + response.Administrators[1] = admin3 + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 4, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, {}, + }, + }, + }, + { + name: "list multiple id, project grant owner", + args: args{ + ctx: projectGrantOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + admin1 := createInstanceAdministrator(iamOwnerCtx, instance, t) + admin2 := createOrganizationAdministrator(iamOwnerCtx, instance, t) + admin3 := createProjectAdministrator(iamOwnerCtx, instance, t, instance.DefaultOrg.GetId(), projectResp.GetId(), projectName) + admin4 := createProjectGrantAdministrator(iamOwnerCtx, instance, t, instance.DefaultOrg.GetId(), projectResp.GetId(), projectName, orgResp.GetOrganizationId()) + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{admin1.GetUser().GetId(), admin2.GetUser().GetId(), admin3.GetUser().GetId(), admin4.GetUser().GetId()}, + }, + } + response.Administrators[0] = admin4 + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 4, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.args.dep != nil { + tt.args.dep(tt.args.req, tt.want) + } + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(iamOwnerCtx, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, listErr := instance.Client.InternalPermissionv2Beta.ListAdministrators(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(ttt, listErr) + return + } + require.NoError(ttt, listErr) + + // always first check length, otherwise its failed anyway + if assert.Len(ttt, got.Administrators, len(tt.want.Administrators)) { + for i := range tt.want.Administrators { + assert.EqualExportedValues(ttt, tt.want.Administrators[i], got.Administrators[i]) + } + } + assertPaginationResponse(ttt, tt.want.Pagination, got.Pagination) + }, retryDuration, tick, "timeout waiting for expected execution result") + }) + } +} + +func assertPaginationResponse(t *assert.CollectT, expected *filter.PaginationResponse, actual *filter.PaginationResponse) { + assert.Equal(t, expected.AppliedLimit, actual.AppliedLimit) + assert.Equal(t, expected.TotalResult, actual.TotalResult) +} + +func createInstanceAdministrator(ctx context.Context, instance *integration.Instance, t *testing.T) *internal_permission.Administrator { + email := gofakeit.Email() + userResp := instance.CreateUserTypeHuman(ctx, email) + memberResp := instance.CreateInstanceMembership(t, ctx, userResp.GetId()) + return &internal_permission.Administrator{ + CreationDate: memberResp.GetCreationDate(), + ChangeDate: memberResp.GetCreationDate(), + User: &internal_permission.User{ + Id: userResp.GetId(), + PreferredLoginName: email, + DisplayName: "Mickey Mouse", + OrganizationId: instance.DefaultOrg.GetId(), + }, + Resource: &internal_permission.Administrator_Instance{ + Instance: true, + }, + Roles: []string{"IAM_OWNER"}, + } +} + +func createOrganizationAdministrator(ctx context.Context, instance *integration.Instance, t *testing.T) *internal_permission.Administrator { + email := gofakeit.Email() + userResp := instance.CreateUserTypeHuman(ctx, email) + memberResp := instance.CreateOrgMembership(t, ctx, instance.DefaultOrg.Id, userResp.GetId()) + return &internal_permission.Administrator{ + CreationDate: memberResp.GetCreationDate(), + ChangeDate: memberResp.GetCreationDate(), + User: &internal_permission.User{ + Id: userResp.GetId(), + PreferredLoginName: email, + DisplayName: "Mickey Mouse", + OrganizationId: instance.DefaultOrg.GetId(), + }, + Resource: &internal_permission.Administrator_Organization{ + Organization: &internal_permission.Organization{ + Id: instance.DefaultOrg.GetId(), + Name: instance.DefaultOrg.GetName(), + }, + }, + Roles: []string{"ORG_OWNER"}, + } +} + +func createProjectAdministrator(ctx context.Context, instance *integration.Instance, t *testing.T, orgID, projectID, projectName string) *internal_permission.Administrator { + email := gofakeit.Email() + userResp := instance.CreateUserTypeHuman(ctx, email) + memberResp := instance.CreateProjectMembership(t, ctx, projectID, userResp.GetId()) + return &internal_permission.Administrator{ + CreationDate: memberResp.GetCreationDate(), + ChangeDate: memberResp.GetCreationDate(), + User: &internal_permission.User{ + Id: userResp.GetId(), + PreferredLoginName: email, + DisplayName: "Mickey Mouse", + OrganizationId: instance.DefaultOrg.GetId(), + }, + Resource: &internal_permission.Administrator_Project{ + Project: &internal_permission.Project{ + Id: projectID, + Name: projectName, + OrganizationId: orgID, + }, + }, + Roles: []string{"PROJECT_OWNER"}, + } +} + +func createProjectGrantAdministrator(ctx context.Context, instance *integration.Instance, t *testing.T, orgID, projectID, projectName, grantedOrgID string) *internal_permission.Administrator { + email := gofakeit.Email() + userResp := instance.CreateUserTypeHuman(ctx, email) + memberResp := instance.CreateProjectGrantMembership(t, ctx, projectID, grantedOrgID, userResp.GetId()) + return &internal_permission.Administrator{ + CreationDate: memberResp.GetCreationDate(), + ChangeDate: memberResp.GetCreationDate(), + User: &internal_permission.User{ + Id: userResp.GetId(), + PreferredLoginName: email, + DisplayName: "Mickey Mouse", + OrganizationId: instance.DefaultOrg.GetId(), + }, + Resource: &internal_permission.Administrator_ProjectGrant{ + ProjectGrant: &internal_permission.ProjectGrant{ + Id: grantedOrgID, + ProjectId: projectID, + ProjectName: projectName, + OrganizationId: orgID, + GrantedOrganizationId: grantedOrgID, + }, + }, + Roles: []string{"PROJECT_GRANT_OWNER"}, + } +} + +func TestServer_ListAdministrators_PermissionV2(t *testing.T) { + // removed as permission v2 is not implemented yet for project grant level permissions + // ensureFeaturePermissionV2Enabled(t, instancePermissionV2) + iamOwnerCtx := instancePermissionV2.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + + projectName := gofakeit.AppName() + projectResp := instancePermissionV2.CreateProject(iamOwnerCtx, t, instancePermissionV2.DefaultOrg.GetId(), projectName, false, false) + orgResp := instancePermissionV2.CreateOrganization(iamOwnerCtx, gofakeit.Company(), gofakeit.Email()) + instancePermissionV2.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), orgResp.GetOrganizationId()) + + userProjectResp := instancePermissionV2.CreateMachineUser(iamOwnerCtx) + instancePermissionV2.CreateProjectMembership(t, iamOwnerCtx, projectResp.GetId(), userProjectResp.GetUserId()) + patProjectResp := instancePermissionV2.CreatePersonalAccessToken(iamOwnerCtx, userProjectResp.GetUserId()) + projectOwnerCtx := integration.WithAuthorizationToken(CTX, patProjectResp.Token) + + userProjectGrantResp := instancePermissionV2.CreateMachineUser(iamOwnerCtx) + instancePermissionV2.CreateProjectGrantMembership(t, iamOwnerCtx, projectResp.GetId(), orgResp.GetOrganizationId(), userProjectGrantResp.GetUserId()) + patProjectGrantResp := instancePermissionV2.CreatePersonalAccessToken(iamOwnerCtx, userProjectGrantResp.GetUserId()) + projectGrantOwnerCtx := integration.WithAuthorizationToken(CTX, patProjectGrantResp.Token) + + type args struct { + ctx context.Context + dep func(*internal_permission.ListAdministratorsRequest, *internal_permission.ListAdministratorsResponse) + req *internal_permission.ListAdministratorsRequest + } + tests := []struct { + name string + args args + want *internal_permission.ListAdministratorsResponse + wantErr bool + }{ + { + name: "list by id, unauthenticated", + args: args{ + ctx: CTX, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + admin := createInstanceAdministrator(iamOwnerCtx, instancePermissionV2, t) + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{admin.GetUser().GetId()}, + }, + } + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + wantErr: true, + }, + { + name: "list by id, no permission", + args: args{ + ctx: instancePermissionV2.WithAuthorizationToken(CTX, integration.UserTypeNoPermission), + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + admin := createInstanceAdministrator(iamOwnerCtx, instancePermissionV2, t) + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{admin.GetUser().GetId()}, + }, + } + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{}, + }, + }, + { + name: "list by id, missing permission", + args: args{ + ctx: instancePermissionV2.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + admin := createInstanceAdministrator(iamOwnerCtx, instancePermissionV2, t) + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{admin.GetUser().GetId()}, + }, + } + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{}, + }, + }, + { + name: "list, not found", + args: args{ + ctx: iamOwnerCtx, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{ + { + Filter: &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{Ids: []string{"notexisting"}}, + }, + }, + }, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 0, + AppliedLimit: 100, + }, + }, + }, + { + name: "list single id, instance", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + admin := createInstanceAdministrator(iamOwnerCtx, instancePermissionV2, t) + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{admin.GetUser().GetId()}, + }, + } + response.Administrators[0] = admin + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, + }, + }, + }, + { + name: "list single id, instance", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + admin := createInstanceAdministrator(iamOwnerCtx, instancePermissionV2, t) + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{admin.GetUser().GetId()}, + }, + } + response.Administrators[0] = admin + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, + }, + }, + }, + { + name: "list multiple id, instance", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + admin1 := createInstanceAdministrator(iamOwnerCtx, instancePermissionV2, t) + admin2 := createInstanceAdministrator(iamOwnerCtx, instancePermissionV2, t) + admin3 := createInstanceAdministrator(iamOwnerCtx, instancePermissionV2, t) + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{admin1.GetUser().GetId(), admin2.GetUser().GetId(), admin3.GetUser().GetId()}, + }, + } + response.Administrators[0] = admin3 + response.Administrators[1] = admin2 + response.Administrators[2] = admin1 + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, {}, {}, + }, + }, + }, + { + name: "list single id, org", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + admin := createOrganizationAdministrator(iamOwnerCtx, instancePermissionV2, t) + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{admin.GetUser().GetId()}, + }, + } + response.Administrators[0] = admin + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, + }, + }, + }, + { + name: "list multiple id, org", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + admin1 := createOrganizationAdministrator(iamOwnerCtx, instancePermissionV2, t) + admin2 := createOrganizationAdministrator(iamOwnerCtx, instancePermissionV2, t) + admin3 := createOrganizationAdministrator(iamOwnerCtx, instancePermissionV2, t) + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{admin1.GetUser().GetId(), admin2.GetUser().GetId(), admin3.GetUser().GetId()}, + }, + } + response.Administrators[0] = admin3 + response.Administrators[1] = admin2 + response.Administrators[2] = admin1 + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, {}, {}, + }, + }, + }, + { + name: "list single id, project", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + admin := createProjectAdministrator(iamOwnerCtx, instancePermissionV2, t, instancePermissionV2.DefaultOrg.GetId(), projectResp.GetId(), projectName) + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{admin.GetUser().GetId()}, + }, + } + response.Administrators[0] = admin + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, + }, + }, + }, + { + name: "list multiple id, project", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + admin1 := createProjectAdministrator(iamOwnerCtx, instancePermissionV2, t, instancePermissionV2.DefaultOrg.GetId(), projectResp.GetId(), projectName) + admin2 := createProjectAdministrator(iamOwnerCtx, instancePermissionV2, t, instancePermissionV2.DefaultOrg.GetId(), projectResp.GetId(), projectName) + admin3 := createProjectAdministrator(iamOwnerCtx, instancePermissionV2, t, instancePermissionV2.DefaultOrg.GetId(), projectResp.GetId(), projectName) + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{admin1.GetUser().GetId(), admin2.GetUser().GetId(), admin3.GetUser().GetId()}, + }, + } + response.Administrators[0] = admin3 + response.Administrators[1] = admin2 + response.Administrators[2] = admin1 + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, {}, {}, + }, + }, + }, + { + name: "list single id, project grant", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + admin := createProjectGrantAdministrator(iamOwnerCtx, instancePermissionV2, t, instancePermissionV2.DefaultOrg.GetId(), projectResp.GetId(), projectName, orgResp.GetOrganizationId()) + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{admin.GetUser().GetId()}, + }, + } + response.Administrators[0] = admin + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, + }, + }, + }, + { + name: "list multiple id, project grant", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + admin1 := createProjectGrantAdministrator(iamOwnerCtx, instancePermissionV2, t, instancePermissionV2.DefaultOrg.GetId(), projectResp.GetId(), projectName, orgResp.GetOrganizationId()) + admin2 := createProjectGrantAdministrator(iamOwnerCtx, instancePermissionV2, t, instancePermissionV2.DefaultOrg.GetId(), projectResp.GetId(), projectName, orgResp.GetOrganizationId()) + admin3 := createProjectGrantAdministrator(iamOwnerCtx, instancePermissionV2, t, instancePermissionV2.DefaultOrg.GetId(), projectResp.GetId(), projectName, orgResp.GetOrganizationId()) + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{admin1.GetUser().GetId(), admin2.GetUser().GetId(), admin3.GetUser().GetId()}, + }, + } + response.Administrators[0] = admin3 + response.Administrators[1] = admin2 + response.Administrators[2] = admin1 + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, {}, {}, + }, + }, + }, + { + name: "list multiple id, instance owner", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + admin1 := createInstanceAdministrator(iamOwnerCtx, instancePermissionV2, t) + admin2 := createOrganizationAdministrator(iamOwnerCtx, instancePermissionV2, t) + admin3 := createProjectAdministrator(iamOwnerCtx, instancePermissionV2, t, instancePermissionV2.DefaultOrg.GetId(), projectResp.GetId(), projectName) + admin4 := createProjectGrantAdministrator(iamOwnerCtx, instancePermissionV2, t, instancePermissionV2.DefaultOrg.GetId(), projectResp.GetId(), projectName, orgResp.GetOrganizationId()) + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{admin1.GetUser().GetId(), admin2.GetUser().GetId(), admin3.GetUser().GetId(), admin4.GetUser().GetId()}, + }, + } + response.Administrators[0] = admin4 + response.Administrators[1] = admin3 + response.Administrators[2] = admin2 + response.Administrators[3] = admin1 + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 4, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, {}, {}, {}, + }, + }, + }, + { + name: "list multiple id, org owner", + args: args{ + ctx: instancePermissionV2.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + admin1 := createInstanceAdministrator(iamOwnerCtx, instancePermissionV2, t) + admin2 := createOrganizationAdministrator(iamOwnerCtx, instancePermissionV2, t) + admin3 := createProjectAdministrator(iamOwnerCtx, instancePermissionV2, t, instancePermissionV2.DefaultOrg.GetId(), projectResp.GetId(), projectName) + admin4 := createProjectGrantAdministrator(iamOwnerCtx, instancePermissionV2, t, instancePermissionV2.DefaultOrg.GetId(), projectResp.GetId(), projectName, orgResp.GetOrganizationId()) + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{admin1.GetUser().GetId(), admin2.GetUser().GetId(), admin3.GetUser().GetId(), admin4.GetUser().GetId()}, + }, + } + response.Administrators[0] = admin4 + response.Administrators[1] = admin3 + response.Administrators[2] = admin2 + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 4, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, {}, {}, + }, + }, + }, + { + name: "list multiple id, project owner", + args: args{ + ctx: projectOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + admin1 := createInstanceAdministrator(iamOwnerCtx, instancePermissionV2, t) + admin2 := createOrganizationAdministrator(iamOwnerCtx, instancePermissionV2, t) + admin3 := createProjectAdministrator(iamOwnerCtx, instancePermissionV2, t, instancePermissionV2.DefaultOrg.GetId(), projectResp.GetId(), projectName) + admin4 := createProjectGrantAdministrator(iamOwnerCtx, instancePermissionV2, t, instancePermissionV2.DefaultOrg.GetId(), projectResp.GetId(), projectName, orgResp.GetOrganizationId()) + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{admin1.GetUser().GetId(), admin2.GetUser().GetId(), admin3.GetUser().GetId(), admin4.GetUser().GetId()}, + }, + } + response.Administrators[0] = admin4 + response.Administrators[1] = admin3 + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 4, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{ + {}, {}, + }, + }, + }, + { + name: "list multiple id, project grant owner", + args: args{ + ctx: projectGrantOwnerCtx, + dep: func(request *internal_permission.ListAdministratorsRequest, response *internal_permission.ListAdministratorsResponse) { + admin1 := createInstanceAdministrator(iamOwnerCtx, instancePermissionV2, t) + admin2 := createOrganizationAdministrator(iamOwnerCtx, instancePermissionV2, t) + admin3 := createProjectAdministrator(iamOwnerCtx, instancePermissionV2, t, instancePermissionV2.DefaultOrg.GetId(), projectResp.GetId(), projectName) + admin4 := createProjectGrantAdministrator(iamOwnerCtx, instancePermissionV2, t, instancePermissionV2.DefaultOrg.GetId(), projectResp.GetId(), projectName, orgResp.GetOrganizationId()) + request.Filters[0].Filter = &internal_permission.AdministratorSearchFilter_InUserIdsFilter{ + InUserIdsFilter: &filter.InIDsFilter{ + Ids: []string{admin1.GetUser().GetId(), admin2.GetUser().GetId(), admin3.GetUser().GetId(), admin4.GetUser().GetId()}, + }, + } + response.Administrators[0] = admin4 + }, + req: &internal_permission.ListAdministratorsRequest{ + Filters: []*internal_permission.AdministratorSearchFilter{{}}, + }, + }, + want: &internal_permission.ListAdministratorsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 4, + AppliedLimit: 100, + }, + Administrators: []*internal_permission.Administrator{{}}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.args.dep != nil { + tt.args.dep(tt.args.req, tt.want) + } + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(iamOwnerCtx, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, listErr := instancePermissionV2.Client.InternalPermissionv2Beta.ListAdministrators(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(ttt, listErr) + return + } + require.NoError(ttt, listErr) + + // always first check length, otherwise its failed anyway + if assert.Len(ttt, got.Administrators, len(tt.want.Administrators)) { + for i := range tt.want.Administrators { + assert.EqualExportedValues(ttt, tt.want.Administrators[i], got.Administrators[i]) + } + } + assertPaginationResponse(ttt, tt.want.Pagination, got.Pagination) + }, retryDuration, tick, "timeout waiting for expected execution result") + }) + } +} diff --git a/internal/api/grpc/internal_permission/v2beta/integration_test/server_test.go b/internal/api/grpc/internal_permission/v2beta/integration_test/server_test.go new file mode 100644 index 0000000000..59d9745222 --- /dev/null +++ b/internal/api/grpc/internal_permission/v2beta/integration_test/server_test.go @@ -0,0 +1,63 @@ +//go:build integration + +package project_test + +import ( + "context" + "os" + "testing" + "time" + + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/internal/integration" + "github.com/zitadel/zitadel/pkg/grpc/feature/v2" +) + +var ( + CTX context.Context + instance *integration.Instance + instancePermissionV2 *integration.Instance +) + +func TestMain(m *testing.M) { + os.Exit(func() int { + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) + defer cancel() + CTX = ctx + instance = integration.NewInstance(ctx) + instancePermissionV2 = integration.NewInstance(CTX) + return m.Run() + }()) +} + +func ensureFeaturePermissionV2Enabled(t *testing.T, instance *integration.Instance) { + ctx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + f, err := instance.Client.FeatureV2.GetInstanceFeatures(ctx, &feature.GetInstanceFeaturesRequest{ + Inheritance: true, + }) + require.NoError(t, err) + if f.PermissionCheckV2.GetEnabled() { + return + } + _, err = instance.Client.FeatureV2.SetInstanceFeatures(ctx, &feature.SetInstanceFeaturesRequest{ + PermissionCheckV2: gu.Ptr(true), + }) + require.NoError(t, err) + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, 5*time.Minute) + require.EventuallyWithT(t, + func(ttt *assert.CollectT) { + f, err := instance.Client.FeatureV2.GetInstanceFeatures(ctx, &feature.GetInstanceFeaturesRequest{ + Inheritance: true, + }) + assert.NoError(ttt, err) + if f.PermissionCheckV2.GetEnabled() { + return + } + }, + retryDuration, + tick, + "timed out waiting for ensuring instance feature") +} diff --git a/internal/api/grpc/internal_permission/v2beta/query.go b/internal/api/grpc/internal_permission/v2beta/query.go new file mode 100644 index 0000000000..3a8de83292 --- /dev/null +++ b/internal/api/grpc/internal_permission/v2beta/query.go @@ -0,0 +1,192 @@ +package internal_permission + +import ( + "context" + + "connectrpc.com/connect" + "google.golang.org/protobuf/types/known/timestamppb" + + filter "github.com/zitadel/zitadel/internal/api/grpc/filter/v2beta" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/zerrors" + filter_pb "github.com/zitadel/zitadel/pkg/grpc/filter/v2beta" + internal_permission "github.com/zitadel/zitadel/pkg/grpc/internal_permission/v2beta" +) + +func (s *Server) ListAdministrators(ctx context.Context, req *connect.Request[internal_permission.ListAdministratorsRequest]) (*connect.Response[internal_permission.ListAdministratorsResponse], error) { + queries, err := s.listAdministratorsRequestToModel(req.Msg) + if err != nil { + return nil, err + } + resp, err := s.query.SearchAdministrators(ctx, queries, s.checkPermission) + if err != nil { + return nil, err + } + return connect.NewResponse(&internal_permission.ListAdministratorsResponse{ + Administrators: administratorsToPb(resp.Administrators), + Pagination: filter.QueryToPaginationPb(queries.SearchRequest, resp.SearchResponse), + }), nil +} + +func (s *Server) listAdministratorsRequestToModel(req *internal_permission.ListAdministratorsRequest) (*query.MembershipSearchQuery, error) { + offset, limit, asc, err := filter.PaginationPbToQuery(s.systemDefaults, req.Pagination) + if err != nil { + return nil, err + } + queries, err := administratorSearchFiltersToQuery(req.Filters) + if err != nil { + return nil, err + } + return &query.MembershipSearchQuery{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + SortingColumn: administratorFieldNameToSortingColumn(req.GetSortingColumn()), + }, + Queries: queries, + }, nil +} + +func administratorFieldNameToSortingColumn(field internal_permission.AdministratorFieldName) query.Column { + switch field { + case internal_permission.AdministratorFieldName_ADMINISTRATOR_FIELD_NAME_CREATION_DATE: + return query.MembershipCreationDate + case internal_permission.AdministratorFieldName_ADMINISTRATOR_FIELD_NAME_USER_ID: + return query.MembershipUserID + case internal_permission.AdministratorFieldName_ADMINISTRATOR_FIELD_NAME_CHANGE_DATE: + return query.MembershipChangeDate + case internal_permission.AdministratorFieldName_ADMINISTRATOR_FIELD_NAME_UNSPECIFIED: + return query.MembershipCreationDate + default: + return query.MembershipCreationDate + } +} + +func administratorSearchFiltersToQuery(queries []*internal_permission.AdministratorSearchFilter) (_ []query.SearchQuery, err error) { + q := make([]query.SearchQuery, len(queries)) + for i, qry := range queries { + q[i], err = administratorFilterToModel(qry) + if err != nil { + return nil, err + } + } + return q, nil +} + +func administratorFilterToModel(filter *internal_permission.AdministratorSearchFilter) (query.SearchQuery, error) { + switch q := filter.Filter.(type) { + case *internal_permission.AdministratorSearchFilter_InUserIdsFilter: + return inUserIDsFilterToQuery(q.InUserIdsFilter) + case *internal_permission.AdministratorSearchFilter_CreationDate: + return creationDateFilterToQuery(q.CreationDate) + case *internal_permission.AdministratorSearchFilter_ChangeDate: + return changeDateFilterToQuery(q.ChangeDate) + case *internal_permission.AdministratorSearchFilter_UserOrganizationId: + return userResourceOwnerFilterToQuery(q.UserOrganizationId) + case *internal_permission.AdministratorSearchFilter_UserPreferredLoginName: + return userLoginNameFilterToQuery(q.UserPreferredLoginName) + case *internal_permission.AdministratorSearchFilter_UserDisplayName: + return userDisplayNameFilterToQuery(q.UserDisplayName) + case *internal_permission.AdministratorSearchFilter_Resource: + return resourceFilterToQuery(q.Resource) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "ORG-vR9nC", "List.Query.Invalid") + } +} + +func inUserIDsFilterToQuery(q *filter_pb.InIDsFilter) (query.SearchQuery, error) { + return query.NewMemberInUserIDsSearchQuery(q.GetIds()) +} + +func userResourceOwnerFilterToQuery(q *filter_pb.IDFilter) (query.SearchQuery, error) { + return query.NewAdministratorUserResourceOwnerSearchQuery(q.GetId()) +} + +func userLoginNameFilterToQuery(q *internal_permission.UserPreferredLoginNameFilter) (query.SearchQuery, error) { + return query.NewAdministratorUserLoginNameSearchQuery(q.GetPreferredLoginName()) +} + +func userDisplayNameFilterToQuery(q *internal_permission.UserDisplayNameFilter) (query.SearchQuery, error) { + return query.NewAdministratorUserDisplayNameSearchQuery(q.GetDisplayName()) +} + +func creationDateFilterToQuery(q *filter_pb.TimestampFilter) (query.SearchQuery, error) { + return query.NewMembershipCreationDateQuery(q.GetTimestamp().AsTime(), filter.TimestampMethodPbToQuery(q.Method)) +} + +func changeDateFilterToQuery(q *filter_pb.TimestampFilter) (query.SearchQuery, error) { + return query.NewMembershipChangeDateQuery(q.GetTimestamp().AsTime(), filter.TimestampMethodPbToQuery(q.Method)) +} + +func resourceFilterToQuery(q *internal_permission.ResourceFilter) (query.SearchQuery, error) { + switch q.GetResource().(type) { + case *internal_permission.ResourceFilter_Instance: + if q.GetInstance() { + return query.NewMembershipIsIAMQuery() + } + case *internal_permission.ResourceFilter_OrganizationId: + return query.NewMembershipOrgIDQuery(q.GetOrganizationId()) + case *internal_permission.ResourceFilter_ProjectId: + return query.NewMembershipProjectIDQuery(q.GetProjectId()) + case *internal_permission.ResourceFilter_ProjectGrantId: + return query.NewMembershipProjectGrantIDQuery(q.GetProjectGrantId()) + } + return nil, nil +} + +func administratorsToPb(administrators []*query.Administrator) []*internal_permission.Administrator { + a := make([]*internal_permission.Administrator, len(administrators)) + for i, admin := range administrators { + a[i] = administratorToPb(admin) + } + return a +} + +func administratorToPb(admin *query.Administrator) *internal_permission.Administrator { + var resource internal_permission.Resource + if admin.Instance != nil { + resource = &internal_permission.Administrator_Instance{Instance: true} + } + if admin.Org != nil { + resource = &internal_permission.Administrator_Organization{ + Organization: &internal_permission.Organization{ + Id: admin.Org.OrgID, + Name: admin.Org.Name, + }, + } + } + if admin.Project != nil { + resource = &internal_permission.Administrator_Project{ + Project: &internal_permission.Project{ + Id: admin.Project.ProjectID, + Name: admin.Project.Name, + OrganizationId: admin.Project.ResourceOwner, + }, + } + } + if admin.ProjectGrant != nil { + resource = &internal_permission.Administrator_ProjectGrant{ + ProjectGrant: &internal_permission.ProjectGrant{ + Id: admin.ProjectGrant.GrantID, + ProjectId: admin.ProjectGrant.ProjectID, + ProjectName: admin.ProjectGrant.ProjectName, + OrganizationId: admin.ProjectGrant.ResourceOwner, + GrantedOrganizationId: admin.ProjectGrant.GrantedOrgID, + }, + } + } + + return &internal_permission.Administrator{ + CreationDate: timestamppb.New(admin.CreationDate), + ChangeDate: timestamppb.New(admin.ChangeDate), + User: &internal_permission.User{ + Id: admin.User.UserID, + PreferredLoginName: admin.User.LoginName, + DisplayName: admin.User.DisplayName, + OrganizationId: admin.User.ResourceOwner, + }, + Resource: resource, + Roles: admin.Roles, + } +} diff --git a/internal/api/grpc/internal_permission/v2beta/server.go b/internal/api/grpc/internal_permission/v2beta/server.go new file mode 100644 index 0000000000..bc1a999faa --- /dev/null +++ b/internal/api/grpc/internal_permission/v2beta/server.go @@ -0,0 +1,66 @@ +package internal_permission + +import ( + "net/http" + + "connectrpc.com/connect" + "google.golang.org/protobuf/reflect/protoreflect" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/api/grpc/server" + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/config/systemdefaults" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/query" + internal_permission "github.com/zitadel/zitadel/pkg/grpc/internal_permission/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/internal_permission/v2beta/internal_permissionconnect" +) + +var _ internal_permissionconnect.InternalPermissionServiceHandler = (*Server)(nil) + +type Server struct { + systemDefaults systemdefaults.SystemDefaults + command *command.Commands + query *query.Queries + checkPermission domain.PermissionCheck +} + +type Config struct{} + +func CreateServer( + systemDefaults systemdefaults.SystemDefaults, + command *command.Commands, + query *query.Queries, + checkPermission domain.PermissionCheck, +) *Server { + return &Server{ + systemDefaults: systemDefaults, + command: command, + query: query, + checkPermission: checkPermission, + } +} + +func (s *Server) RegisterConnectServer(interceptors ...connect.Interceptor) (string, http.Handler) { + return internal_permissionconnect.NewInternalPermissionServiceHandler(s, connect.WithInterceptors(interceptors...)) +} + +func (s *Server) FileDescriptor() protoreflect.FileDescriptor { + return internal_permission.File_zitadel_internal_permission_v2beta_internal_permission_service_proto +} + +func (s *Server) AppName() string { + return internal_permission.InternalPermissionService_ServiceDesc.ServiceName +} + +func (s *Server) MethodPrefix() string { + return internal_permission.InternalPermissionService_ServiceDesc.ServiceName +} + +func (s *Server) AuthMethods() authz.MethodMapping { + return internal_permission.InternalPermissionService_AuthMethods +} + +func (s *Server) RegisterGateway() server.RegisterGatewayFunc { + return internal_permission.RegisterInternalPermissionServiceHandler +} diff --git a/internal/api/grpc/management/org.go b/internal/api/grpc/management/org.go index 57caeda3ce..a006db063d 100644 --- a/internal/api/grpc/management/org.go +++ b/internal/api/grpc/management/org.go @@ -278,28 +278,28 @@ func (s *Server) ListOrgMembers(ctx context.Context, req *mgmt_pb.ListOrgMembers } func (s *Server) AddOrgMember(ctx context.Context, req *mgmt_pb.AddOrgMemberRequest) (*mgmt_pb.AddOrgMemberResponse, error) { - addedMember, err := s.command.AddOrgMember(ctx, authz.GetCtxData(ctx).OrgID, req.UserId, req.Roles...) + addedMember, err := s.command.AddOrgMember(ctx, AddOrgMemberRequestToCommand(req, authz.GetCtxData(ctx).OrgID)) if err != nil { return nil, err } return &mgmt_pb.AddOrgMemberResponse{ Details: object.AddToDetailsPb( addedMember.Sequence, - addedMember.ChangeDate, + addedMember.EventDate, addedMember.ResourceOwner, ), }, nil } func (s *Server) UpdateOrgMember(ctx context.Context, req *mgmt_pb.UpdateOrgMemberRequest) (*mgmt_pb.UpdateOrgMemberResponse, error) { - changedMember, err := s.command.ChangeOrgMember(ctx, UpdateOrgMemberRequestToDomain(ctx, req)) + changedMember, err := s.command.ChangeOrgMember(ctx, UpdateOrgMemberRequestToCommand(req, authz.GetCtxData(ctx).OrgID)) if err != nil { return nil, err } return &mgmt_pb.UpdateOrgMemberResponse{ Details: object.ChangeToDetailsPb( changedMember.Sequence, - changedMember.ChangeDate, + changedMember.EventDate, changedMember.ResourceOwner, ), }, nil diff --git a/internal/api/grpc/management/org_converter.go b/internal/api/grpc/management/org_converter.go index 879b5e0763..07c772189c 100644 --- a/internal/api/grpc/management/org_converter.go +++ b/internal/api/grpc/management/org_converter.go @@ -8,6 +8,7 @@ import ( "github.com/zitadel/zitadel/internal/api/grpc/metadata" "github.com/zitadel/zitadel/internal/api/grpc/object" org_grpc "github.com/zitadel/zitadel/internal/api/grpc/org" + "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore/v1/models" "github.com/zitadel/zitadel/internal/query" @@ -26,7 +27,7 @@ func ListOrgDomainsRequestToModel(req *mgmt_pb.ListOrgDomainsRequest) (*query.Or Limit: limit, Asc: asc, }, - //SortingColumn: //TODO: sorting + // SortingColumn: //TODO: sorting Queries: queries, }, nil } @@ -67,8 +68,20 @@ func SetPrimaryOrgDomainRequestToDomain(ctx context.Context, req *mgmt_pb.SetPri } } -func UpdateOrgMemberRequestToDomain(ctx context.Context, req *mgmt_pb.UpdateOrgMemberRequest) *domain.Member { - return domain.NewMember(authz.GetCtxData(ctx).OrgID, req.UserId, req.Roles...) +func AddOrgMemberRequestToCommand(req *mgmt_pb.AddOrgMemberRequest, orgID string) *command.AddOrgMember { + return &command.AddOrgMember{ + OrgID: orgID, + UserID: req.UserId, + Roles: req.Roles, + } +} + +func UpdateOrgMemberRequestToCommand(req *mgmt_pb.UpdateOrgMemberRequest, orgID string) *command.ChangeOrgMember { + return &command.ChangeOrgMember{ + OrgID: orgID, + UserID: req.UserId, + Roles: req.Roles, + } } func ListOrgMembersRequestToModel(ctx context.Context, req *mgmt_pb.ListOrgMembersRequest) (*query.OrgMembersQuery, error) { @@ -89,7 +102,7 @@ func ListOrgMembersRequestToModel(ctx context.Context, req *mgmt_pb.ListOrgMembe Offset: offset, Limit: limit, Asc: asc, - //SortingColumn: //TODO: sorting + // SortingColumn: //TODO: sorting }, Queries: queries, }, diff --git a/internal/api/grpc/management/project.go b/internal/api/grpc/management/project.go index 52b6b10e9a..be196d14ce 100644 --- a/internal/api/grpc/management/project.go +++ b/internal/api/grpc/management/project.go @@ -47,7 +47,7 @@ func (s *Server) ListProjects(ctx context.Context, req *mgmt_pb.ListProjectsRequ if err != nil { return nil, err } - projects, err := s.query.SearchProjects(ctx, queries) + projects, err := s.query.SearchProjects(ctx, queries, nil) if err != nil { return nil, err } @@ -109,7 +109,7 @@ func (s *Server) ListGrantedProjects(ctx context.Context, req *mgmt_pb.ListGrant if err != nil { return nil, err } - projects, err := s.query.SearchProjectGrants(ctx, queries) + projects, err := s.query.SearchProjectGrants(ctx, queries, nil) if err != nil { return nil, err } @@ -175,25 +175,26 @@ func (s *Server) ListProjectChanges(ctx context.Context, req *mgmt_pb.ListProjec } func (s *Server) AddProject(ctx context.Context, req *mgmt_pb.AddProjectRequest) (*mgmt_pb.AddProjectResponse, error) { - project, err := s.command.AddProject(ctx, ProjectCreateToDomain(req), authz.GetCtxData(ctx).OrgID) + add := ProjectCreateToCommand(req, "", authz.GetCtxData(ctx).OrgID) + project, err := s.command.AddProject(ctx, add) if err != nil { return nil, err } return &mgmt_pb.AddProjectResponse{ - Id: project.AggregateID, - Details: object_grpc.AddToDetailsPb(project.Sequence, project.ChangeDate, project.ResourceOwner), + Id: add.AggregateID, + Details: object_grpc.AddToDetailsPb(project.Sequence, project.EventDate, project.ResourceOwner), }, nil } func (s *Server) UpdateProject(ctx context.Context, req *mgmt_pb.UpdateProjectRequest) (*mgmt_pb.UpdateProjectResponse, error) { - project, err := s.command.ChangeProject(ctx, ProjectUpdateToDomain(req), authz.GetCtxData(ctx).OrgID) + project, err := s.command.ChangeProject(ctx, ProjectUpdateToCommand(req, authz.GetCtxData(ctx).OrgID)) if err != nil { return nil, err } return &mgmt_pb.UpdateProjectResponse{ Details: object_grpc.ChangeToDetailsPb( project.Sequence, - project.ChangeDate, + project.EventDate, project.ResourceOwner, ), }, nil @@ -226,7 +227,7 @@ func (s *Server) RemoveProject(ctx context.Context, req *mgmt_pb.RemoveProjectRe } grants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{ Queries: []query.SearchQuery{projectQuery}, - }, true) + }, true, nil) if err != nil { return nil, err } @@ -252,7 +253,7 @@ func (s *Server) ListProjectRoles(ctx context.Context, req *mgmt_pb.ListProjectR if err != nil { return nil, err } - roles, err := s.query.SearchProjectRoles(ctx, true, queries) + roles, err := s.query.SearchProjectRoles(ctx, true, queries, nil) if err != nil { return nil, err } @@ -263,21 +264,21 @@ func (s *Server) ListProjectRoles(ctx context.Context, req *mgmt_pb.ListProjectR } func (s *Server) AddProjectRole(ctx context.Context, req *mgmt_pb.AddProjectRoleRequest) (*mgmt_pb.AddProjectRoleResponse, error) { - role, err := s.command.AddProjectRole(ctx, AddProjectRoleRequestToDomain(req), authz.GetCtxData(ctx).OrgID) + role, err := s.command.AddProjectRole(ctx, AddProjectRoleRequestToCommand(req, authz.GetCtxData(ctx).OrgID)) if err != nil { return nil, err } return &mgmt_pb.AddProjectRoleResponse{ Details: object_grpc.AddToDetailsPb( role.Sequence, - role.ChangeDate, + role.EventDate, role.ResourceOwner, ), }, nil } func (s *Server) BulkAddProjectRoles(ctx context.Context, req *mgmt_pb.BulkAddProjectRolesRequest) (*mgmt_pb.BulkAddProjectRolesResponse, error) { - details, err := s.command.BulkAddProjectRole(ctx, req.ProjectId, authz.GetCtxData(ctx).OrgID, BulkAddProjectRolesRequestToDomain(req)) + details, err := s.command.BulkAddProjectRole(ctx, req.ProjectId, authz.GetCtxData(ctx).OrgID, BulkAddProjectRolesRequestToCommand(req, authz.GetCtxData(ctx).OrgID)) if err != nil { return nil, err } @@ -287,14 +288,14 @@ func (s *Server) BulkAddProjectRoles(ctx context.Context, req *mgmt_pb.BulkAddPr } func (s *Server) UpdateProjectRole(ctx context.Context, req *mgmt_pb.UpdateProjectRoleRequest) (*mgmt_pb.UpdateProjectRoleResponse, error) { - role, err := s.command.ChangeProjectRole(ctx, UpdateProjectRoleRequestToDomain(req), authz.GetCtxData(ctx).OrgID) + role, err := s.command.ChangeProjectRole(ctx, UpdateProjectRoleRequestToCommand(req, authz.GetCtxData(ctx).OrgID)) if err != nil { return nil, err } return &mgmt_pb.UpdateProjectRoleResponse{ Details: object_grpc.ChangeToDetailsPb( role.Sequence, - role.ChangeDate, + role.EventDate, role.ResourceOwner, ), }, nil @@ -311,7 +312,7 @@ func (s *Server) RemoveProjectRole(ctx context.Context, req *mgmt_pb.RemoveProje } userGrants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{ Queries: []query.SearchQuery{projectQuery, rolesQuery}, - }, false) + }, false, nil) if err != nil { return nil, err @@ -353,24 +354,28 @@ func (s *Server) ListProjectMembers(ctx context.Context, req *mgmt_pb.ListProjec } func (s *Server) AddProjectMember(ctx context.Context, req *mgmt_pb.AddProjectMemberRequest) (*mgmt_pb.AddProjectMemberResponse, error) { - member, err := s.command.AddProjectMember(ctx, AddProjectMemberRequestToDomain(req), authz.GetCtxData(ctx).OrgID) + member, err := s.command.AddProjectMember(ctx, AddProjectMemberRequestToCommand(req, authz.GetCtxData(ctx).OrgID)) if err != nil { return nil, err } return &mgmt_pb.AddProjectMemberResponse{ - Details: object_grpc.AddToDetailsPb(member.Sequence, member.ChangeDate, member.ResourceOwner), + Details: object_grpc.AddToDetailsPb( + member.Sequence, + member.EventDate, + member.ResourceOwner, + ), }, nil } func (s *Server) UpdateProjectMember(ctx context.Context, req *mgmt_pb.UpdateProjectMemberRequest) (*mgmt_pb.UpdateProjectMemberResponse, error) { - member, err := s.command.ChangeProjectMember(ctx, UpdateProjectMemberRequestToDomain(req), authz.GetCtxData(ctx).OrgID) + member, err := s.command.ChangeProjectMember(ctx, UpdateProjectMemberRequestToCommand(req, authz.GetCtxData(ctx).OrgID)) if err != nil { return nil, err } return &mgmt_pb.UpdateProjectMemberResponse{ Details: object_grpc.ChangeToDetailsPb( member.Sequence, - member.ChangeDate, + member.EventDate, member.ResourceOwner, ), }, nil diff --git a/internal/api/grpc/management/project_application.go b/internal/api/grpc/management/project_application.go index 3a0e1d5f92..a5526d3cb7 100644 --- a/internal/api/grpc/management/project_application.go +++ b/internal/api/grpc/management/project_application.go @@ -29,7 +29,7 @@ func (s *Server) ListApps(ctx context.Context, req *mgmt_pb.ListAppsRequest) (*m if err != nil { return nil, err } - apps, err := s.query.SearchApps(ctx, queries, false) + apps, err := s.query.SearchApps(ctx, queries, nil) if err != nil { return nil, err } @@ -125,7 +125,7 @@ func (s *Server) AddAPIApp(ctx context.Context, req *mgmt_pb.AddAPIAppRequest) ( } func (s *Server) UpdateApp(ctx context.Context, req *mgmt_pb.UpdateAppRequest) (*mgmt_pb.UpdateAppResponse, error) { - details, err := s.command.ChangeApplication(ctx, req.ProjectId, UpdateAppRequestToDomain(req), authz.GetCtxData(ctx).OrgID) + details, err := s.command.UpdateApplicationName(ctx, req.ProjectId, UpdateAppRequestToDomain(req), authz.GetCtxData(ctx).OrgID) if err != nil { return nil, err } @@ -139,7 +139,7 @@ func (s *Server) UpdateOIDCAppConfig(ctx context.Context, req *mgmt_pb.UpdateOID if err != nil { return nil, err } - config, err := s.command.ChangeOIDCApplication(ctx, oidcApp, authz.GetCtxData(ctx).OrgID) + config, err := s.command.UpdateOIDCApplication(ctx, oidcApp, authz.GetCtxData(ctx).OrgID) if err != nil { return nil, err } @@ -157,7 +157,7 @@ func (s *Server) UpdateSAMLAppConfig(ctx context.Context, req *mgmt_pb.UpdateSAM if err != nil { return nil, err } - config, err := s.command.ChangeSAMLApplication(ctx, samlApp, authz.GetCtxData(ctx).OrgID) + config, err := s.command.UpdateSAMLApplication(ctx, samlApp, authz.GetCtxData(ctx).OrgID) if err != nil { return nil, err } @@ -171,7 +171,7 @@ func (s *Server) UpdateSAMLAppConfig(ctx context.Context, req *mgmt_pb.UpdateSAM } func (s *Server) UpdateAPIAppConfig(ctx context.Context, req *mgmt_pb.UpdateAPIAppConfigRequest) (*mgmt_pb.UpdateAPIAppConfigResponse, error) { - config, err := s.command.ChangeAPIApplication(ctx, UpdateAPIAppConfigRequestToDomain(req), authz.GetCtxData(ctx).OrgID) + config, err := s.command.UpdateAPIApplication(ctx, UpdateAPIAppConfigRequestToDomain(req), authz.GetCtxData(ctx).OrgID) if err != nil { return nil, err } @@ -271,7 +271,7 @@ func (s *Server) ListAppKeys(ctx context.Context, req *mgmt_pb.ListAppKeysReques if err != nil { return nil, err } - keys, err := s.query.SearchAuthNKeys(ctx, queries, false) + keys, err := s.query.SearchAuthNKeys(ctx, queries, query.JoinFilterApp, nil) if err != nil { return nil, err } diff --git a/internal/api/grpc/management/project_application_converter.go b/internal/api/grpc/management/project_application_converter.go index 13a0048a5b..fa31565445 100644 --- a/internal/api/grpc/management/project_application_converter.go +++ b/internal/api/grpc/management/project_application_converter.go @@ -4,6 +4,8 @@ import ( "context" "time" + "github.com/muhlemmer/gu" + "github.com/zitadel/zitadel/internal/api/authz" authn_grpc "github.com/zitadel/zitadel/internal/api/grpc/authn" "github.com/zitadel/zitadel/internal/api/grpc/object" @@ -46,24 +48,24 @@ func AddOIDCAppRequestToDomain(req *mgmt_pb.AddOIDCAppRequest) (*domain.OIDCApp, AggregateID: req.ProjectId, }, AppName: req.Name, - OIDCVersion: app_grpc.OIDCVersionToDomain(req.Version), + OIDCVersion: gu.Ptr(app_grpc.OIDCVersionToDomain(req.Version)), RedirectUris: req.RedirectUris, ResponseTypes: app_grpc.OIDCResponseTypesToDomain(req.ResponseTypes), GrantTypes: app_grpc.OIDCGrantTypesToDomain(req.GrantTypes), - ApplicationType: app_grpc.OIDCApplicationTypeToDomain(req.AppType), - AuthMethodType: app_grpc.OIDCAuthMethodTypeToDomain(req.AuthMethodType), + ApplicationType: gu.Ptr(app_grpc.OIDCApplicationTypeToDomain(req.AppType)), + AuthMethodType: gu.Ptr(app_grpc.OIDCAuthMethodTypeToDomain(req.AuthMethodType)), PostLogoutRedirectUris: req.PostLogoutRedirectUris, - DevMode: req.DevMode, - AccessTokenType: app_grpc.OIDCTokenTypeToDomain(req.AccessTokenType), - AccessTokenRoleAssertion: req.AccessTokenRoleAssertion, - IDTokenRoleAssertion: req.IdTokenRoleAssertion, - IDTokenUserinfoAssertion: req.IdTokenUserinfoAssertion, - ClockSkew: req.ClockSkew.AsDuration(), + DevMode: gu.Ptr(req.GetDevMode()), + AccessTokenType: gu.Ptr(app_grpc.OIDCTokenTypeToDomain(req.AccessTokenType)), + AccessTokenRoleAssertion: gu.Ptr(req.GetAccessTokenRoleAssertion()), + IDTokenRoleAssertion: gu.Ptr(req.GetIdTokenRoleAssertion()), + IDTokenUserinfoAssertion: gu.Ptr(req.GetIdTokenUserinfoAssertion()), + ClockSkew: gu.Ptr(req.GetClockSkew().AsDuration()), AdditionalOrigins: req.AdditionalOrigins, - SkipNativeAppSuccessPage: req.SkipNativeAppSuccessPage, - BackChannelLogoutURI: req.GetBackChannelLogoutUri(), - LoginVersion: loginVersion, - LoginBaseURI: loginBaseURI, + SkipNativeAppSuccessPage: gu.Ptr(req.GetSkipNativeAppSuccessPage()), + BackChannelLogoutURI: gu.Ptr(req.GetBackChannelLogoutUri()), + LoginVersion: gu.Ptr(loginVersion), + LoginBaseURI: gu.Ptr(loginBaseURI), }, nil } @@ -78,9 +80,9 @@ func AddSAMLAppRequestToDomain(req *mgmt_pb.AddSAMLAppRequest) (*domain.SAMLApp, }, AppName: req.Name, Metadata: req.GetMetadataXml(), - MetadataURL: req.GetMetadataUrl(), - LoginVersion: loginVersion, - LoginBaseURI: loginBaseURI, + MetadataURL: gu.Ptr(req.GetMetadataUrl()), + LoginVersion: gu.Ptr(loginVersion), + LoginBaseURI: gu.Ptr(loginBaseURI), }, nil } @@ -114,20 +116,20 @@ func UpdateOIDCAppConfigRequestToDomain(app *mgmt_pb.UpdateOIDCAppConfigRequest) RedirectUris: app.RedirectUris, ResponseTypes: app_grpc.OIDCResponseTypesToDomain(app.ResponseTypes), GrantTypes: app_grpc.OIDCGrantTypesToDomain(app.GrantTypes), - ApplicationType: app_grpc.OIDCApplicationTypeToDomain(app.AppType), - AuthMethodType: app_grpc.OIDCAuthMethodTypeToDomain(app.AuthMethodType), + ApplicationType: gu.Ptr(app_grpc.OIDCApplicationTypeToDomain(app.AppType)), + AuthMethodType: gu.Ptr(app_grpc.OIDCAuthMethodTypeToDomain(app.AuthMethodType)), PostLogoutRedirectUris: app.PostLogoutRedirectUris, - DevMode: app.DevMode, - AccessTokenType: app_grpc.OIDCTokenTypeToDomain(app.AccessTokenType), - AccessTokenRoleAssertion: app.AccessTokenRoleAssertion, - IDTokenRoleAssertion: app.IdTokenRoleAssertion, - IDTokenUserinfoAssertion: app.IdTokenUserinfoAssertion, - ClockSkew: app.ClockSkew.AsDuration(), + DevMode: gu.Ptr(app.GetDevMode()), + AccessTokenType: gu.Ptr(app_grpc.OIDCTokenTypeToDomain(app.AccessTokenType)), + AccessTokenRoleAssertion: gu.Ptr(app.GetAccessTokenRoleAssertion()), + IDTokenRoleAssertion: gu.Ptr(app.GetIdTokenRoleAssertion()), + IDTokenUserinfoAssertion: gu.Ptr(app.GetIdTokenUserinfoAssertion()), + ClockSkew: gu.Ptr(app.GetClockSkew().AsDuration()), AdditionalOrigins: app.AdditionalOrigins, - SkipNativeAppSuccessPage: app.SkipNativeAppSuccessPage, - BackChannelLogoutURI: app.BackChannelLogoutUri, - LoginVersion: loginVersion, - LoginBaseURI: loginBaseURI, + SkipNativeAppSuccessPage: gu.Ptr(app.GetSkipNativeAppSuccessPage()), + BackChannelLogoutURI: gu.Ptr(app.GetBackChannelLogoutUri()), + LoginVersion: gu.Ptr(loginVersion), + LoginBaseURI: gu.Ptr(loginBaseURI), }, nil } @@ -142,9 +144,9 @@ func UpdateSAMLAppConfigRequestToDomain(app *mgmt_pb.UpdateSAMLAppConfigRequest) }, AppID: app.AppId, Metadata: app.GetMetadataXml(), - MetadataURL: app.GetMetadataUrl(), - LoginVersion: loginVersion, - LoginBaseURI: loginBaseURI, + MetadataURL: gu.Ptr(app.GetMetadataUrl()), + LoginVersion: gu.Ptr(loginVersion), + LoginBaseURI: gu.Ptr(loginBaseURI), }, nil } @@ -175,7 +177,7 @@ func AddAPIClientKeyRequestToDomain(key *mgmt_pb.AddAppKeyRequest) *domain.Appli } func ListAPIClientKeysRequestToQuery(ctx context.Context, req *mgmt_pb.ListAppKeysRequest) (*query.AuthNKeySearchQueries, error) { - resourcOwner, err := query.NewAuthNKeyResourceOwnerQuery(authz.GetCtxData(ctx).OrgID) + resourceOwner, err := query.NewAuthNKeyResourceOwnerQuery(authz.GetCtxData(ctx).OrgID) if err != nil { return nil, err } @@ -195,7 +197,7 @@ func ListAPIClientKeysRequestToQuery(ctx context.Context, req *mgmt_pb.ListAppKe Asc: asc, }, Queries: []query.SearchQuery{ - resourcOwner, + resourceOwner, projectID, appID, }, diff --git a/internal/api/grpc/management/project_converter.go b/internal/api/grpc/management/project_converter.go index 64243ba258..8ea0014800 100644 --- a/internal/api/grpc/management/project_converter.go +++ b/internal/api/grpc/management/project_converter.go @@ -3,10 +3,13 @@ package management import ( "context" + "github.com/muhlemmer/gu" + "github.com/zitadel/zitadel/internal/api/authz" member_grpc "github.com/zitadel/zitadel/internal/api/grpc/member" "github.com/zitadel/zitadel/internal/api/grpc/object" proj_grpc "github.com/zitadel/zitadel/internal/api/grpc/project" + "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore/v1/models" "github.com/zitadel/zitadel/internal/query" @@ -14,8 +17,12 @@ import ( proj_pb "github.com/zitadel/zitadel/pkg/grpc/project" ) -func ProjectCreateToDomain(req *mgmt_pb.AddProjectRequest) *domain.Project { - return &domain.Project{ +func ProjectCreateToCommand(req *mgmt_pb.AddProjectRequest, projectID string, resourceOwner string) *command.AddProject { + return &command.AddProject{ + ObjectRoot: models.ObjectRoot{ + AggregateID: projectID, + ResourceOwner: resourceOwner, + }, Name: req.Name, ProjectRoleAssertion: req.ProjectRoleAssertion, ProjectRoleCheck: req.ProjectRoleCheck, @@ -24,16 +31,17 @@ func ProjectCreateToDomain(req *mgmt_pb.AddProjectRequest) *domain.Project { } } -func ProjectUpdateToDomain(req *mgmt_pb.UpdateProjectRequest) *domain.Project { - return &domain.Project{ +func ProjectUpdateToCommand(req *mgmt_pb.UpdateProjectRequest, resourceOwner string) *command.ChangeProject { + return &command.ChangeProject{ ObjectRoot: models.ObjectRoot{ - AggregateID: req.Id, + AggregateID: req.Id, + ResourceOwner: resourceOwner, }, - Name: req.Name, - ProjectRoleAssertion: req.ProjectRoleAssertion, - ProjectRoleCheck: req.ProjectRoleCheck, - HasProjectCheck: req.HasProjectCheck, - PrivateLabelingSetting: privateLabelingSettingToDomain(req.PrivateLabelingSetting), + Name: gu.Ptr(req.Name), + ProjectRoleAssertion: gu.Ptr(req.ProjectRoleAssertion), + ProjectRoleCheck: gu.Ptr(req.ProjectRoleCheck), + HasProjectCheck: gu.Ptr(req.HasProjectCheck), + PrivateLabelingSetting: gu.Ptr(privateLabelingSettingToDomain(req.PrivateLabelingSetting)), } } @@ -48,10 +56,11 @@ func privateLabelingSettingToDomain(setting proj_pb.PrivateLabelingSetting) doma } } -func AddProjectRoleRequestToDomain(req *mgmt_pb.AddProjectRoleRequest) *domain.ProjectRole { - return &domain.ProjectRole{ +func AddProjectRoleRequestToCommand(req *mgmt_pb.AddProjectRoleRequest, resourceOwner string) *command.AddProjectRole { + return &command.AddProjectRole{ ObjectRoot: models.ObjectRoot{ - AggregateID: req.ProjectId, + AggregateID: req.ProjectId, + ResourceOwner: resourceOwner, }, Key: req.RoleKey, DisplayName: req.DisplayName, @@ -59,12 +68,13 @@ func AddProjectRoleRequestToDomain(req *mgmt_pb.AddProjectRoleRequest) *domain.P } } -func BulkAddProjectRolesRequestToDomain(req *mgmt_pb.BulkAddProjectRolesRequest) []*domain.ProjectRole { - roles := make([]*domain.ProjectRole, len(req.Roles)) +func BulkAddProjectRolesRequestToCommand(req *mgmt_pb.BulkAddProjectRolesRequest, resourceOwner string) []*command.AddProjectRole { + roles := make([]*command.AddProjectRole, len(req.Roles)) for i, role := range req.Roles { - roles[i] = &domain.ProjectRole{ + roles[i] = &command.AddProjectRole{ ObjectRoot: models.ObjectRoot{ - AggregateID: req.ProjectId, + AggregateID: req.ProjectId, + ResourceOwner: resourceOwner, }, Key: role.Key, DisplayName: role.DisplayName, @@ -74,10 +84,11 @@ func BulkAddProjectRolesRequestToDomain(req *mgmt_pb.BulkAddProjectRolesRequest) return roles } -func UpdateProjectRoleRequestToDomain(req *mgmt_pb.UpdateProjectRoleRequest) *domain.ProjectRole { - return &domain.ProjectRole{ +func UpdateProjectRoleRequestToCommand(req *mgmt_pb.UpdateProjectRoleRequest, resourceOwner string) *command.ChangeProjectRole { + return &command.ChangeProjectRole{ ObjectRoot: models.ObjectRoot{ - AggregateID: req.ProjectId, + AggregateID: req.ProjectId, + ResourceOwner: resourceOwner, }, Key: req.RoleKey, DisplayName: req.DisplayName, @@ -93,12 +104,22 @@ func ProjectGrantsToIDs(projectGrants *query.ProjectGrants) []string { return converted } -func AddProjectMemberRequestToDomain(req *mgmt_pb.AddProjectMemberRequest) *domain.Member { - return domain.NewMember(req.ProjectId, req.UserId, req.Roles...) +func AddProjectMemberRequestToCommand(req *mgmt_pb.AddProjectMemberRequest, orgID string) *command.AddProjectMember { + return &command.AddProjectMember{ + ResourceOwner: orgID, + ProjectID: req.ProjectId, + UserID: req.UserId, + Roles: req.Roles, + } } -func UpdateProjectMemberRequestToDomain(req *mgmt_pb.UpdateProjectMemberRequest) *domain.Member { - return domain.NewMember(req.ProjectId, req.UserId, req.Roles...) +func UpdateProjectMemberRequestToCommand(req *mgmt_pb.UpdateProjectMemberRequest, orgID string) *command.ChangeProjectMember { + return &command.ChangeProjectMember{ + ResourceOwner: orgID, + ProjectID: req.ProjectId, + UserID: req.UserId, + Roles: req.Roles, + } } func listProjectRequestToModel(req *mgmt_pb.ListProjectsRequest) (*query.ProjectSearchQueries, error) { diff --git a/internal/api/grpc/management/project_grant.go b/internal/api/grpc/management/project_grant.go index cea8e929a4..26f51f6851 100644 --- a/internal/api/grpc/management/project_grant.go +++ b/internal/api/grpc/management/project_grant.go @@ -31,7 +31,7 @@ func (s *Server) ListProjectGrants(ctx context.Context, req *mgmt_pb.ListProject if err != nil { return nil, err } - grants, err := s.query.SearchProjectGrants(ctx, queries) + grants, err := s.query.SearchProjectGrants(ctx, queries, nil) if err != nil { return nil, err } @@ -54,7 +54,7 @@ func (s *Server) ListAllProjectGrants(ctx context.Context, req *mgmt_pb.ListAllP if err != nil { return nil, err } - grants, err := s.query.SearchProjectGrants(ctx, queries) + grants, err := s.query.SearchProjectGrants(ctx, queries, nil) if err != nil { return nil, err } @@ -65,16 +65,17 @@ func (s *Server) ListAllProjectGrants(ctx context.Context, req *mgmt_pb.ListAllP } func (s *Server) AddProjectGrant(ctx context.Context, req *mgmt_pb.AddProjectGrantRequest) (*mgmt_pb.AddProjectGrantResponse, error) { - grant, err := s.command.AddProjectGrant(ctx, AddProjectGrantRequestToDomain(req), authz.GetCtxData(ctx).OrgID) + grant := AddProjectGrantRequestToCommand(req, "", authz.GetCtxData(ctx).OrgID) + details, err := s.command.AddProjectGrant(ctx, grant) if err != nil { return nil, err } return &mgmt_pb.AddProjectGrantResponse{ GrantId: grant.GrantID, Details: object_grpc.AddToDetailsPb( - grant.Sequence, - grant.ChangeDate, - grant.ResourceOwner, + details.Sequence, + details.EventDate, + details.ResourceOwner, ), }, nil } @@ -90,25 +91,25 @@ func (s *Server) UpdateProjectGrant(ctx context.Context, req *mgmt_pb.UpdateProj } grants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{ Queries: []query.SearchQuery{projectQuery, grantQuery}, - }, true) + }, true, nil) if err != nil { return nil, err } - grant, err := s.command.ChangeProjectGrant(ctx, UpdateProjectGrantRequestToDomain(req), authz.GetCtxData(ctx).OrgID, userGrantsToIDs(grants.UserGrants)...) + grant, err := s.command.ChangeProjectGrant(ctx, UpdateProjectGrantRequestToCommand(req, authz.GetCtxData(ctx).OrgID), userGrantsToIDs(grants.UserGrants)...) if err != nil { return nil, err } return &mgmt_pb.UpdateProjectGrantResponse{ Details: object_grpc.ChangeToDetailsPb( grant.Sequence, - grant.ChangeDate, + grant.EventDate, grant.ResourceOwner, ), }, nil } func (s *Server) DeactivateProjectGrant(ctx context.Context, req *mgmt_pb.DeactivateProjectGrantRequest) (*mgmt_pb.DeactivateProjectGrantResponse, error) { - details, err := s.command.DeactivateProjectGrant(ctx, req.ProjectId, req.GrantId, authz.GetCtxData(ctx).OrgID) + details, err := s.command.DeactivateProjectGrant(ctx, req.ProjectId, req.GrantId, "", authz.GetCtxData(ctx).OrgID) if err != nil { return nil, err } @@ -118,7 +119,7 @@ func (s *Server) DeactivateProjectGrant(ctx context.Context, req *mgmt_pb.Deacti } func (s *Server) ReactivateProjectGrant(ctx context.Context, req *mgmt_pb.ReactivateProjectGrantRequest) (*mgmt_pb.ReactivateProjectGrantResponse, error) { - details, err := s.command.ReactivateProjectGrant(ctx, req.ProjectId, req.GrantId, authz.GetCtxData(ctx).OrgID) + details, err := s.command.ReactivateProjectGrant(ctx, req.ProjectId, req.GrantId, "", authz.GetCtxData(ctx).OrgID) if err != nil { return nil, err } @@ -138,7 +139,7 @@ func (s *Server) RemoveProjectGrant(ctx context.Context, req *mgmt_pb.RemoveProj } userGrants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{ Queries: []query.SearchQuery{projectQuery, grantQuery}, - }, false) + }, false, nil) if err != nil { return nil, err } @@ -175,28 +176,28 @@ func (s *Server) ListProjectGrantMembers(ctx context.Context, req *mgmt_pb.ListP } func (s *Server) AddProjectGrantMember(ctx context.Context, req *mgmt_pb.AddProjectGrantMemberRequest) (*mgmt_pb.AddProjectGrantMemberResponse, error) { - member, err := s.command.AddProjectGrantMember(ctx, AddProjectGrantMemberRequestToDomain(req)) + member, err := s.command.AddProjectGrantMember(ctx, AddProjectGrantMemberRequestToCommand(req, authz.GetCtxData(ctx).OrgID)) if err != nil { return nil, err } return &mgmt_pb.AddProjectGrantMemberResponse{ Details: object_grpc.AddToDetailsPb( member.Sequence, - member.ChangeDate, + member.EventDate, member.ResourceOwner, ), }, nil } func (s *Server) UpdateProjectGrantMember(ctx context.Context, req *mgmt_pb.UpdateProjectGrantMemberRequest) (*mgmt_pb.UpdateProjectGrantMemberResponse, error) { - member, err := s.command.ChangeProjectGrantMember(ctx, UpdateProjectGrantMemberRequestToDomain(req)) + member, err := s.command.ChangeProjectGrantMember(ctx, UpdateProjectGrantMemberRequestToCommand(req)) if err != nil { return nil, err } return &mgmt_pb.UpdateProjectGrantMemberResponse{ Details: object_grpc.ChangeToDetailsPb( member.Sequence, - member.ChangeDate, + member.EventDate, member.ResourceOwner, ), }, nil diff --git a/internal/api/grpc/management/project_grant_converter.go b/internal/api/grpc/management/project_grant_converter.go index de7d1de041..0523eed13a 100644 --- a/internal/api/grpc/management/project_grant_converter.go +++ b/internal/api/grpc/management/project_grant_converter.go @@ -6,7 +6,7 @@ import ( "github.com/zitadel/zitadel/internal/api/authz" member_grpc "github.com/zitadel/zitadel/internal/api/grpc/member" "github.com/zitadel/zitadel/internal/api/grpc/object" - "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/eventstore/v1/models" "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/zerrors" @@ -100,20 +100,23 @@ func AllProjectGrantQueryToModel(apiQuery *proj_pb.AllProjectGrantQuery) (query. return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-M099f", "List.Query.Invalid") } } -func AddProjectGrantRequestToDomain(req *mgmt_pb.AddProjectGrantRequest) *domain.ProjectGrant { - return &domain.ProjectGrant{ +func AddProjectGrantRequestToCommand(req *mgmt_pb.AddProjectGrantRequest, grantID string, resourceOwner string) *command.AddProjectGrant { + return &command.AddProjectGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: req.ProjectId, + AggregateID: req.ProjectId, + ResourceOwner: resourceOwner, }, + GrantID: grantID, GrantedOrgID: req.GrantedOrgId, RoleKeys: req.RoleKeys, } } -func UpdateProjectGrantRequestToDomain(req *mgmt_pb.UpdateProjectGrantRequest) *domain.ProjectGrant { - return &domain.ProjectGrant{ +func UpdateProjectGrantRequestToCommand(req *mgmt_pb.UpdateProjectGrantRequest, resourceOwner string) *command.ChangeProjectGrant { + return &command.ChangeProjectGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: req.ProjectId, + AggregateID: req.ProjectId, + ResourceOwner: resourceOwner, }, GrantID: req.GrantId, RoleKeys: req.RoleKeys, @@ -142,24 +145,21 @@ func ListProjectGrantMembersRequestToModel(ctx context.Context, req *mgmt_pb.Lis }, nil } -func AddProjectGrantMemberRequestToDomain(req *mgmt_pb.AddProjectGrantMemberRequest) *domain.ProjectGrantMember { - return &domain.ProjectGrantMember{ - ObjectRoot: models.ObjectRoot{ - AggregateID: req.ProjectId, - }, - GrantID: req.GrantId, - UserID: req.UserId, - Roles: req.Roles, +func AddProjectGrantMemberRequestToCommand(req *mgmt_pb.AddProjectGrantMemberRequest, orgID string) *command.AddProjectGrantMember { + return &command.AddProjectGrantMember{ + ResourceOwner: orgID, + ProjectID: req.ProjectId, + GrantID: req.GrantId, + UserID: req.UserId, + Roles: req.Roles, } } -func UpdateProjectGrantMemberRequestToDomain(req *mgmt_pb.UpdateProjectGrantMemberRequest) *domain.ProjectGrantMember { - return &domain.ProjectGrantMember{ - ObjectRoot: models.ObjectRoot{ - AggregateID: req.ProjectId, - }, - GrantID: req.GrantId, - UserID: req.UserId, - Roles: req.Roles, +func UpdateProjectGrantMemberRequestToCommand(req *mgmt_pb.UpdateProjectGrantMemberRequest) *command.ChangeProjectGrantMember { + return &command.ChangeProjectGrantMember{ + ProjectID: req.ProjectId, + GrantID: req.GrantId, + UserID: req.UserId, + Roles: req.Roles, } } diff --git a/internal/api/grpc/management/user.go b/internal/api/grpc/management/user.go index b876999584..f40a29868e 100644 --- a/internal/api/grpc/management/user.go +++ b/internal/api/grpc/management/user.go @@ -69,7 +69,7 @@ func (s *Server) ListUsers(ctx context.Context, req *mgmt_pb.ListUsersRequest) ( if err != nil { return nil, err } - res, err := s.query.SearchUsers(ctx, queries, orgID, nil) + res, err := s.query.SearchUsers(ctx, queries, nil) if err != nil { return nil, err } @@ -142,7 +142,7 @@ func (s *Server) ListUserMetadata(ctx context.Context, req *mgmt_pb.ListUserMeta if err != nil { return nil, err } - res, err := s.query.SearchUserMetadata(ctx, true, req.Id, metadataQueries, false) + res, err := s.query.SearchUserMetadata(ctx, true, req.Id, metadataQueries, nil) if err != nil { return nil, err } @@ -273,7 +273,8 @@ func (s *Server) ImportHumanUser(ctx context.Context, req *mgmt_pb.ImportHumanUs if err != nil { return nil, err } - addedHuman, code, err := s.command.ImportHuman(ctx, authz.GetCtxData(ctx).OrgID, human, passwordless, links, initCodeGenerator, phoneCodeGenerator, emailCodeGenerator, passwordlessInitCode) + //nolint:staticcheck + addedHuman, code, err := s.command.ImportHuman(ctx, authz.GetCtxData(ctx).OrgID, human, passwordless, nil, links, initCodeGenerator, phoneCodeGenerator, emailCodeGenerator, passwordlessInitCode) if err != nil { return nil, err } @@ -297,7 +298,7 @@ func (s *Server) ImportHumanUser(ctx context.Context, req *mgmt_pb.ImportHumanUs func (s *Server) AddMachineUser(ctx context.Context, req *mgmt_pb.AddMachineUserRequest) (*mgmt_pb.AddMachineUserResponse, error) { machine := AddMachineUserRequestToCommand(req, authz.GetCtxData(ctx).OrgID) - objectDetails, err := s.command.AddMachine(ctx, machine) + objectDetails, err := s.command.AddMachine(ctx, machine, nil, nil) if err != nil { return nil, err } @@ -368,7 +369,7 @@ func (s *Server) removeUserDependencies(ctx context.Context, userID string) ([]* } grants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{ Queries: []query.SearchQuery{userGrantUserQuery}, - }, true) + }, true, nil) if err != nil { return nil, nil, err } @@ -752,11 +753,11 @@ func (s *Server) GetMachineKeyByIDs(ctx context.Context, req *mgmt_pb.GetMachine } func (s *Server) ListMachineKeys(ctx context.Context, req *mgmt_pb.ListMachineKeysRequest) (*mgmt_pb.ListMachineKeysResponse, error) { - query, err := ListMachineKeysRequestToQuery(ctx, req) + q, err := ListMachineKeysRequestToQuery(ctx, req) if err != nil { return nil, err } - result, err := s.query.SearchAuthNKeys(ctx, query, false) + result, err := s.query.SearchAuthNKeys(ctx, q, query.JoinFilterUserMachine, nil) if err != nil { return nil, err } @@ -774,7 +775,6 @@ func (s *Server) AddMachineKey(ctx context.Context, req *mgmt_pb.AddMachineKeyRe if err != nil { return nil, err } - // Return key details only if the pubkey wasn't supplied, otherwise the user already has // private key locally var keyDetails []byte @@ -821,7 +821,7 @@ func (s *Server) GenerateMachineSecret(ctx context.Context, req *mgmt_pb.Generat } func (s *Server) RemoveMachineSecret(ctx context.Context, req *mgmt_pb.RemoveMachineSecretRequest) (*mgmt_pb.RemoveMachineSecretResponse, error) { - objectDetails, err := s.command.RemoveMachineSecret(ctx, req.UserId, authz.GetCtxData(ctx).OrgID) + objectDetails, err := s.command.RemoveMachineSecret(ctx, req.UserId, authz.GetCtxData(ctx).OrgID, nil) if err != nil { return nil, err } @@ -839,7 +839,7 @@ func (s *Server) GetPersonalAccessTokenByIDs(ctx context.Context, req *mgmt_pb.G if err != nil { return nil, err } - token, err := s.query.PersonalAccessTokenByID(ctx, true, req.TokenId, false, resourceOwner, aggregateID) + token, err := s.query.PersonalAccessTokenByID(ctx, true, req.TokenId, resourceOwner, aggregateID) if err != nil { return nil, err } @@ -853,7 +853,7 @@ func (s *Server) ListPersonalAccessTokens(ctx context.Context, req *mgmt_pb.List if err != nil { return nil, err } - result, err := s.query.SearchPersonalAccessTokens(ctx, queries, false) + result, err := s.query.SearchPersonalAccessTokens(ctx, queries, nil) if err != nil { return nil, err } @@ -901,6 +901,7 @@ func (s *Server) ListHumanLinkedIDPs(ctx context.Context, req *mgmt_pb.ListHuman Details: obj_grpc.ToListDetails(res.Count, res.Sequence, res.LastRun), }, nil } + func (s *Server) RemoveHumanLinkedIDP(ctx context.Context, req *mgmt_pb.RemoveHumanLinkedIDPRequest) (*mgmt_pb.RemoveHumanLinkedIDPResponse, error) { objectDetails, err := s.command.RemoveUserIDPLink(ctx, RemoveHumanLinkedIDPRequestToDomain(ctx, req)) if err != nil { @@ -947,18 +948,21 @@ func cascadingIAMMembership(membership *query.IAMMembership) *command.CascadingI } return &command.CascadingIAMMembership{IAMID: membership.IAMID} } + func cascadingOrgMembership(membership *query.OrgMembership) *command.CascadingOrgMembership { if membership == nil { return nil } return &command.CascadingOrgMembership{OrgID: membership.OrgID} } + func cascadingProjectMembership(membership *query.ProjectMembership) *command.CascadingProjectMembership { if membership == nil { return nil } return &command.CascadingProjectMembership{ProjectID: membership.ProjectID} } + func cascadingProjectGrantMembership(membership *query.ProjectGrantMembership) *command.CascadingProjectGrantMembership { if membership == nil { return nil diff --git a/internal/api/grpc/management/user_grant.go b/internal/api/grpc/management/user_grant.go index 8589b49e30..3a894c8c29 100644 --- a/internal/api/grpc/management/user_grant.go +++ b/internal/api/grpc/management/user_grant.go @@ -33,7 +33,7 @@ func (s *Server) ListUserGrants(ctx context.Context, req *mgmt_pb.ListUserGrantR if err != nil { return nil, err } - res, err := s.query.UserGrants(ctx, queries, false) + res, err := s.query.UserGrants(ctx, queries, false, nil) if err != nil { return nil, err } @@ -44,11 +44,11 @@ func (s *Server) ListUserGrants(ctx context.Context, req *mgmt_pb.ListUserGrantR } func (s *Server) AddUserGrant(ctx context.Context, req *mgmt_pb.AddUserGrantRequest) (*mgmt_pb.AddUserGrantResponse, error) { - grant := AddUserGrantRequestToDomain(req) + grant := AddUserGrantRequestToDomain(req, authz.GetCtxData(ctx).OrgID) if err := checkExplicitProjectPermission(ctx, grant.ProjectGrantID, grant.ProjectID); err != nil { return nil, err } - grant, err := s.command.AddUserGrant(ctx, grant, authz.GetCtxData(ctx).OrgID) + grant, err := s.command.AddUserGrant(ctx, grant, nil) if err != nil { return nil, err } @@ -63,7 +63,7 @@ func (s *Server) AddUserGrant(ctx context.Context, req *mgmt_pb.AddUserGrantRequ } func (s *Server) UpdateUserGrant(ctx context.Context, req *mgmt_pb.UpdateUserGrantRequest) (*mgmt_pb.UpdateUserGrantResponse, error) { - grant, err := s.command.ChangeUserGrant(ctx, UpdateUserGrantRequestToDomain(req), authz.GetCtxData(ctx).OrgID) + grant, err := s.command.ChangeUserGrant(ctx, UpdateUserGrantRequestToDomain(req, authz.GetCtxData(ctx).OrgID), false, false, nil) if err != nil { return nil, err } @@ -77,7 +77,7 @@ func (s *Server) UpdateUserGrant(ctx context.Context, req *mgmt_pb.UpdateUserGra } func (s *Server) DeactivateUserGrant(ctx context.Context, req *mgmt_pb.DeactivateUserGrantRequest) (*mgmt_pb.DeactivateUserGrantResponse, error) { - objectDetails, err := s.command.DeactivateUserGrant(ctx, req.GrantId, authz.GetCtxData(ctx).OrgID) + objectDetails, err := s.command.DeactivateUserGrant(ctx, req.GrantId, authz.GetCtxData(ctx).OrgID, nil) if err != nil { return nil, err } @@ -87,7 +87,7 @@ func (s *Server) DeactivateUserGrant(ctx context.Context, req *mgmt_pb.Deactivat } func (s *Server) ReactivateUserGrant(ctx context.Context, req *mgmt_pb.ReactivateUserGrantRequest) (*mgmt_pb.ReactivateUserGrantResponse, error) { - objectDetails, err := s.command.ReactivateUserGrant(ctx, req.GrantId, authz.GetCtxData(ctx).OrgID) + objectDetails, err := s.command.ReactivateUserGrant(ctx, req.GrantId, authz.GetCtxData(ctx).OrgID, nil) if err != nil { return nil, err } @@ -97,7 +97,7 @@ func (s *Server) ReactivateUserGrant(ctx context.Context, req *mgmt_pb.Reactivat } func (s *Server) RemoveUserGrant(ctx context.Context, req *mgmt_pb.RemoveUserGrantRequest) (*mgmt_pb.RemoveUserGrantResponse, error) { - objectDetails, err := s.command.RemoveUserGrant(ctx, req.GrantId, authz.GetCtxData(ctx).OrgID) + objectDetails, err := s.command.RemoveUserGrant(ctx, req.GrantId, authz.GetCtxData(ctx).OrgID, false, nil) if err != nil { return nil, err } diff --git a/internal/api/grpc/management/user_grant_converter.go b/internal/api/grpc/management/user_grant_converter.go index 17d992d251..ad1b4acdf6 100644 --- a/internal/api/grpc/management/user_grant_converter.go +++ b/internal/api/grpc/management/user_grant_converter.go @@ -49,19 +49,23 @@ func shouldAppendUserGrantOwnerQuery(queries []*user.UserGrantQuery) bool { return true } -func AddUserGrantRequestToDomain(req *mgmt_pb.AddUserGrantRequest) *domain.UserGrant { +func AddUserGrantRequestToDomain(req *mgmt_pb.AddUserGrantRequest, resourceowner string) *domain.UserGrant { return &domain.UserGrant{ UserID: req.UserId, ProjectID: req.ProjectId, ProjectGrantID: req.ProjectGrantId, RoleKeys: req.RoleKeys, + ObjectRoot: models.ObjectRoot{ + ResourceOwner: resourceowner, + }, } } -func UpdateUserGrantRequestToDomain(req *mgmt_pb.UpdateUserGrantRequest) *domain.UserGrant { +func UpdateUserGrantRequestToDomain(req *mgmt_pb.UpdateUserGrantRequest, resourceowner string) *domain.UserGrant { return &domain.UserGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: req.GrantId, + AggregateID: req.GrantId, + ResourceOwner: resourceowner, }, UserID: req.UserId, RoleKeys: req.RoleKeys, diff --git a/internal/api/grpc/metadata/v2/metadata.go b/internal/api/grpc/metadata/v2/metadata.go new file mode 100644 index 0000000000..f50ad57f64 --- /dev/null +++ b/internal/api/grpc/metadata/v2/metadata.go @@ -0,0 +1,47 @@ +package metadata + +import ( + "google.golang.org/protobuf/types/known/timestamppb" + + filter_v2 "github.com/zitadel/zitadel/internal/api/grpc/filter/v2" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/zerrors" + meta_pb "github.com/zitadel/zitadel/pkg/grpc/metadata/v2" +) + +func UserMetadataListToPb(dataList []*query.UserMetadata) []*meta_pb.Metadata { + mds := make([]*meta_pb.Metadata, len(dataList)) + for i, data := range dataList { + mds[i] = UserMetadataToPb(data) + } + return mds +} + +func UserMetadataToPb(data *query.UserMetadata) *meta_pb.Metadata { + return &meta_pb.Metadata{ + Key: data.Key, + Value: data.Value, + CreationDate: timestamppb.New(data.CreationDate), + ChangeDate: timestamppb.New(data.ChangeDate), + } +} + +func UserMetadataFiltersToQuery(queries []*meta_pb.MetadataSearchFilter) (_ []query.SearchQuery, err error) { + q := make([]query.SearchQuery, len(queries)) + for i, query := range queries { + q[i], err = UserMetadataFilterToQuery(query) + if err != nil { + return nil, err + } + } + return q, nil +} + +func UserMetadataFilterToQuery(filter *meta_pb.MetadataSearchFilter) (query.SearchQuery, error) { + switch q := filter.Filter.(type) { + case *meta_pb.MetadataSearchFilter_KeyFilter: + return query.NewUserMetadataKeySearchQuery(q.KeyFilter.Key, filter_v2.TextMethodPbToQuery(q.KeyFilter.Method)) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "METAD-fdg23", "List.Query.Invalid") + } +} diff --git a/internal/api/grpc/metadata/v2beta/metadata.go b/internal/api/grpc/metadata/v2beta/metadata.go new file mode 100644 index 0000000000..57da21dfd2 --- /dev/null +++ b/internal/api/grpc/metadata/v2beta/metadata.go @@ -0,0 +1,49 @@ +package metadata + +import ( + "google.golang.org/protobuf/types/known/timestamppb" + + v2beta_object "github.com/zitadel/zitadel/internal/api/grpc/object/v2beta" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/zerrors" + meta_pb "github.com/zitadel/zitadel/pkg/grpc/metadata/v2beta" +) + +// code in this file is copied from internal/api/grpc/metadata/metadata.go + +func OrgMetadataListToPb(dataList []*query.OrgMetadata) []*meta_pb.Metadata { + mds := make([]*meta_pb.Metadata, len(dataList)) + for i, data := range dataList { + mds[i] = OrgMetadataToPb(data) + } + return mds +} + +func OrgMetadataToPb(data *query.OrgMetadata) *meta_pb.Metadata { + return &meta_pb.Metadata{ + Key: data.Key, + Value: data.Value, + CreationDate: timestamppb.New(data.CreationDate), + ChangeDate: timestamppb.New(data.ChangeDate), + } +} + +func OrgMetadataQueriesToQuery(queries []*meta_pb.MetadataQuery) (_ []query.SearchQuery, err error) { + q := make([]query.SearchQuery, len(queries)) + for i, query := range queries { + q[i], err = OrgMetadataQueryToQuery(query) + if err != nil { + return nil, err + } + } + return q, nil +} + +func OrgMetadataQueryToQuery(metadataQuery *meta_pb.MetadataQuery) (query.SearchQuery, error) { + switch q := metadataQuery.Query.(type) { + case *meta_pb.MetadataQuery_KeyQuery: + return query.NewOrgMetadataKeySearchQuery(q.KeyQuery.Key, v2beta_object.TextMethodToQuery(q.KeyQuery.Method)) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "METAD-fdg23", "List.Query.Invalid") + } +} diff --git a/internal/api/grpc/object/v2beta/converter.go b/internal/api/grpc/object/v2beta/converter.go index 9b14bb677a..73d5f18843 100644 --- a/internal/api/grpc/object/v2beta/converter.go +++ b/internal/api/grpc/object/v2beta/converter.go @@ -9,6 +9,7 @@ import ( "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query" object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" + org_pb "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" ) func DomainToDetailsPb(objectDetail *domain.ObjectDetails) *object.Details { @@ -34,6 +35,7 @@ func ToListDetails(response query.SearchResponse) *object.ListDetails { return details } + func ListQueryToQuery(query *object.ListQuery) (offset, limit uint64, asc bool) { if query == nil { return 0, 0, false @@ -73,3 +75,56 @@ func TextMethodToQuery(method object.TextQueryMethod) query.TextComparison { return -1 } } + +func ListQueryToModel(query *object.ListQuery) (offset, limit uint64, asc bool) { + if query == nil { + return 0, 0, false + } + return query.Offset, uint64(query.Limit), query.Asc +} + +func DomainsToPb(domains []*query.Domain) []*org_pb.Domain { + d := make([]*org_pb.Domain, len(domains)) + for i, domain := range domains { + d[i] = DomainToPb(domain) + } + return d +} + +func DomainToPb(d *query.Domain) *org_pb.Domain { + return &org_pb.Domain{ + OrganizationId: d.OrgID, + DomainName: d.Domain, + IsVerified: d.IsVerified, + IsPrimary: d.IsPrimary, + ValidationType: DomainValidationTypeFromModel(d.ValidationType), + } +} + +func DomainValidationTypeFromModel(validationType domain.OrgDomainValidationType) org_pb.DomainValidationType { + switch validationType { + case domain.OrgDomainValidationTypeDNS: + return org_pb.DomainValidationType_DOMAIN_VALIDATION_TYPE_DNS + case domain.OrgDomainValidationTypeHTTP: + return org_pb.DomainValidationType_DOMAIN_VALIDATION_TYPE_HTTP + case domain.OrgDomainValidationTypeUnspecified: + // added to please golangci-lint + return org_pb.DomainValidationType_DOMAIN_VALIDATION_TYPE_UNSPECIFIED + default: + return org_pb.DomainValidationType_DOMAIN_VALIDATION_TYPE_UNSPECIFIED + } +} + +func DomainValidationTypeToDomain(validationType org_pb.DomainValidationType) domain.OrgDomainValidationType { + switch validationType { + case org_pb.DomainValidationType_DOMAIN_VALIDATION_TYPE_HTTP: + return domain.OrgDomainValidationTypeHTTP + case org_pb.DomainValidationType_DOMAIN_VALIDATION_TYPE_DNS: + return domain.OrgDomainValidationTypeDNS + case org_pb.DomainValidationType_DOMAIN_VALIDATION_TYPE_UNSPECIFIED: + // added to please golangci-lint + return domain.OrgDomainValidationTypeUnspecified + default: + return domain.OrgDomainValidationTypeUnspecified + } +} diff --git a/internal/api/grpc/oidc/v2/integration_test/oidc_test.go b/internal/api/grpc/oidc/v2/integration_test/oidc_test.go index 93d16b2155..31d6177201 100644 --- a/internal/api/grpc/oidc/v2/integration_test/oidc_test.go +++ b/internal/api/grpc/oidc/v2/integration_test/oidc_test.go @@ -24,8 +24,7 @@ import ( ) func TestServer_GetAuthRequest(t *testing.T) { - project, err := Instance.CreateProject(CTX) - require.NoError(t, err) + project := Instance.CreateProject(CTX, t, "", gofakeit.AppName(), false, false) client, err := Instance.CreateOIDCNativeClient(CTX, redirectURI, logoutRedirectURI, project.GetId(), false) require.NoError(t, err) @@ -98,8 +97,7 @@ func TestServer_GetAuthRequest(t *testing.T) { } func TestServer_CreateCallback(t *testing.T) { - project, err := Instance.CreateProject(CTX) - require.NoError(t, err) + project := Instance.CreateProject(CTX, t, "", gofakeit.AppName(), false, false) client, err := Instance.CreateOIDCNativeClient(CTX, redirectURI, logoutRedirectURI, project.GetId(), false) require.NoError(t, err) clientV2, err := Instance.CreateOIDCClientLoginVersion(CTX, redirectURI, logoutRedirectURI, project.GetId(), app.OIDCAppType_OIDC_APP_TYPE_NATIVE, app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE, false, loginV2) @@ -307,7 +305,7 @@ func TestServer_CreateCallback(t *testing.T) { ctx: CTXLoginClient, req: &oidc_pb.CreateCallbackRequest{ AuthRequestId: func() string { - client, err := Instance.CreateOIDCImplicitFlowClient(CTX, redirectURIImplicit, nil) + client, err := Instance.CreateOIDCImplicitFlowClient(CTX, t, redirectURIImplicit, nil) require.NoError(t, err) authRequestID, err := Instance.CreateOIDCAuthRequestImplicit(CTXLoginClient, client.GetClientId(), Instance.Users.Get(integration.UserTypeLogin).ID, redirectURIImplicit) require.NoError(t, err) @@ -334,7 +332,7 @@ func TestServer_CreateCallback(t *testing.T) { ctx: CTXLoginClient, req: &oidc_pb.CreateCallbackRequest{ AuthRequestId: func() string { - clientV2, err := Instance.CreateOIDCImplicitFlowClient(CTX, redirectURIImplicit, loginV2) + clientV2, err := Instance.CreateOIDCImplicitFlowClient(CTX, t, redirectURIImplicit, loginV2) require.NoError(t, err) authRequestID, err := Instance.CreateOIDCAuthRequestImplicitWithoutLoginClientHeader(CTXLoginClient, clientV2.GetClientId(), redirectURIImplicit) require.NoError(t, err) @@ -390,7 +388,7 @@ func TestServer_CreateCallback_Permission(t *testing.T) { projectID2, _ := createOIDCApplication(ctx, t, true, true) orgResp := Instance.CreateOrganization(ctx, "oidc-permission-"+gofakeit.AppName(), gofakeit.Email()) - Instance.CreateProjectGrant(ctx, projectID2, orgResp.GetOrganizationId()) + Instance.CreateProjectGrant(ctx, t, projectID2, orgResp.GetOrganizationId()) user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone()) Instance.CreateProjectUserGrant(t, ctx, projectID, user.GetUserId()) @@ -405,7 +403,7 @@ func TestServer_CreateCallback_Permission(t *testing.T) { projectID, clientID := createOIDCApplication(ctx, t, true, true) orgResp := Instance.CreateOrganization(ctx, "oidc-permission-"+gofakeit.AppName(), gofakeit.Email()) - Instance.CreateProjectGrant(ctx, projectID, orgResp.GetOrganizationId()) + Instance.CreateProjectGrant(ctx, t, projectID, orgResp.GetOrganizationId()) user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone()) Instance.CreateProjectUserGrant(t, ctx, projectID, user.GetUserId()) @@ -426,9 +424,9 @@ func TestServer_CreateCallback_Permission(t *testing.T) { projectID, clientID := createOIDCApplication(ctx, t, true, true) orgResp := Instance.CreateOrganization(ctx, "oidc-permission-"+gofakeit.AppName(), gofakeit.Email()) - projectGrantResp := Instance.CreateProjectGrant(ctx, projectID, orgResp.GetOrganizationId()) + Instance.CreateProjectGrant(ctx, t, projectID, orgResp.GetOrganizationId()) user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone()) - Instance.CreateProjectGrantUserGrant(ctx, orgResp.GetOrganizationId(), projectID, projectGrantResp.GetGrantId(), user.GetUserId()) + Instance.CreateProjectGrantUserGrant(ctx, orgResp.GetOrganizationId(), projectID, orgResp.GetOrganizationId(), user.GetUserId()) return createSessionAndAuthRequestForCallback(ctx, t, clientID, Instance.Users.Get(integration.UserTypeLogin).ID, user.GetUserId()) }, @@ -563,9 +561,9 @@ func TestServer_CreateCallback_Permission(t *testing.T) { projectID, clientID := createOIDCApplication(ctx, t, true, false) orgResp := Instance.CreateOrganization(ctx, "oidc-permission-"+gofakeit.AppName(), gofakeit.Email()) - projectGrantResp := Instance.CreateProjectGrant(ctx, projectID, orgResp.GetOrganizationId()) + Instance.CreateProjectGrant(ctx, t, projectID, orgResp.GetOrganizationId()) user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone()) - Instance.CreateProjectGrantUserGrant(ctx, orgResp.GetOrganizationId(), projectID, projectGrantResp.GetGrantId(), user.GetUserId()) + Instance.CreateProjectGrantUserGrant(ctx, orgResp.GetOrganizationId(), projectID, orgResp.GetOrganizationId(), user.GetUserId()) return createSessionAndAuthRequestForCallback(ctx, t, clientID, Instance.Users.Get(integration.UserTypeLogin).ID, user.GetUserId()) }, want: &oidc_pb.CreateCallbackResponse{ @@ -583,7 +581,7 @@ func TestServer_CreateCallback_Permission(t *testing.T) { projectID, clientID := createOIDCApplication(ctx, t, true, false) orgResp := Instance.CreateOrganization(ctx, "oidc-permission-"+gofakeit.AppName(), gofakeit.Email()) - Instance.CreateProjectGrant(ctx, projectID, orgResp.GetOrganizationId()) + Instance.CreateProjectGrant(ctx, t, projectID, orgResp.GetOrganizationId()) user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone()) return createSessionAndAuthRequestForCallback(ctx, t, clientID, Instance.Users.Get(integration.UserTypeLogin).ID, user.GetUserId()) }, @@ -625,7 +623,7 @@ func TestServer_CreateCallback_Permission(t *testing.T) { projectID, clientID := createOIDCApplication(ctx, t, false, true) orgResp := Instance.CreateOrganization(ctx, "oidc-permission-"+gofakeit.AppName(), gofakeit.Email()) - Instance.CreateProjectGrant(ctx, projectID, orgResp.GetOrganizationId()) + Instance.CreateProjectGrant(ctx, t, projectID, orgResp.GetOrganizationId()) user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone()) return createSessionAndAuthRequestForCallback(ctx, t, clientID, Instance.Users.Get(integration.UserTypeLogin).ID, user.GetUserId()) @@ -658,8 +656,7 @@ func TestServer_CreateCallback_Permission(t *testing.T) { } func TestServer_GetDeviceAuthorizationRequest(t *testing.T) { - project, err := Instance.CreateProject(CTX) - require.NoError(t, err) + project := Instance.CreateProject(CTX, t, "", gofakeit.AppName(), false, false) client, err := Instance.CreateOIDCClient(CTX, redirectURI, logoutRedirectURI, project.GetId(), app.OIDCAppType_OIDC_APP_TYPE_NATIVE, app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE, false, app.OIDCGrantType_OIDC_GRANT_TYPE_DEVICE_CODE) require.NoError(t, err) @@ -716,8 +713,7 @@ func TestServer_GetDeviceAuthorizationRequest(t *testing.T) { } func TestServer_AuthorizeOrDenyDeviceAuthorization(t *testing.T) { - project, err := Instance.CreateProject(CTX) - require.NoError(t, err) + project := Instance.CreateProject(CTX, t, "", gofakeit.AppName(), false, false) client, err := Instance.CreateOIDCClient(CTX, redirectURI, logoutRedirectURI, project.GetId(), app.OIDCAppType_OIDC_APP_TYPE_NATIVE, app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE, false, app.OIDCGrantType_OIDC_GRANT_TYPE_DEVICE_CODE) require.NoError(t, err) sessionResp := createSession(t, CTXLoginClient, Instance.Users[integration.UserTypeLogin].ID) @@ -936,8 +932,7 @@ func createSessionAndAuthRequestForCallback(ctx context.Context, t *testing.T, c } func createOIDCApplication(ctx context.Context, t *testing.T, projectRoleCheck, hasProjectCheck bool) (string, string) { - project, err := Instance.CreateProjectWithPermissionCheck(ctx, projectRoleCheck, hasProjectCheck) - require.NoError(t, err) + project := Instance.CreateProject(ctx, t, "", gofakeit.AppName(), projectRoleCheck, hasProjectCheck) clientV2, err := Instance.CreateOIDCClientLoginVersion(ctx, redirectURI, logoutRedirectURI, project.GetId(), app.OIDCAppType_OIDC_APP_TYPE_NATIVE, app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE, false, loginV2) require.NoError(t, err) return project.GetId(), clientV2.GetClientId() diff --git a/internal/api/grpc/oidc/v2/oidc.go b/internal/api/grpc/oidc/v2/oidc.go index 8612d11558..d56d6da056 100644 --- a/internal/api/grpc/oidc/v2/oidc.go +++ b/internal/api/grpc/oidc/v2/oidc.go @@ -4,6 +4,7 @@ import ( "context" "encoding/base64" + "connectrpc.com/connect" "github.com/zitadel/logging" "github.com/zitadel/oidc/v3/pkg/op" "google.golang.org/protobuf/types/known/durationpb" @@ -18,30 +19,30 @@ import ( oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2" ) -func (s *Server) GetAuthRequest(ctx context.Context, req *oidc_pb.GetAuthRequestRequest) (*oidc_pb.GetAuthRequestResponse, error) { - authRequest, err := s.query.AuthRequestByID(ctx, true, req.GetAuthRequestId(), true) +func (s *Server) GetAuthRequest(ctx context.Context, req *connect.Request[oidc_pb.GetAuthRequestRequest]) (*connect.Response[oidc_pb.GetAuthRequestResponse], error) { + authRequest, err := s.query.AuthRequestByID(ctx, true, req.Msg.GetAuthRequestId(), true) if err != nil { logging.WithError(err).Error("query authRequest by ID") return nil, err } - return &oidc_pb.GetAuthRequestResponse{ + return connect.NewResponse(&oidc_pb.GetAuthRequestResponse{ AuthRequest: authRequestToPb(authRequest), - }, nil + }), nil } -func (s *Server) CreateCallback(ctx context.Context, req *oidc_pb.CreateCallbackRequest) (*oidc_pb.CreateCallbackResponse, error) { - switch v := req.GetCallbackKind().(type) { +func (s *Server) CreateCallback(ctx context.Context, req *connect.Request[oidc_pb.CreateCallbackRequest]) (*connect.Response[oidc_pb.CreateCallbackResponse], error) { + switch v := req.Msg.GetCallbackKind().(type) { case *oidc_pb.CreateCallbackRequest_Error: - return s.failAuthRequest(ctx, req.GetAuthRequestId(), v.Error) + return s.failAuthRequest(ctx, req.Msg.GetAuthRequestId(), v.Error) case *oidc_pb.CreateCallbackRequest_Session: - return s.linkSessionToAuthRequest(ctx, req.GetAuthRequestId(), v.Session) + return s.linkSessionToAuthRequest(ctx, req.Msg.GetAuthRequestId(), v.Session) default: return nil, zerrors.ThrowUnimplementedf(nil, "OIDCv2-zee7A", "verification oneOf %T in method CreateCallback not implemented", v) } } -func (s *Server) GetDeviceAuthorizationRequest(ctx context.Context, req *oidc_pb.GetDeviceAuthorizationRequestRequest) (*oidc_pb.GetDeviceAuthorizationRequestResponse, error) { - deviceRequest, err := s.query.DeviceAuthRequestByUserCode(ctx, req.GetUserCode()) +func (s *Server) GetDeviceAuthorizationRequest(ctx context.Context, req *connect.Request[oidc_pb.GetDeviceAuthorizationRequestRequest]) (*connect.Response[oidc_pb.GetDeviceAuthorizationRequestResponse], error) { + deviceRequest, err := s.query.DeviceAuthRequestByUserCode(ctx, req.Msg.GetUserCode()) if err != nil { return nil, err } @@ -49,7 +50,7 @@ func (s *Server) GetDeviceAuthorizationRequest(ctx context.Context, req *oidc_pb if err != nil { return nil, err } - return &oidc_pb.GetDeviceAuthorizationRequestResponse{ + return connect.NewResponse(&oidc_pb.GetDeviceAuthorizationRequestResponse{ DeviceAuthorizationRequest: &oidc_pb.DeviceAuthorizationRequest{ Id: base64.RawURLEncoding.EncodeToString(encrypted), ClientId: deviceRequest.ClientID, @@ -57,24 +58,24 @@ func (s *Server) GetDeviceAuthorizationRequest(ctx context.Context, req *oidc_pb AppName: deviceRequest.AppName, ProjectName: deviceRequest.ProjectName, }, - }, nil + }), nil } -func (s *Server) AuthorizeOrDenyDeviceAuthorization(ctx context.Context, req *oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest) (*oidc_pb.AuthorizeOrDenyDeviceAuthorizationResponse, error) { - deviceCode, err := s.deviceCodeFromID(req.GetDeviceAuthorizationId()) +func (s *Server) AuthorizeOrDenyDeviceAuthorization(ctx context.Context, req *connect.Request[oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest]) (*connect.Response[oidc_pb.AuthorizeOrDenyDeviceAuthorizationResponse], error) { + deviceCode, err := s.deviceCodeFromID(req.Msg.GetDeviceAuthorizationId()) if err != nil { return nil, err } - switch req.GetDecision().(type) { + switch req.Msg.GetDecision().(type) { case *oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest_Session: - _, err = s.command.ApproveDeviceAuthWithSession(ctx, deviceCode, req.GetSession().GetSessionId(), req.GetSession().GetSessionToken()) + _, err = s.command.ApproveDeviceAuthWithSession(ctx, deviceCode, req.Msg.GetSession().GetSessionId(), req.Msg.GetSession().GetSessionToken()) case *oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest_Deny: _, err = s.command.CancelDeviceAuth(ctx, deviceCode, domain.DeviceAuthCanceledDenied) } if err != nil { return nil, err } - return &oidc_pb.AuthorizeOrDenyDeviceAuthorizationResponse{}, nil + return connect.NewResponse(&oidc_pb.AuthorizeOrDenyDeviceAuthorizationResponse{}), nil } func authRequestToPb(a *query.AuthRequest) *oidc_pb.AuthRequest { @@ -136,7 +137,7 @@ func (s *Server) checkPermission(ctx context.Context, clientID string, userID st return nil } -func (s *Server) failAuthRequest(ctx context.Context, authRequestID string, ae *oidc_pb.AuthorizationError) (*oidc_pb.CreateCallbackResponse, error) { +func (s *Server) failAuthRequest(ctx context.Context, authRequestID string, ae *oidc_pb.AuthorizationError) (*connect.Response[oidc_pb.CreateCallbackResponse], error) { details, aar, err := s.command.FailAuthRequest(ctx, authRequestID, errorReasonToDomain(ae.GetError())) if err != nil { return nil, err @@ -146,13 +147,13 @@ func (s *Server) failAuthRequest(ctx context.Context, authRequestID string, ae * if err != nil { return nil, err } - return &oidc_pb.CreateCallbackResponse{ + return connect.NewResponse(&oidc_pb.CreateCallbackResponse{ Details: object.DomainToDetailsPb(details), CallbackUrl: callback, - }, nil + }), nil } -func (s *Server) linkSessionToAuthRequest(ctx context.Context, authRequestID string, session *oidc_pb.Session) (*oidc_pb.CreateCallbackResponse, error) { +func (s *Server) linkSessionToAuthRequest(ctx context.Context, authRequestID string, session *oidc_pb.Session) (*connect.Response[oidc_pb.CreateCallbackResponse], error) { details, aar, err := s.command.LinkSessionToAuthRequest(ctx, authRequestID, session.GetSessionId(), session.GetSessionToken(), true, s.checkPermission) if err != nil { return nil, err @@ -172,10 +173,10 @@ func (s *Server) linkSessionToAuthRequest(ctx context.Context, authRequestID str if err != nil { return nil, err } - return &oidc_pb.CreateCallbackResponse{ + return connect.NewResponse(&oidc_pb.CreateCallbackResponse{ Details: object.DomainToDetailsPb(details), CallbackUrl: callback, - }, nil + }), nil } func errorReasonToDomain(errorReason oidc_pb.ErrorReason) domain.OIDCErrorReason { diff --git a/internal/api/grpc/oidc/v2/server.go b/internal/api/grpc/oidc/v2/server.go index 99234ee3d7..3d8f78a8ad 100644 --- a/internal/api/grpc/oidc/v2/server.go +++ b/internal/api/grpc/oidc/v2/server.go @@ -1,7 +1,10 @@ package oidc import ( - "google.golang.org/grpc" + "net/http" + + "connectrpc.com/connect" + "google.golang.org/protobuf/reflect/protoreflect" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/grpc/server" @@ -10,12 +13,12 @@ import ( "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/query" oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2" + "github.com/zitadel/zitadel/pkg/grpc/oidc/v2/oidcconnect" ) -var _ oidc_pb.OIDCServiceServer = (*Server)(nil) +var _ oidcconnect.OIDCServiceHandler = (*Server)(nil) type Server struct { - oidc_pb.UnimplementedOIDCServiceServer command *command.Commands query *query.Queries @@ -42,8 +45,12 @@ func CreateServer( } } -func (s *Server) RegisterServer(grpcServer *grpc.Server) { - oidc_pb.RegisterOIDCServiceServer(grpcServer, s) +func (s *Server) RegisterConnectServer(interceptors ...connect.Interceptor) (string, http.Handler) { + return oidcconnect.NewOIDCServiceHandler(s, connect.WithInterceptors(interceptors...)) +} + +func (s *Server) FileDescriptor() protoreflect.FileDescriptor { + return oidc_pb.File_zitadel_oidc_v2_oidc_service_proto } func (s *Server) AppName() string { diff --git a/internal/api/grpc/oidc/v2beta/integration_test/oidc_test.go b/internal/api/grpc/oidc/v2beta/integration_test/oidc_test.go index a8e1aa94d7..303cdd3ad5 100644 --- a/internal/api/grpc/oidc/v2beta/integration_test/oidc_test.go +++ b/internal/api/grpc/oidc/v2beta/integration_test/oidc_test.go @@ -23,8 +23,7 @@ import ( ) func TestServer_GetAuthRequest(t *testing.T) { - project, err := Instance.CreateProject(CTX) - require.NoError(t, err) + project := Instance.CreateProject(CTX, t, "", gofakeit.AppName(), false, false) client, err := Instance.CreateOIDCNativeClient(CTX, redirectURI, logoutRedirectURI, project.GetId(), false) require.NoError(t, err) @@ -97,8 +96,7 @@ func TestServer_GetAuthRequest(t *testing.T) { } func TestServer_CreateCallback(t *testing.T) { - project, err := Instance.CreateProject(CTX) - require.NoError(t, err) + project := Instance.CreateProject(CTX, t, "", gofakeit.AppName(), false, false) client, err := Instance.CreateOIDCNativeClient(CTX, redirectURI, logoutRedirectURI, project.GetId(), false) require.NoError(t, err) clientV2, err := Instance.CreateOIDCClientLoginVersion(CTX, redirectURI, logoutRedirectURI, project.GetId(), app.OIDCAppType_OIDC_APP_TYPE_NATIVE, app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE, false, loginV2) @@ -308,7 +306,7 @@ func TestServer_CreateCallback(t *testing.T) { ctx: CTXLoginClient, req: &oidc_pb.CreateCallbackRequest{ AuthRequestId: func() string { - client, err := Instance.CreateOIDCImplicitFlowClient(CTX, redirectURIImplicit, nil) + client, err := Instance.CreateOIDCImplicitFlowClient(CTX, t, redirectURIImplicit, nil) require.NoError(t, err) authRequestID, err := Instance.CreateOIDCAuthRequestImplicit(CTXLoginClient, client.GetClientId(), Instance.Users.Get(integration.UserTypeLogin).ID, redirectURIImplicit) require.NoError(t, err) @@ -335,7 +333,7 @@ func TestServer_CreateCallback(t *testing.T) { ctx: CTXLoginClient, req: &oidc_pb.CreateCallbackRequest{ AuthRequestId: func() string { - clientV2, err := Instance.CreateOIDCImplicitFlowClient(CTX, redirectURIImplicit, loginV2) + clientV2, err := Instance.CreateOIDCImplicitFlowClient(CTX, t, redirectURIImplicit, loginV2) require.NoError(t, err) authRequestID, err := Instance.CreateOIDCAuthRequestImplicitWithoutLoginClientHeader(CTXLoginClient, clientV2.GetClientId(), redirectURIImplicit) require.NoError(t, err) @@ -391,7 +389,7 @@ func TestServer_CreateCallback_Permission(t *testing.T) { projectID2, _ := createOIDCApplication(ctx, t, true, true) orgResp := Instance.CreateOrganization(ctx, "oidc-permission-"+gofakeit.AppName(), gofakeit.Email()) - Instance.CreateProjectGrant(ctx, projectID2, orgResp.GetOrganizationId()) + Instance.CreateProjectGrant(ctx, t, projectID2, orgResp.GetOrganizationId()) user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone()) Instance.CreateProjectUserGrant(t, ctx, projectID, user.GetUserId()) @@ -406,7 +404,7 @@ func TestServer_CreateCallback_Permission(t *testing.T) { projectID, clientID := createOIDCApplication(ctx, t, true, true) orgResp := Instance.CreateOrganization(ctx, "oidc-permission-"+gofakeit.AppName(), gofakeit.Email()) - Instance.CreateProjectGrant(ctx, projectID, orgResp.GetOrganizationId()) + Instance.CreateProjectGrant(ctx, t, projectID, orgResp.GetOrganizationId()) user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone()) Instance.CreateProjectUserGrant(t, ctx, projectID, user.GetUserId()) @@ -427,9 +425,9 @@ func TestServer_CreateCallback_Permission(t *testing.T) { projectID, clientID := createOIDCApplication(ctx, t, true, true) orgResp := Instance.CreateOrganization(ctx, "oidc-permission-"+gofakeit.AppName(), gofakeit.Email()) - projectGrantResp := Instance.CreateProjectGrant(ctx, projectID, orgResp.GetOrganizationId()) + Instance.CreateProjectGrant(ctx, t, projectID, orgResp.GetOrganizationId()) user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone()) - Instance.CreateProjectGrantUserGrant(ctx, orgResp.GetOrganizationId(), projectID, projectGrantResp.GetGrantId(), user.GetUserId()) + Instance.CreateProjectGrantUserGrant(ctx, orgResp.GetOrganizationId(), projectID, orgResp.GetOrganizationId(), user.GetUserId()) return createSessionAndAuthRequestForCallback(ctx, t, clientID, Instance.Users.Get(integration.UserTypeLogin).ID, user.GetUserId()) }, @@ -564,9 +562,9 @@ func TestServer_CreateCallback_Permission(t *testing.T) { projectID, clientID := createOIDCApplication(ctx, t, true, false) orgResp := Instance.CreateOrganization(ctx, "oidc-permission-"+gofakeit.AppName(), gofakeit.Email()) - projectGrantResp := Instance.CreateProjectGrant(ctx, projectID, orgResp.GetOrganizationId()) + Instance.CreateProjectGrant(ctx, t, projectID, orgResp.GetOrganizationId()) user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone()) - Instance.CreateProjectGrantUserGrant(ctx, orgResp.GetOrganizationId(), projectID, projectGrantResp.GetGrantId(), user.GetUserId()) + Instance.CreateProjectGrantUserGrant(ctx, orgResp.GetOrganizationId(), projectID, orgResp.GetOrganizationId(), user.GetUserId()) return createSessionAndAuthRequestForCallback(ctx, t, clientID, Instance.Users.Get(integration.UserTypeLogin).ID, user.GetUserId()) }, want: &oidc_pb.CreateCallbackResponse{ @@ -584,7 +582,7 @@ func TestServer_CreateCallback_Permission(t *testing.T) { projectID, clientID := createOIDCApplication(ctx, t, true, false) orgResp := Instance.CreateOrganization(ctx, "oidc-permission-"+gofakeit.AppName(), gofakeit.Email()) - Instance.CreateProjectGrant(ctx, projectID, orgResp.GetOrganizationId()) + Instance.CreateProjectGrant(ctx, t, projectID, orgResp.GetOrganizationId()) user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone()) return createSessionAndAuthRequestForCallback(ctx, t, clientID, Instance.Users.Get(integration.UserTypeLogin).ID, user.GetUserId()) }, @@ -626,7 +624,7 @@ func TestServer_CreateCallback_Permission(t *testing.T) { projectID, clientID := createOIDCApplication(ctx, t, false, true) orgResp := Instance.CreateOrganization(ctx, "oidc-permission-"+gofakeit.AppName(), gofakeit.Email()) - Instance.CreateProjectGrant(ctx, projectID, orgResp.GetOrganizationId()) + Instance.CreateProjectGrant(ctx, t, projectID, orgResp.GetOrganizationId()) user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone()) return createSessionAndAuthRequestForCallback(ctx, t, clientID, Instance.Users.Get(integration.UserTypeLogin).ID, user.GetUserId()) @@ -688,8 +686,7 @@ func createSessionAndAuthRequestForCallback(ctx context.Context, t *testing.T, c } func createOIDCApplication(ctx context.Context, t *testing.T, projectRoleCheck, hasProjectCheck bool) (string, string) { - project, err := Instance.CreateProjectWithPermissionCheck(ctx, projectRoleCheck, hasProjectCheck) - require.NoError(t, err) + project := Instance.CreateProject(ctx, t, "", gofakeit.AppName(), projectRoleCheck, hasProjectCheck) clientV2, err := Instance.CreateOIDCClientLoginVersion(ctx, redirectURI, logoutRedirectURI, project.GetId(), app.OIDCAppType_OIDC_APP_TYPE_NATIVE, app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE, false, loginV2) require.NoError(t, err) return project.GetId(), clientV2.GetClientId() diff --git a/internal/api/grpc/oidc/v2beta/oidc.go b/internal/api/grpc/oidc/v2beta/oidc.go index 66c4bee828..432e6f833f 100644 --- a/internal/api/grpc/oidc/v2beta/oidc.go +++ b/internal/api/grpc/oidc/v2beta/oidc.go @@ -3,6 +3,7 @@ package oidc import ( "context" + "connectrpc.com/connect" "github.com/zitadel/logging" "github.com/zitadel/oidc/v3/pkg/op" "google.golang.org/protobuf/types/known/durationpb" @@ -17,15 +18,15 @@ import ( oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta" ) -func (s *Server) GetAuthRequest(ctx context.Context, req *oidc_pb.GetAuthRequestRequest) (*oidc_pb.GetAuthRequestResponse, error) { - authRequest, err := s.query.AuthRequestByID(ctx, true, req.GetAuthRequestId(), true) +func (s *Server) GetAuthRequest(ctx context.Context, req *connect.Request[oidc_pb.GetAuthRequestRequest]) (*connect.Response[oidc_pb.GetAuthRequestResponse], error) { + authRequest, err := s.query.AuthRequestByID(ctx, true, req.Msg.GetAuthRequestId(), true) if err != nil { logging.WithError(err).Error("query authRequest by ID") return nil, err } - return &oidc_pb.GetAuthRequestResponse{ + return connect.NewResponse(&oidc_pb.GetAuthRequestResponse{ AuthRequest: authRequestToPb(authRequest), - }, nil + }), nil } func authRequestToPb(a *query.AuthRequest) *oidc_pb.AuthRequest { @@ -73,18 +74,18 @@ func promptToPb(p domain.Prompt) oidc_pb.Prompt { } } -func (s *Server) CreateCallback(ctx context.Context, req *oidc_pb.CreateCallbackRequest) (*oidc_pb.CreateCallbackResponse, error) { - switch v := req.GetCallbackKind().(type) { +func (s *Server) CreateCallback(ctx context.Context, req *connect.Request[oidc_pb.CreateCallbackRequest]) (*connect.Response[oidc_pb.CreateCallbackResponse], error) { + switch v := req.Msg.GetCallbackKind().(type) { case *oidc_pb.CreateCallbackRequest_Error: - return s.failAuthRequest(ctx, req.GetAuthRequestId(), v.Error) + return s.failAuthRequest(ctx, req.Msg.GetAuthRequestId(), v.Error) case *oidc_pb.CreateCallbackRequest_Session: - return s.linkSessionToAuthRequest(ctx, req.GetAuthRequestId(), v.Session) + return s.linkSessionToAuthRequest(ctx, req.Msg.GetAuthRequestId(), v.Session) default: return nil, zerrors.ThrowUnimplementedf(nil, "OIDCv2-zee7A", "verification oneOf %T in method CreateCallback not implemented", v) } } -func (s *Server) failAuthRequest(ctx context.Context, authRequestID string, ae *oidc_pb.AuthorizationError) (*oidc_pb.CreateCallbackResponse, error) { +func (s *Server) failAuthRequest(ctx context.Context, authRequestID string, ae *oidc_pb.AuthorizationError) (*connect.Response[oidc_pb.CreateCallbackResponse], error) { details, aar, err := s.command.FailAuthRequest(ctx, authRequestID, errorReasonToDomain(ae.GetError())) if err != nil { return nil, err @@ -94,10 +95,10 @@ func (s *Server) failAuthRequest(ctx context.Context, authRequestID string, ae * if err != nil { return nil, err } - return &oidc_pb.CreateCallbackResponse{ + return connect.NewResponse(&oidc_pb.CreateCallbackResponse{ Details: object.DomainToDetailsPb(details), CallbackUrl: callback, - }, nil + }), nil } func (s *Server) checkPermission(ctx context.Context, clientID string, userID string) error { @@ -114,7 +115,7 @@ func (s *Server) checkPermission(ctx context.Context, clientID string, userID st return nil } -func (s *Server) linkSessionToAuthRequest(ctx context.Context, authRequestID string, session *oidc_pb.Session) (*oidc_pb.CreateCallbackResponse, error) { +func (s *Server) linkSessionToAuthRequest(ctx context.Context, authRequestID string, session *oidc_pb.Session) (*connect.Response[oidc_pb.CreateCallbackResponse], error) { details, aar, err := s.command.LinkSessionToAuthRequest(ctx, authRequestID, session.GetSessionId(), session.GetSessionToken(), true, s.checkPermission) if err != nil { return nil, err @@ -130,10 +131,10 @@ func (s *Server) linkSessionToAuthRequest(ctx context.Context, authRequestID str if err != nil { return nil, err } - return &oidc_pb.CreateCallbackResponse{ + return connect.NewResponse(&oidc_pb.CreateCallbackResponse{ Details: object.DomainToDetailsPb(details), CallbackUrl: callback, - }, nil + }), nil } func errorReasonToDomain(errorReason oidc_pb.ErrorReason) domain.OIDCErrorReason { diff --git a/internal/api/grpc/oidc/v2beta/server.go b/internal/api/grpc/oidc/v2beta/server.go index 7595ae927e..5309a5093e 100644 --- a/internal/api/grpc/oidc/v2beta/server.go +++ b/internal/api/grpc/oidc/v2beta/server.go @@ -1,7 +1,10 @@ package oidc import ( - "google.golang.org/grpc" + "net/http" + + "connectrpc.com/connect" + "google.golang.org/protobuf/reflect/protoreflect" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/grpc/server" @@ -9,12 +12,12 @@ import ( "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/query" oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta/oidcconnect" ) -var _ oidc_pb.OIDCServiceServer = (*Server)(nil) +var _ oidcconnect.OIDCServiceHandler = (*Server)(nil) type Server struct { - oidc_pb.UnimplementedOIDCServiceServer command *command.Commands query *query.Queries @@ -38,8 +41,12 @@ func CreateServer( } } -func (s *Server) RegisterServer(grpcServer *grpc.Server) { - oidc_pb.RegisterOIDCServiceServer(grpcServer, s) +func (s *Server) RegisterConnectServer(interceptors ...connect.Interceptor) (string, http.Handler) { + return oidcconnect.NewOIDCServiceHandler(s, connect.WithInterceptors(interceptors...)) +} + +func (s *Server) FileDescriptor() protoreflect.FileDescriptor { + return oidc_pb.File_zitadel_oidc_v2beta_oidc_service_proto } func (s *Server) AppName() string { diff --git a/internal/api/grpc/org/v2/integration_test/org_test.go b/internal/api/grpc/org/v2/integration_test/org_test.go index aa8a718e68..b28bbf5ef2 100644 --- a/internal/api/grpc/org/v2/integration_test/org_test.go +++ b/internal/api/grpc/org/v2/integration_test/org_test.go @@ -81,6 +81,18 @@ func TestServer_AddOrganization(t *testing.T) { }, wantErr: true, }, + { + name: "no admin, custom org ID", + ctx: CTX, + req: &org.AddOrganizationRequest{ + Name: gofakeit.AppName(), + OrgId: gu.Ptr("custom-org-ID"), + }, + want: &org.AddOrganizationResponse{ + OrganizationId: "custom-org-ID", + CreatedAdmins: []*org.AddOrganizationResponse_CreatedAdmin{}, + }, + }, { name: "admin with init with userID passed for Human admin", ctx: CTX, diff --git a/internal/api/grpc/org/v2/integration_test/query_test.go b/internal/api/grpc/org/v2/integration_test/query_test.go index cb7576455c..b2f27dbe62 100644 --- a/internal/api/grpc/org/v2/integration_test/query_test.go +++ b/internal/api/grpc/org/v2/integration_test/query_test.go @@ -38,6 +38,16 @@ func createOrganization(ctx context.Context, name string) orgAttr { } } +func createOrganizationWithCustomOrgID(ctx context.Context, name string, orgID string) orgAttr { + orgResp := Instance.CreateOrganizationWithCustomOrgID(ctx, name, orgID) + orgResp.Details.CreationDate = orgResp.Details.ChangeDate + return orgAttr{ + ID: orgResp.GetOrganizationId(), + Name: name, + Details: orgResp.GetDetails(), + } +} + func TestServer_ListOrganizations(t *testing.T) { type args struct { ctx context.Context @@ -163,6 +173,35 @@ func TestServer_ListOrganizations(t *testing.T) { }, }, }, + { + name: "list org by custom id, ok", + args: args{ + CTX, + &org.ListOrganizationsRequest{}, + func(ctx context.Context, request *org.ListOrganizationsRequest) ([]orgAttr, error) { + orgs := make([]orgAttr, 1) + name := fmt.Sprintf("ListOrgs-%s", gofakeit.AppName()) + orgID := gofakeit.Company() + orgs[0] = createOrganizationWithCustomOrgID(ctx, name, orgID) + request.Queries = []*org.SearchQuery{ + OrganizationIdQuery(orgID), + } + return orgs, nil + }, + }, + want: &org.ListOrganizationsResponse{ + Details: &object.ListDetails{ + TotalResult: 1, + Timestamp: timestamppb.Now(), + }, + SortingColumn: 0, + Result: []*org.Organization{ + { + State: org.OrganizationState_ORGANIZATION_STATE_ACTIVE, + }, + }, + }, + }, { name: "list org by name, ok", args: args{ diff --git a/internal/api/grpc/org/v2/org.go b/internal/api/grpc/org/v2/org.go index 5f21f7403e..42832d147f 100644 --- a/internal/api/grpc/org/v2/org.go +++ b/internal/api/grpc/org/v2/org.go @@ -3,6 +3,8 @@ package org import ( "context" + "connectrpc.com/connect" + "github.com/zitadel/zitadel/internal/api/grpc/object/v2" "github.com/zitadel/zitadel/internal/api/grpc/user/v2" "github.com/zitadel/zitadel/internal/command" @@ -10,8 +12,8 @@ import ( "github.com/zitadel/zitadel/pkg/grpc/org/v2" ) -func (s *Server) AddOrganization(ctx context.Context, request *org.AddOrganizationRequest) (*org.AddOrganizationResponse, error) { - orgSetup, err := addOrganizationRequestToCommand(request) +func (s *Server) AddOrganization(ctx context.Context, request *connect.Request[org.AddOrganizationRequest]) (*connect.Response[org.AddOrganizationResponse], error) { + orgSetup, err := addOrganizationRequestToCommand(request.Msg) if err != nil { return nil, err } @@ -31,6 +33,7 @@ func addOrganizationRequestToCommand(request *org.AddOrganizationRequest) (*comm Name: request.GetName(), CustomDomain: "", Admins: admins, + OrgID: request.GetOrgId(), }, nil } @@ -67,18 +70,21 @@ func addOrganizationRequestAdminToCommand(admin *org.AddOrganizationRequest_Admi } } -func createdOrganizationToPb(createdOrg *command.CreatedOrg) (_ *org.AddOrganizationResponse, err error) { - admins := make([]*org.AddOrganizationResponse_CreatedAdmin, len(createdOrg.CreatedAdmins)) - for i, admin := range createdOrg.CreatedAdmins { - admins[i] = &org.AddOrganizationResponse_CreatedAdmin{ - UserId: admin.ID, - EmailCode: admin.EmailCode, - PhoneCode: admin.PhoneCode, +func createdOrganizationToPb(createdOrg *command.CreatedOrg) (_ *connect.Response[org.AddOrganizationResponse], err error) { + admins := make([]*org.AddOrganizationResponse_CreatedAdmin, 0, len(createdOrg.OrgAdmins)) + for _, admin := range createdOrg.OrgAdmins { + admin, ok := admin.(*command.CreatedOrgAdmin) + if ok { + admins = append(admins, &org.AddOrganizationResponse_CreatedAdmin{ + UserId: admin.GetID(), + EmailCode: admin.EmailCode, + PhoneCode: admin.PhoneCode, + }) } } - return &org.AddOrganizationResponse{ + return connect.NewResponse(&org.AddOrganizationResponse{ Details: object.DomainToDetailsPb(createdOrg.ObjectDetails), OrganizationId: createdOrg.ObjectDetails.ResourceOwner, CreatedAdmins: admins, - }, nil + }), nil } diff --git a/internal/api/grpc/org/v2/org_test.go b/internal/api/grpc/org/v2/org_test.go index b384f858de..564c5597ee 100644 --- a/internal/api/grpc/org/v2/org_test.go +++ b/internal/api/grpc/org/v2/org_test.go @@ -4,6 +4,7 @@ import ( "testing" "time" + "connectrpc.com/connect" "github.com/muhlemmer/gu" "github.com/stretchr/testify/assert" "google.golang.org/protobuf/types/known/timestamppb" @@ -38,6 +39,21 @@ func Test_addOrganizationRequestToCommand(t *testing.T) { }, wantErr: zerrors.ThrowUnimplementedf(nil, "ORGv2-SD2r1", "userType oneOf %T in method AddOrganization not implemented", nil), }, + { + name: "custom org ID", + args: args{ + request: &org.AddOrganizationRequest{ + Name: "custom org ID", + OrgId: gu.Ptr("org-ID"), + }, + }, + want: &command.OrgSetup{ + Name: "custom org ID", + CustomDomain: "", + Admins: []*command.OrgSetupAdmin{}, + OrgID: "org-ID", + }, + }, { name: "user ID", args: args{ @@ -123,7 +139,7 @@ func Test_createdOrganizationToPb(t *testing.T) { tests := []struct { name string args args - want *org.AddOrganizationResponse + want *connect.Response[org.AddOrganizationResponse] wantErr error }{ { @@ -135,8 +151,8 @@ func Test_createdOrganizationToPb(t *testing.T) { EventDate: now, ResourceOwner: "orgID", }, - CreatedAdmins: []*command.CreatedOrgAdmin{ - { + OrgAdmins: []command.OrgAdmin{ + &command.CreatedOrgAdmin{ ID: "id", EmailCode: gu.Ptr("emailCode"), PhoneCode: gu.Ptr("phoneCode"), @@ -144,7 +160,7 @@ func Test_createdOrganizationToPb(t *testing.T) { }, }, }, - want: &org.AddOrganizationResponse{ + want: connect.NewResponse(&org.AddOrganizationResponse{ Details: &object.Details{ Sequence: 1, ChangeDate: timestamppb.New(now), @@ -158,7 +174,7 @@ func Test_createdOrganizationToPb(t *testing.T) { PhoneCode: gu.Ptr("phoneCode"), }, }, - }, + }), }, } for _, tt := range tests { diff --git a/internal/api/grpc/org/v2/query.go b/internal/api/grpc/org/v2/query.go index 27f279d40e..09e2534e8d 100644 --- a/internal/api/grpc/org/v2/query.go +++ b/internal/api/grpc/org/v2/query.go @@ -3,6 +3,8 @@ package org import ( "context" + "connectrpc.com/connect" + "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/grpc/object/v2" "github.com/zitadel/zitadel/internal/domain" @@ -11,36 +13,36 @@ import ( "github.com/zitadel/zitadel/pkg/grpc/org/v2" ) -func (s *Server) ListOrganizations(ctx context.Context, req *org.ListOrganizationsRequest) (*org.ListOrganizationsResponse, error) { +func (s *Server) ListOrganizations(ctx context.Context, req *connect.Request[org.ListOrganizationsRequest]) (*connect.Response[org.ListOrganizationsResponse], error) { queries, err := listOrgRequestToModel(ctx, req) if err != nil { return nil, err } - orgs, err := s.query.SearchOrgs(ctx, queries, s.checkPermission) + orgs, err := s.query.SearchOrgs(ctx, queries.Msg, s.checkPermission) if err != nil { return nil, err } - return &org.ListOrganizationsResponse{ + return connect.NewResponse(&org.ListOrganizationsResponse{ Result: organizationsToPb(orgs.Orgs), Details: object.ToListDetails(orgs.SearchResponse), - }, nil + }), nil } -func listOrgRequestToModel(ctx context.Context, req *org.ListOrganizationsRequest) (*query.OrgSearchQueries, error) { - offset, limit, asc := object.ListQueryToQuery(req.Query) - queries, err := orgQueriesToQuery(ctx, req.Queries) +func listOrgRequestToModel(ctx context.Context, req *connect.Request[org.ListOrganizationsRequest]) (*connect.Response[query.OrgSearchQueries], error) { + offset, limit, asc := object.ListQueryToQuery(req.Msg.Query) + queries, err := orgQueriesToQuery(ctx, req.Msg.Queries) if err != nil { return nil, err } - return &query.OrgSearchQueries{ + return connect.NewResponse(&query.OrgSearchQueries{ SearchRequest: query.SearchRequest{ Offset: offset, Limit: limit, - SortingColumn: fieldNameToOrganizationColumn(req.SortingColumn), + SortingColumn: fieldNameToOrganizationColumn(req.Msg.SortingColumn), Asc: asc, }, Queries: queries, - }, nil + }), nil } func orgQueriesToQuery(ctx context.Context, queries []*org.SearchQuery) (_ []query.SearchQuery, err error) { diff --git a/internal/api/grpc/org/v2/server.go b/internal/api/grpc/org/v2/server.go index 36588f3eb7..6fd318d114 100644 --- a/internal/api/grpc/org/v2/server.go +++ b/internal/api/grpc/org/v2/server.go @@ -1,7 +1,10 @@ package org import ( - "google.golang.org/grpc" + "net/http" + + "connectrpc.com/connect" + "google.golang.org/protobuf/reflect/protoreflect" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/grpc/server" @@ -9,12 +12,12 @@ import ( "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/pkg/grpc/org/v2" + "github.com/zitadel/zitadel/pkg/grpc/org/v2/orgconnect" ) -var _ org.OrganizationServiceServer = (*Server)(nil) +var _ orgconnect.OrganizationServiceHandler = (*Server)(nil) type Server struct { - org.UnimplementedOrganizationServiceServer command *command.Commands query *query.Queries checkPermission domain.PermissionCheck @@ -34,8 +37,12 @@ func CreateServer( } } -func (s *Server) RegisterServer(grpcServer *grpc.Server) { - org.RegisterOrganizationServiceServer(grpcServer, s) +func (s *Server) RegisterConnectServer(interceptors ...connect.Interceptor) (string, http.Handler) { + return orgconnect.NewOrganizationServiceHandler(s, connect.WithInterceptors(interceptors...)) +} + +func (s *Server) FileDescriptor() protoreflect.FileDescriptor { + return org.File_zitadel_org_v2_org_service_proto } func (s *Server) AppName() string { diff --git a/internal/api/grpc/org/v2beta/helper.go b/internal/api/grpc/org/v2beta/helper.go new file mode 100644 index 0000000000..77c3130488 --- /dev/null +++ b/internal/api/grpc/org/v2beta/helper.go @@ -0,0 +1,272 @@ +package org + +import ( + "context" + + "connectrpc.com/connect" + "google.golang.org/protobuf/types/known/timestamppb" + + // TODO fix below + filter "github.com/zitadel/zitadel/internal/api/grpc/filter/v2beta" + metadata "github.com/zitadel/zitadel/internal/api/grpc/metadata/v2beta" + v2beta_object "github.com/zitadel/zitadel/internal/api/grpc/object/v2beta" + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/config/systemdefaults" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore/v1/models" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/zerrors" + v2beta "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" + org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" + v2beta_org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" +) + +// NOTE: most of this code is copied from `internal/api/grpc/admin/*`, as we will eventually axe the previous versons of the API, +// we will have code duplication until then + +func listOrgRequestToModel(systemDefaults systemdefaults.SystemDefaults, request *v2beta_org.ListOrganizationsRequest) (*query.OrgSearchQueries, error) { + offset, limit, asc, err := filter.PaginationPbToQuery(systemDefaults, request.Pagination) + if err != nil { + return nil, err + } + queries, err := OrgQueriesToModel(request.Filter) + if err != nil { + return nil, err + } + return &query.OrgSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + SortingColumn: FieldNameToOrgColumn(request.SortingColumn), + Asc: asc, + }, + Queries: queries, + }, nil +} + +func OrganizationViewToPb(org *query.Org) *v2beta_org.Organization { + return &v2beta_org.Organization{ + Id: org.ID, + State: OrgStateToPb(org.State), + Name: org.Name, + PrimaryDomain: org.Domain, + CreationDate: timestamppb.New(org.CreationDate), + ChangedDate: timestamppb.New(org.ChangeDate), + } +} + +func OrgStateToPb(state domain.OrgState) v2beta_org.OrgState { + switch state { + case domain.OrgStateActive: + return v2beta_org.OrgState_ORG_STATE_ACTIVE + case domain.OrgStateInactive: + return v2beta_org.OrgState_ORG_STATE_INACTIVE + case domain.OrgStateRemoved: + // added to please golangci-lint + return v2beta_org.OrgState_ORG_STATE_REMOVED + case domain.OrgStateUnspecified: + // added to please golangci-lint + return v2beta_org.OrgState_ORG_STATE_UNSPECIFIED + default: + return v2beta_org.OrgState_ORG_STATE_UNSPECIFIED + } +} + +func createdOrganizationToPb(createdOrg *command.CreatedOrg) (_ *connect.Response[org.CreateOrganizationResponse], err error) { + admins := make([]*org.OrganizationAdmin, len(createdOrg.OrgAdmins)) + for i, admin := range createdOrg.OrgAdmins { + switch admin := admin.(type) { + case *command.CreatedOrgAdmin: + admins[i] = &org.OrganizationAdmin{ + OrganizationAdmin: &org.OrganizationAdmin_CreatedAdmin{ + CreatedAdmin: &org.CreatedAdmin{ + UserId: admin.ID, + EmailCode: admin.EmailCode, + PhoneCode: admin.PhoneCode, + }, + }, + } + case *command.AssignedOrgAdmin: + admins[i] = &org.OrganizationAdmin{ + OrganizationAdmin: &org.OrganizationAdmin_AssignedAdmin{ + AssignedAdmin: &org.AssignedAdmin{ + UserId: admin.ID, + }, + }, + } + } + } + return connect.NewResponse(&org.CreateOrganizationResponse{ + CreationDate: timestamppb.New(createdOrg.ObjectDetails.EventDate), + Id: createdOrg.ObjectDetails.ResourceOwner, + OrganizationAdmins: admins, + }), nil +} + +func OrgViewsToPb(orgs []*query.Org) []*v2beta_org.Organization { + o := make([]*v2beta_org.Organization, len(orgs)) + for i, org := range orgs { + o[i] = OrganizationViewToPb(org) + } + return o +} + +func OrgQueriesToModel(queries []*v2beta_org.OrganizationSearchFilter) (_ []query.SearchQuery, err error) { + q := make([]query.SearchQuery, len(queries)) + for i, query := range queries { + q[i], err = OrgQueryToModel(query) + if err != nil { + return nil, err + } + } + return q, nil +} + +func OrgQueryToModel(apiQuery *v2beta_org.OrganizationSearchFilter) (query.SearchQuery, error) { + switch q := apiQuery.Filter.(type) { + case *v2beta_org.OrganizationSearchFilter_DomainFilter: + return query.NewOrgVerifiedDomainSearchQuery(v2beta_object.TextMethodToQuery(q.DomainFilter.Method), q.DomainFilter.Domain) + case *v2beta_org.OrganizationSearchFilter_NameFilter: + return query.NewOrgNameSearchQuery(v2beta_object.TextMethodToQuery(q.NameFilter.Method), q.NameFilter.Name) + case *v2beta_org.OrganizationSearchFilter_StateFilter: + return query.NewOrgStateSearchQuery(OrgStateToDomain(q.StateFilter.State)) + case *v2beta_org.OrganizationSearchFilter_IdFilter: + return query.NewOrgIDSearchQuery(q.IdFilter.Id) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "ORG-vR9nC", "List.Query.Invalid") + } +} + +func OrgStateToDomain(state v2beta_org.OrgState) domain.OrgState { + switch state { + case v2beta_org.OrgState_ORG_STATE_ACTIVE: + return domain.OrgStateActive + case v2beta_org.OrgState_ORG_STATE_INACTIVE: + return domain.OrgStateInactive + case v2beta_org.OrgState_ORG_STATE_REMOVED: + // added to please golangci-lint + return domain.OrgStateRemoved + case v2beta_org.OrgState_ORG_STATE_UNSPECIFIED: + fallthrough + default: + return domain.OrgStateUnspecified + } +} + +func FieldNameToOrgColumn(fieldName v2beta_org.OrgFieldName) query.Column { + switch fieldName { + case v2beta_org.OrgFieldName_ORG_FIELD_NAME_NAME: + return query.OrgColumnName + case v2beta_org.OrgFieldName_ORG_FIELD_NAME_CREATION_DATE: + return query.OrgColumnCreationDate + case v2beta_org.OrgFieldName_ORG_FIELD_NAME_UNSPECIFIED: + return query.Column{} + default: + return query.Column{} + } +} + +func ListOrgDomainsRequestToModel(systemDefaults systemdefaults.SystemDefaults, request *org.ListOrganizationDomainsRequest) (*query.OrgDomainSearchQueries, error) { + offset, limit, asc, err := filter.PaginationPbToQuery(systemDefaults, request.Pagination) + if err != nil { + return nil, err + } + queries, err := DomainQueriesToModel(request.Filters) + if err != nil { + return nil, err + } + return &query.OrgDomainSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + }, + // SortingColumn: //TODO: sorting + Queries: queries, + }, nil +} + +func ListQueryToModel(query *v2beta.ListQuery) (offset, limit uint64, asc bool) { + if query == nil { + return 0, 0, false + } + return query.Offset, uint64(query.Limit), query.Asc +} + +func DomainQueriesToModel(queries []*v2beta_org.DomainSearchFilter) (_ []query.SearchQuery, err error) { + q := make([]query.SearchQuery, len(queries)) + for i, query := range queries { + q[i], err = DomainQueryToModel(query) + if err != nil { + return nil, err + } + } + return q, nil +} + +func DomainQueryToModel(searchQuery *v2beta_org.DomainSearchFilter) (query.SearchQuery, error) { + switch q := searchQuery.Filter.(type) { + case *v2beta_org.DomainSearchFilter_DomainNameFilter: + return query.NewOrgDomainDomainSearchQuery(v2beta_object.TextMethodToQuery(q.DomainNameFilter.Method), q.DomainNameFilter.Name) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "ORG-Ags89", "List.Query.Invalid") + } +} + +func RemoveOrgDomainRequestToDomain(ctx context.Context, req *v2beta_org.DeleteOrganizationDomainRequest) *domain.OrgDomain { + return &domain.OrgDomain{ + ObjectRoot: models.ObjectRoot{ + AggregateID: req.OrganizationId, + }, + Domain: req.Domain, + } +} + +func GenerateOrgDomainValidationRequestToDomain(ctx context.Context, req *v2beta_org.GenerateOrganizationDomainValidationRequest) *domain.OrgDomain { + return &domain.OrgDomain{ + ObjectRoot: models.ObjectRoot{ + AggregateID: req.OrganizationId, + }, + Domain: req.Domain, + ValidationType: v2beta_object.DomainValidationTypeToDomain(req.Type), + } +} + +func ValidateOrgDomainRequestToDomain(ctx context.Context, req *v2beta_org.VerifyOrganizationDomainRequest) *domain.OrgDomain { + return &domain.OrgDomain{ + ObjectRoot: models.ObjectRoot{ + AggregateID: req.OrganizationId, + }, + Domain: req.Domain, + } +} + +func BulkSetOrgMetadataToDomain(req *v2beta_org.SetOrganizationMetadataRequest) []*domain.Metadata { + metadata := make([]*domain.Metadata, len(req.Metadata)) + for i, data := range req.Metadata { + metadata[i] = &domain.Metadata{ + Key: data.Key, + Value: data.Value, + } + } + return metadata +} + +func ListOrgMetadataToDomain(systemDefaults systemdefaults.SystemDefaults, request *v2beta_org.ListOrganizationMetadataRequest) (*query.OrgMetadataSearchQueries, error) { + offset, limit, asc, err := filter.PaginationPbToQuery(systemDefaults, request.Pagination) + if err != nil { + return nil, err + } + queries, err := metadata.OrgMetadataQueriesToQuery(request.Filter) + if err != nil { + return nil, err + } + return &query.OrgMetadataSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + }, + Queries: queries, + }, nil +} diff --git a/internal/api/grpc/org/v2beta/integration_test/org_test.go b/internal/api/grpc/org/v2beta/integration_test/org_test.go index 5998b17a71..a8a507bab3 100644 --- a/internal/api/grpc/org/v2beta/integration_test/org_test.go +++ b/internal/api/grpc/org/v2beta/integration_test/org_test.go @@ -4,7 +4,9 @@ package org_test import ( "context" + "errors" "os" + "strings" "testing" "time" @@ -14,7 +16,9 @@ import ( "github.com/stretchr/testify/require" "github.com/zitadel/zitadel/internal/integration" - org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/admin" + v2beta_object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" + v2beta_org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" "github.com/zitadel/zitadel/pkg/grpc/user/v2" user_v2beta "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" ) @@ -22,7 +26,7 @@ import ( var ( CTX context.Context Instance *integration.Instance - Client org.OrganizationServiceClient + Client v2beta_org.OrganizationServiceClient User *user.AddHumanUserResponse ) @@ -40,20 +44,24 @@ func TestMain(m *testing.M) { }()) } -func TestServer_AddOrganization(t *testing.T) { +func TestServer_CreateOrganization(t *testing.T) { idpResp := Instance.AddGenericOAuthProvider(CTX, Instance.DefaultOrg.Id) - tests := []struct { - name string - ctx context.Context - req *org.AddOrganizationRequest - want *org.AddOrganizationResponse - wantErr bool - }{ + type test struct { + name string + ctx context.Context + req *v2beta_org.CreateOrganizationRequest + id string + testFunc func(ctx context.Context, t *testing.T) + want *v2beta_org.CreateOrganizationResponse + wantErr bool + } + + tests := []test{ { name: "missing permission", - ctx: Instance.WithAuthorization(context.Background(), integration.UserTypeOrgOwner), - req: &org.AddOrganizationRequest{ + ctx: Instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + req: &v2beta_org.CreateOrganizationRequest{ Name: "name", Admins: nil, }, @@ -62,31 +70,73 @@ func TestServer_AddOrganization(t *testing.T) { { name: "empty name", ctx: CTX, - req: &org.AddOrganizationRequest{ + req: &v2beta_org.CreateOrganizationRequest{ Name: "", Admins: nil, }, wantErr: true, }, + func() test { + orgName := gofakeit.Name() + return test{ + name: "adding org with same name twice", + ctx: CTX, + req: &v2beta_org.CreateOrganizationRequest{ + Name: orgName, + Admins: nil, + }, + testFunc: func(ctx context.Context, t *testing.T) { + // create org initially + _, err := Client.CreateOrganization(ctx, &v2beta_org.CreateOrganizationRequest{ + Name: orgName, + }) + require.NoError(t, err) + }, + wantErr: true, + } + }(), { name: "invalid admin type", ctx: CTX, - req: &org.AddOrganizationRequest{ + req: &v2beta_org.CreateOrganizationRequest{ Name: gofakeit.AppName(), - Admins: []*org.AddOrganizationRequest_Admin{ + Admins: []*v2beta_org.CreateOrganizationRequest_Admin{ {}, }, }, wantErr: true, }, + { + name: "existing user as admin", + ctx: CTX, + req: &v2beta_org.CreateOrganizationRequest{ + Name: gofakeit.AppName(), + Admins: []*v2beta_org.CreateOrganizationRequest_Admin{ + { + UserType: &v2beta_org.CreateOrganizationRequest_Admin_UserId{UserId: User.GetUserId()}, + }, + }, + }, + want: &v2beta_org.CreateOrganizationResponse{ + OrganizationAdmins: []*v2beta_org.OrganizationAdmin{ + { + OrganizationAdmin: &v2beta_org.OrganizationAdmin_AssignedAdmin{ + AssignedAdmin: &v2beta_org.AssignedAdmin{ + UserId: User.GetUserId(), + }, + }, + }, + }, + }, + }, { name: "admin with init", ctx: CTX, - req: &org.AddOrganizationRequest{ + req: &v2beta_org.CreateOrganizationRequest{ Name: gofakeit.AppName(), - Admins: []*org.AddOrganizationRequest_Admin{ + Admins: []*v2beta_org.CreateOrganizationRequest_Admin{ { - UserType: &org.AddOrganizationRequest_Admin_Human{ + UserType: &v2beta_org.CreateOrganizationRequest_Admin_Human{ Human: &user_v2beta.AddHumanUserRequest{ Profile: &user_v2beta.SetHumanProfile{ GivenName: "firstname", @@ -103,13 +153,17 @@ func TestServer_AddOrganization(t *testing.T) { }, }, }, - want: &org.AddOrganizationResponse{ - OrganizationId: integration.NotEmpty, - CreatedAdmins: []*org.AddOrganizationResponse_CreatedAdmin{ + want: &v2beta_org.CreateOrganizationResponse{ + Id: integration.NotEmpty, + OrganizationAdmins: []*v2beta_org.OrganizationAdmin{ { - UserId: integration.NotEmpty, - EmailCode: gu.Ptr(integration.NotEmpty), - PhoneCode: nil, + OrganizationAdmin: &v2beta_org.OrganizationAdmin_CreatedAdmin{ + CreatedAdmin: &v2beta_org.CreatedAdmin{ + UserId: integration.NotEmpty, + EmailCode: gu.Ptr(integration.NotEmpty), + PhoneCode: nil, + }, + }, }, }, }, @@ -117,14 +171,14 @@ func TestServer_AddOrganization(t *testing.T) { { name: "existing user and new human with idp", ctx: CTX, - req: &org.AddOrganizationRequest{ + req: &v2beta_org.CreateOrganizationRequest{ Name: gofakeit.AppName(), - Admins: []*org.AddOrganizationRequest_Admin{ + Admins: []*v2beta_org.CreateOrganizationRequest_Admin{ { - UserType: &org.AddOrganizationRequest_Admin_UserId{UserId: User.GetUserId()}, + UserType: &v2beta_org.CreateOrganizationRequest_Admin_UserId{UserId: User.GetUserId()}, }, { - UserType: &org.AddOrganizationRequest_Admin_Human{ + UserType: &v2beta_org.CreateOrganizationRequest_Admin_Human{ Human: &user_v2beta.AddHumanUserRequest{ Profile: &user_v2beta.SetHumanProfile{ GivenName: "firstname", @@ -148,19 +202,151 @@ func TestServer_AddOrganization(t *testing.T) { }, }, }, - want: &org.AddOrganizationResponse{ - CreatedAdmins: []*org.AddOrganizationResponse_CreatedAdmin{ - // a single admin is expected, because the first provided already exists + want: &v2beta_org.CreateOrganizationResponse{ + // OrganizationId: integration.NotEmpty, + OrganizationAdmins: []*v2beta_org.OrganizationAdmin{ { - UserId: integration.NotEmpty, + OrganizationAdmin: &v2beta_org.OrganizationAdmin_AssignedAdmin{ + AssignedAdmin: &v2beta_org.AssignedAdmin{ + UserId: User.GetUserId(), + }, + }, + }, + { + OrganizationAdmin: &v2beta_org.OrganizationAdmin_CreatedAdmin{ + CreatedAdmin: &v2beta_org.CreatedAdmin{ + UserId: integration.NotEmpty, + }, + }, }, }, }, }, + { + name: "create with ID", + ctx: CTX, + id: "custom_id", + req: &v2beta_org.CreateOrganizationRequest{ + Name: gofakeit.AppName(), + Id: gu.Ptr("custom_id"), + }, + want: &v2beta_org.CreateOrganizationResponse{ + Id: "custom_id", + }, + }, + func() test { + orgID := gofakeit.Name() + return test{ + name: "adding org with same ID twice", + ctx: CTX, + req: &v2beta_org.CreateOrganizationRequest{ + Id: &orgID, + Name: gofakeit.Name(), + Admins: nil, + }, + testFunc: func(ctx context.Context, t *testing.T) { + // create org initially + _, err := Client.CreateOrganization(ctx, &v2beta_org.CreateOrganizationRequest{ + Id: &orgID, + Name: gofakeit.Name(), + }) + require.NoError(t, err) + }, + wantErr: true, + } + }(), } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := Client.AddOrganization(tt.ctx, tt.req) + if tt.testFunc != nil { + tt.testFunc(tt.ctx, t) + } + + got, err := Client.CreateOrganization(tt.ctx, tt.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + + if tt.id != "" { + require.Equal(t, tt.id, got.Id) + } + + // check details + gotCD := got.GetCreationDate().AsTime() + now := time.Now() + assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + + // check the admins + require.Equal(t, len(tt.want.GetOrganizationAdmins()), len(got.GetOrganizationAdmins())) + for i, admin := range tt.want.GetOrganizationAdmins() { + gotAdmin := got.GetOrganizationAdmins()[i].OrganizationAdmin + switch admin := admin.OrganizationAdmin.(type) { + case *v2beta_org.OrganizationAdmin_CreatedAdmin: + assertCreatedAdmin(t, admin.CreatedAdmin, gotAdmin.(*v2beta_org.OrganizationAdmin_CreatedAdmin).CreatedAdmin) + case *v2beta_org.OrganizationAdmin_AssignedAdmin: + assert.Equal(t, admin.AssignedAdmin.GetUserId(), gotAdmin.(*v2beta_org.OrganizationAdmin_AssignedAdmin).AssignedAdmin.GetUserId()) + } + } + }) + } +} + +func TestServer_UpdateOrganization(t *testing.T) { + orgs, orgsName, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return + } + orgId := orgs[0].Id + orgName := orgsName[0] + + tests := []struct { + name string + ctx context.Context + req *v2beta_org.UpdateOrganizationRequest + want *v2beta_org.UpdateOrganizationResponse + wantErr bool + }{ + { + name: "update org with new name", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + req: &v2beta_org.UpdateOrganizationRequest{ + Id: orgId, + Name: "new org name", + }, + }, + { + name: "update org with same name", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + req: &v2beta_org.UpdateOrganizationRequest{ + Id: orgId, + Name: orgName, + }, + }, + { + name: "update org with non existent org id", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + req: &v2beta_org.UpdateOrganizationRequest{ + Id: "non existant org id", + // Name: "", + }, + wantErr: true, + }, + { + name: "update org with no id", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + req: &v2beta_org.UpdateOrganizationRequest{ + Id: "", + Name: orgName, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.UpdateOrganization(tt.ctx, tt.req) if tt.wantErr { require.Error(t, err) return @@ -168,26 +354,1742 @@ func TestServer_AddOrganization(t *testing.T) { require.NoError(t, err) // check details - assert.NotZero(t, got.GetDetails().GetSequence()) - gotCD := got.GetDetails().GetChangeDate().AsTime() + gotCD := got.GetChangeDate().AsTime() now := time.Now() assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) - assert.NotEmpty(t, got.GetDetails().GetResourceOwner()) + }) + } +} - // organization id must be the same as the resourceOwner - assert.Equal(t, got.GetDetails().GetResourceOwner(), got.GetOrganizationId()) +func TestServer_ListOrganizations(t *testing.T) { + testStartTimestamp := time.Now() + ListOrgIinstance := integration.NewInstance(CTX) + listOrgIAmOwnerCtx := ListOrgIinstance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + listOrgClient := ListOrgIinstance.Client.OrgV2beta - // check the admins - require.Len(t, got.GetCreatedAdmins(), len(tt.want.GetCreatedAdmins())) - for i, admin := range tt.want.GetCreatedAdmins() { - gotAdmin := got.GetCreatedAdmins()[i] - assertCreatedAdmin(t, admin, gotAdmin) + noOfOrgs := 3 + orgs, orgsName, err := createOrgs(listOrgIAmOwnerCtx, listOrgClient, noOfOrgs) + if err != nil { + require.NoError(t, err) + return + } + + // deactivat org[1] + _, err = listOrgClient.DeactivateOrganization(listOrgIAmOwnerCtx, &v2beta_org.DeactivateOrganizationRequest{ + Id: orgs[1].Id, + }) + require.NoError(t, err) + + tests := []struct { + name string + ctx context.Context + query []*v2beta_org.OrganizationSearchFilter + want []*v2beta_org.Organization + err error + }{ + { + name: "list organizations, without required permissions", + ctx: ListOrgIinstance.WithAuthorization(CTX, integration.UserTypeNoPermission), + err: errors.New("membership not found"), + }, + { + name: "list organizations happy path, no filter", + ctx: listOrgIAmOwnerCtx, + want: []*v2beta_org.Organization{ + { + // default org + Name: "testinstance", + }, + { + Id: orgs[0].Id, + Name: orgsName[0], + }, + { + Id: orgs[1].Id, + Name: orgsName[1], + }, + { + Id: orgs[2].Id, + Name: orgsName[2], + }, + }, + }, + { + name: "list organizations by id happy path", + ctx: listOrgIAmOwnerCtx, + query: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &v2beta_org.OrganizationSearchFilter_IdFilter{ + IdFilter: &v2beta_org.OrgIDFilter{ + Id: orgs[1].Id, + }, + }, + }, + }, + want: []*v2beta_org.Organization{ + { + Id: orgs[1].Id, + Name: orgsName[1], + }, + }, + }, + { + name: "list organizations by state active", + ctx: listOrgIAmOwnerCtx, + query: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &v2beta_org.OrganizationSearchFilter_StateFilter{ + StateFilter: &v2beta_org.OrgStateFilter{ + State: v2beta_org.OrgState_ORG_STATE_ACTIVE, + }, + }, + }, + }, + want: []*v2beta_org.Organization{ + { + // default org + Name: "testinstance", + }, + { + Id: orgs[0].Id, + Name: orgsName[0], + }, + { + Id: orgs[2].Id, + Name: orgsName[2], + }, + }, + }, + { + name: "list organizations by state inactive", + ctx: listOrgIAmOwnerCtx, + query: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &v2beta_org.OrganizationSearchFilter_StateFilter{ + StateFilter: &v2beta_org.OrgStateFilter{ + State: v2beta_org.OrgState_ORG_STATE_INACTIVE, + }, + }, + }, + }, + want: []*v2beta_org.Organization{ + { + Id: orgs[1].Id, + Name: orgsName[1], + }, + }, + }, + { + name: "list organizations by id bad id", + ctx: listOrgIAmOwnerCtx, + query: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &v2beta_org.OrganizationSearchFilter_IdFilter{ + IdFilter: &v2beta_org.OrgIDFilter{ + Id: "bad id", + }, + }, + }, + }, + }, + { + name: "list organizations specify org name equals", + ctx: listOrgIAmOwnerCtx, + query: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &v2beta_org.OrganizationSearchFilter_NameFilter{ + NameFilter: &v2beta_org.OrgNameFilter{ + Name: orgsName[1], + Method: v2beta_object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS, + }, + }, + }, + }, + want: []*v2beta_org.Organization{ + { + Id: orgs[1].Id, + Name: orgsName[1], + }, + }, + }, + { + name: "list organizations specify org name contains", + ctx: listOrgIAmOwnerCtx, + query: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &v2beta_org.OrganizationSearchFilter_NameFilter{ + NameFilter: &v2beta_org.OrgNameFilter{ + Name: func() string { + return orgsName[1][1 : len(orgsName[1])-2] + }(), + Method: v2beta_object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS, + }, + }, + }, + }, + want: []*v2beta_org.Organization{ + { + Id: orgs[1].Id, + Name: orgsName[1], + }, + }, + }, + { + name: "list organizations specify org name contains IGNORE CASE", + ctx: listOrgIAmOwnerCtx, + query: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &v2beta_org.OrganizationSearchFilter_NameFilter{ + NameFilter: &v2beta_org.OrgNameFilter{ + Name: func() string { + return strings.ToUpper(orgsName[1][1 : len(orgsName[1])-2]) + }(), + Method: v2beta_object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS_IGNORE_CASE, + }, + }, + }, + }, + want: []*v2beta_org.Organization{ + { + Id: orgs[1].Id, + Name: orgsName[1], + }, + }, + }, + { + name: "list organizations specify domain name equals", + ctx: listOrgIAmOwnerCtx, + query: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &v2beta_org.OrganizationSearchFilter_DomainFilter{ + DomainFilter: &v2beta_org.OrgDomainFilter{ + Domain: func() string { + listOrgRes, err := listOrgClient.ListOrganizations(listOrgIAmOwnerCtx, &v2beta_org.ListOrganizationsRequest{ + Filter: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &v2beta_org.OrganizationSearchFilter_IdFilter{ + IdFilter: &v2beta_org.OrgIDFilter{ + Id: orgs[1].Id, + }, + }, + }, + }, + }) + require.NoError(t, err) + domain := listOrgRes.Organizations[0].PrimaryDomain + return domain + }(), + Method: v2beta_object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS, + }, + }, + }, + }, + want: []*v2beta_org.Organization{ + { + Id: orgs[1].Id, + Name: orgsName[1], + }, + }, + }, + { + name: "list organizations specify domain name contains", + ctx: listOrgIAmOwnerCtx, + query: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &v2beta_org.OrganizationSearchFilter_DomainFilter{ + DomainFilter: &v2beta_org.OrgDomainFilter{ + Domain: func() string { + domain := strings.ToLower(strings.ReplaceAll(orgsName[1][1:len(orgsName[1])-2], " ", "-")) + return domain + }(), + Method: v2beta_object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS, + }, + }, + }, + }, + want: []*v2beta_org.Organization{ + { + Id: orgs[1].Id, + Name: orgsName[1], + }, + }, + }, + { + name: "list organizations specify org name contains IGNORE CASE", + ctx: listOrgIAmOwnerCtx, + query: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &v2beta_org.OrganizationSearchFilter_DomainFilter{ + DomainFilter: &v2beta_org.OrgDomainFilter{ + Domain: func() string { + domain := strings.ToUpper(strings.ReplaceAll(orgsName[1][1:len(orgsName[1])-2], " ", "-")) + return domain + }(), + Method: v2beta_object.TextQueryMethod_TEXT_QUERY_METHOD_CONTAINS_IGNORE_CASE, + }, + }, + }, + }, + want: []*v2beta_org.Organization{ + { + Id: orgs[1].Id, + Name: orgsName[1], + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 10*time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, err := listOrgClient.ListOrganizations(tt.ctx, &v2beta_org.ListOrganizationsRequest{ + Filter: tt.query, + }) + if tt.err != nil { + require.ErrorContains(t, err, tt.err.Error()) + return + } + require.NoError(ttt, err) + + require.Equal(ttt, uint64(len(tt.want)), got.Pagination.GetTotalResult()) + + foundOrgs := 0 + for _, got := range got.Organizations { + for _, org := range tt.want { + + // created/chagned date + gotCD := got.GetCreationDate().AsTime() + now := time.Now() + assert.WithinRange(ttt, gotCD, testStartTimestamp, now.Add(time.Minute)) + gotCD = got.GetChangedDate().AsTime() + assert.WithinRange(ttt, gotCD, testStartTimestamp, now.Add(time.Minute)) + + // default org + if org.Name == got.Name && got.Name == "testinstance" { + foundOrgs += 1 + continue + } + + if org.Name == got.Name && + org.Id == got.Id { + foundOrgs += 1 + } + } + } + require.Equal(ttt, len(tt.want), foundOrgs) + }, retryDuration, tick, "timeout waiting for expected organizations being created") + }) + } +} + +func TestServer_DeleteOrganization(t *testing.T) { + tests := []struct { + name string + ctx context.Context + createOrgFunc func() string + req *v2beta_org.DeleteOrganizationRequest + want *v2beta_org.DeleteOrganizationResponse + dontCheckTime bool + err error + }{ + { + name: "delete org no permission", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + createOrgFunc: func() string { + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return "" + } + return orgs[0].Id + }, + req: &v2beta_org.DeleteOrganizationRequest{}, + err: errors.New("membership not found"), + }, + { + name: "delete org happy path", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + createOrgFunc: func() string { + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return "" + } + return orgs[0].Id + }, + req: &v2beta_org.DeleteOrganizationRequest{}, + }, + { + name: "delete already deleted org", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + createOrgFunc: func() string { + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return "" + } + // delete org + _, err = Client.DeleteOrganization(CTX, &v2beta_org.DeleteOrganizationRequest{Id: orgs[0].Id}) + require.NoError(t, err) + + return orgs[0].Id + }, + req: &v2beta_org.DeleteOrganizationRequest{}, + dontCheckTime: true, + }, + { + name: "delete non existent org", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + req: &v2beta_org.DeleteOrganizationRequest{ + Id: "non existent org id", + }, + dontCheckTime: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.createOrgFunc != nil { + tt.req.Id = tt.createOrgFunc() + } + + got, err := Client.DeleteOrganization(tt.ctx, tt.req) + if tt.err != nil { + require.Contains(t, err.Error(), tt.err.Error()) + return + } + require.NoError(t, err) + + // check details + gotCD := got.GetDeletionDate().AsTime() + if !tt.dontCheckTime { + now := time.Now() + assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) } }) } } -func assertCreatedAdmin(t *testing.T, expected, got *org.AddOrganizationResponse_CreatedAdmin) { +func TestServer_DeactivateReactivateNonExistentOrganization(t *testing.T) { + ctx := Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + + // deactivate non existent organization + _, err := Client.DeactivateOrganization(ctx, &v2beta_org.DeactivateOrganizationRequest{ + Id: "non existent organization", + }) + require.Contains(t, err.Error(), "Organisation not found") + + // reactivate non existent organization + _, err = Client.ActivateOrganization(ctx, &v2beta_org.ActivateOrganizationRequest{ + Id: "non existent organization", + }) + require.Contains(t, err.Error(), "Organisation not found") +} + +func TestServer_ActivateOrganization(t *testing.T) { + tests := []struct { + name string + ctx context.Context + testFunc func() string + err error + }{ + { + name: "Activate, happy path", + ctx: CTX, + testFunc: func() string { + // 1. create organization + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create orgs") + return "" + } + orgId := orgs[0].Id + + // 2. deactivate organization once + deactivate_res, err := Client.DeactivateOrganization(CTX, &v2beta_org.DeactivateOrganizationRequest{ + Id: orgId, + }) + require.NoError(t, err) + gotCD := deactivate_res.GetChangeDate().AsTime() + now := time.Now() + assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + + // 3. check organization state is deactivated + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 10*time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + listOrgRes, err := Client.ListOrganizations(CTX, &v2beta_org.ListOrganizationsRequest{ + Filter: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &v2beta_org.OrganizationSearchFilter_IdFilter{ + IdFilter: &v2beta_org.OrgIDFilter{ + Id: orgId, + }, + }, + }, + }, + }) + require.NoError(ttt, err) + require.Equal(ttt, v2beta_org.OrgState_ORG_STATE_INACTIVE, listOrgRes.Organizations[0].State) + }, retryDuration, tick, "timeout waiting for expected organizations being created") + + return orgId + }, + }, + { + name: "Activate, no permission", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + testFunc: func() string { + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create orgs") + return "" + } + orgId := orgs[0].Id + return orgId + }, + // BUG: this needs changing + err: errors.New("membership not found"), + }, + { + name: "Activate, not existing", + ctx: CTX, + testFunc: func() string { + return "non-existing-org-id" + }, + err: errors.New("Organisation not found"), + }, + { + name: "Activate, already activated", + ctx: CTX, + testFunc: func() string { + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create orgs") + return "" + } + orgId := orgs[0].Id + return orgId + }, + err: errors.New("Organisation is already active"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var orgId string + if tt.testFunc != nil { + orgId = tt.testFunc() + } + _, err := Client.ActivateOrganization(tt.ctx, &v2beta_org.ActivateOrganizationRequest{ + Id: orgId, + }) + if tt.err != nil { + require.Contains(t, err.Error(), tt.err.Error()) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestServer_DeactivateOrganization(t *testing.T) { + tests := []struct { + name string + ctx context.Context + testFunc func() string + err error + }{ + { + name: "Deactivate, happy path", + ctx: CTX, + testFunc: func() string { + // 1. create organization + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create orgs") + return "" + } + orgId := orgs[0].Id + + return orgId + }, + }, + { + name: "Deactivate, no permission", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + testFunc: func() string { + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create orgs") + return "" + } + orgId := orgs[0].Id + return orgId + }, + // BUG: this needs changing + err: errors.New("membership not found"), + }, + { + name: "Deactivate, not existing", + ctx: CTX, + testFunc: func() string { + return "non-existing-org-id" + }, + err: errors.New("Organisation not found"), + }, + { + name: "Deactivate, already deactivated", + ctx: CTX, + testFunc: func() string { + // 1. create organization + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create orgs") + return "" + } + orgId := orgs[0].Id + + // 2. deactivate organization once + deactivate_res, err := Client.DeactivateOrganization(CTX, &v2beta_org.DeactivateOrganizationRequest{ + Id: orgId, + }) + require.NoError(t, err) + gotCD := deactivate_res.GetChangeDate().AsTime() + now := time.Now() + assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + + // 3. check organization state is deactivated + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 10*time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + listOrgRes, err := Client.ListOrganizations(CTX, &v2beta_org.ListOrganizationsRequest{ + Filter: []*v2beta_org.OrganizationSearchFilter{ + { + Filter: &v2beta_org.OrganizationSearchFilter_IdFilter{ + IdFilter: &v2beta_org.OrgIDFilter{ + Id: orgId, + }, + }, + }, + }, + }) + require.NoError(ttt, err) + require.Equal(ttt, v2beta_org.OrgState_ORG_STATE_INACTIVE, listOrgRes.Organizations[0].State) + }, retryDuration, tick, "timeout waiting for expected organizations being created") + + return orgId + }, + err: errors.New("Organisation is already deactivated"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var orgId string + orgId = tt.testFunc() + _, err := Client.DeactivateOrganization(tt.ctx, &v2beta_org.DeactivateOrganizationRequest{ + Id: orgId, + }) + if tt.err != nil { + require.Contains(t, err.Error(), tt.err.Error()) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestServer_AddOrganizationDomain(t *testing.T) { + tests := []struct { + name string + ctx context.Context + domain string + testFunc func() string + err error + }{ + { + name: "add org domain, happy path", + domain: gofakeit.URL(), + testFunc: func() string { + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return "" + } + orgId := orgs[0].Id + return orgId + }, + }, + { + name: "add org domain, twice", + domain: gofakeit.URL(), + testFunc: func() string { + // 1. create organization + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return "" + } + orgId := orgs[0].Id + + domain := gofakeit.URL() + // 2. add domain + addOrgDomainRes, err := Client.AddOrganizationDomain(CTX, &v2beta_org.AddOrganizationDomainRequest{ + OrganizationId: orgId, + Domain: domain, + }) + require.NoError(t, err) + // check details + gotCD := addOrgDomainRes.GetCreationDate().AsTime() + now := time.Now() + assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + + // check domain added + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 10*time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + queryRes, err := Client.ListOrganizationDomains(CTX, &v2beta_org.ListOrganizationDomainsRequest{ + OrganizationId: orgId, + }) + require.NoError(t, err) + found := false + for _, res := range queryRes.Domains { + if res.DomainName == domain { + found = true + } + } + require.True(t, found, "unable to find added domain") + }, retryDuration, tick, "timeout waiting for expected organizations being created") + + return orgId + }, + }, + { + name: "add org domain to non existent org", + domain: gofakeit.URL(), + testFunc: func() string { + return "non-existing-org-id" + }, + // BUG: should return a error + err: nil, + }, + } + + for _, tt := range tests { + var orgId string + t.Run(tt.name, func(t *testing.T) { + orgId = tt.testFunc() + }) + addOrgDomainRes, err := Client.AddOrganizationDomain(CTX, &v2beta_org.AddOrganizationDomainRequest{ + OrganizationId: orgId, + Domain: tt.domain, + }) + if tt.err != nil { + require.Contains(t, err.Error(), tt.err.Error()) + } else { + require.NoError(t, err) + // check details + gotCD := addOrgDomainRes.GetCreationDate().AsTime() + now := time.Now() + assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + } + } +} + +func TestServer_AddOrganizationDomain_ClaimDomain(t *testing.T) { + domain := gofakeit.DomainName() + + // create an organization, ensure it has globally unique usernames + // and create a user with a loginname that matches the domain later on + organization, err := Client.CreateOrganization(CTX, &v2beta_org.CreateOrganizationRequest{ + Name: gofakeit.AppName(), + }) + require.NoError(t, err) + _, err = Instance.Client.Admin.AddCustomDomainPolicy(CTX, &admin.AddCustomDomainPolicyRequest{ + OrgId: organization.GetId(), + UserLoginMustBeDomain: false, + }) + require.NoError(t, err) + username := gofakeit.Username() + "@" + domain + ownUser := Instance.CreateHumanUserVerified(CTX, organization.GetId(), username, "") + + // create another organization, ensure it has globally unique usernames + // and create a user with a loginname that matches the domain later on + otherOrg, err := Client.CreateOrganization(CTX, &v2beta_org.CreateOrganizationRequest{ + Name: gofakeit.AppName(), + }) + require.NoError(t, err) + _, err = Instance.Client.Admin.AddCustomDomainPolicy(CTX, &admin.AddCustomDomainPolicyRequest{ + OrgId: otherOrg.GetId(), + UserLoginMustBeDomain: false, + }) + require.NoError(t, err) + + otherUsername := gofakeit.Username() + "@" + domain + otherUser := Instance.CreateHumanUserVerified(CTX, otherOrg.GetId(), otherUsername, "") + + // if we add the domain now to the first organization, it should be claimed on the second organization, resp. its user(s) + _, err = Client.AddOrganizationDomain(CTX, &v2beta_org.AddOrganizationDomainRequest{ + OrganizationId: organization.GetId(), + Domain: domain, + }) + require.NoError(t, err) + + // check both users: the first one must be untouched, the second one must be updated + users, err := Instance.Client.UserV2.ListUsers(CTX, &user.ListUsersRequest{ + Queries: []*user.SearchQuery{ + { + Query: &user.SearchQuery_InUserIdsQuery{ + InUserIdsQuery: &user.InUserIDQuery{UserIds: []string{ownUser.GetUserId(), otherUser.GetUserId()}}, + }, + }, + }, + }) + require.NoError(t, err) + require.Len(t, users.GetResult(), 2) + + for _, u := range users.GetResult() { + if u.GetUserId() == ownUser.GetUserId() { + assert.Equal(t, username, u.GetPreferredLoginName()) + continue + } + if u.GetUserId() == otherUser.GetUserId() { + assert.NotEqual(t, otherUsername, u.GetPreferredLoginName()) + assert.Contains(t, u.GetPreferredLoginName(), "@temporary.") + } + } +} + +func TestServer_ListOrganizationDomains(t *testing.T) { + domain := gofakeit.URL() + tests := []struct { + name string + ctx context.Context + domain string + testFunc func() string + err error + }{ + { + name: "list org domain, happy path", + domain: domain, + testFunc: func() string { + // 1. create organization + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return "" + } + orgId := orgs[0].Id + // 2. add domain + addOrgDomainRes, err := Client.AddOrganizationDomain(CTX, &v2beta_org.AddOrganizationDomainRequest{ + OrganizationId: orgId, + Domain: domain, + }) + require.NoError(t, err) + // check details + gotCD := addOrgDomainRes.GetCreationDate().AsTime() + now := time.Now() + assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + + return orgId + }, + }, + } + + for _, tt := range tests { + var orgId string + t.Run(tt.name, func(t *testing.T) { + orgId = tt.testFunc() + }) + + var err error + var queryRes *v2beta_org.ListOrganizationDomainsResponse + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 10*time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + queryRes, err = Client.ListOrganizationDomains(CTX, &v2beta_org.ListOrganizationDomainsRequest{ + OrganizationId: orgId, + }) + require.NoError(t, err) + found := false + for _, res := range queryRes.Domains { + if res.DomainName == tt.domain { + found = true + } + } + require.True(t, found, "unable to find added domain") + }, retryDuration, tick, "timeout waiting for adding domain") + + } +} + +func TestServer_DeleteOerganizationDomain(t *testing.T) { + domain := gofakeit.URL() + tests := []struct { + name string + ctx context.Context + domain string + testFunc func() string + err error + }{ + { + name: "delete org domain, happy path", + domain: domain, + testFunc: func() string { + // 1. create organization + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return "" + } + orgId := orgs[0].Id + + // 2. add domain + addOrgDomainRes, err := Client.AddOrganizationDomain(CTX, &v2beta_org.AddOrganizationDomainRequest{ + OrganizationId: orgId, + Domain: domain, + }) + require.NoError(t, err) + // check details + gotCD := addOrgDomainRes.GetCreationDate().AsTime() + now := time.Now() + assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + + // check domain added + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 10*time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + queryRes, err := Client.ListOrganizationDomains(CTX, &v2beta_org.ListOrganizationDomainsRequest{ + OrganizationId: orgId, + }) + require.NoError(t, err) + found := false + for _, res := range queryRes.Domains { + if res.DomainName == domain { + found = true + } + } + require.True(t, found, "unable to find added domain") + }, retryDuration, tick, "timeout waiting for expected organizations being created") + + return orgId + }, + }, + { + name: "delete org domain, twice", + domain: gofakeit.URL(), + testFunc: func() string { + // 1. create organization + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return "" + } + orgId := orgs[0].Id + + domain := gofakeit.URL() + // 2. add domain + addOrgDomainRes, err := Client.AddOrganizationDomain(CTX, &v2beta_org.AddOrganizationDomainRequest{ + OrganizationId: orgId, + Domain: domain, + }) + require.NoError(t, err) + // check details + gotCD := addOrgDomainRes.GetCreationDate().AsTime() + now := time.Now() + assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + + // check domain added + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 10*time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + queryRes, err := Client.ListOrganizationDomains(CTX, &v2beta_org.ListOrganizationDomainsRequest{ + OrganizationId: orgId, + }) + require.NoError(t, err) + found := false + for _, res := range queryRes.Domains { + if res.DomainName == domain { + found = true + } + } + require.True(t, found, "unable to find added domain") + }, retryDuration, tick, "timeout waiting for expected organizations being created") + + _, err = Client.DeleteOrganizationDomain(CTX, &v2beta_org.DeleteOrganizationDomainRequest{ + OrganizationId: orgId, + Domain: domain, + }) + require.NoError(t, err) + + return orgId + }, + err: errors.New("Domain doesn't exist on organization"), + }, + { + name: "delete org domain to non existent org", + domain: gofakeit.URL(), + testFunc: func() string { + return "non-existing-org-id" + }, + // BUG: + err: errors.New("Domain doesn't exist on organization"), + }, + } + + for _, tt := range tests { + var orgId string + t.Run(tt.name, func(t *testing.T) { + orgId = tt.testFunc() + }) + + _, err := Client.DeleteOrganizationDomain(CTX, &v2beta_org.DeleteOrganizationDomainRequest{ + OrganizationId: orgId, + Domain: tt.domain, + }) + + if tt.err != nil { + require.Contains(t, err.Error(), tt.err.Error()) + } else { + require.NoError(t, err) + } + } +} + +func TestServer_AddListDeleteOrganizationDomain(t *testing.T) { + tests := []struct { + name string + testFunc func() + }{ + { + name: "add org domain, re-add org domain", + testFunc: func() { + // 1. create organization + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return + } + orgId := orgs[0].Id + // ctx := Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + + domain := gofakeit.URL() + // 2. add domain + addOrgDomainRes, err := Client.AddOrganizationDomain(CTX, &v2beta_org.AddOrganizationDomainRequest{ + OrganizationId: orgId, + Domain: domain, + }) + require.NoError(t, err) + // check details + gotCD := addOrgDomainRes.GetCreationDate().AsTime() + now := time.Now() + assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + + // 3. re-add domain + _, err = Client.AddOrganizationDomain(CTX, &v2beta_org.AddOrganizationDomainRequest{ + OrganizationId: orgId, + Domain: domain, + }) + // TODO remove error for adding already existing domain + // require.NoError(t, err) + require.Contains(t, err.Error(), "Errors.Already.Exists") + // check details + // gotCD = addOrgDomainRes.GetDetails().GetChangeDate().AsTime() + // now = time.Now() + // assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + + // 4. check domain is added + queryRes, err := Client.ListOrganizationDomains(CTX, &v2beta_org.ListOrganizationDomainsRequest{ + OrganizationId: orgId, + }) + require.NoError(t, err) + found := false + for _, res := range queryRes.Domains { + if res.DomainName == domain { + found = true + } + } + require.True(t, found, "unable to find added domain") + }, + }, + { + name: "add org domain, delete org domain, re-delete org domain", + testFunc: func() { + // 1. create organization + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return + } + orgId := orgs[0].Id + + domain := gofakeit.URL() + // 2. add domain + addOrgDomainRes, err := Client.AddOrganizationDomain(CTX, &v2beta_org.AddOrganizationDomainRequest{ + OrganizationId: orgId, + Domain: domain, + }) + require.NoError(t, err) + // check details + gotCD := addOrgDomainRes.GetCreationDate().AsTime() + now := time.Now() + assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + + // 2. delete organisation domain + deleteOrgDomainRes, err := Client.DeleteOrganizationDomain(CTX, &v2beta_org.DeleteOrganizationDomainRequest{ + OrganizationId: orgId, + Domain: domain, + }) + require.NoError(t, err) + // check details + gotCD = deleteOrgDomainRes.GetDeletionDate().AsTime() + now = time.Now() + assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 10*time.Minute) + require.EventuallyWithT(t, func(t *assert.CollectT) { + // 3. check organization domain deleted + queryRes, err := Client.ListOrganizationDomains(CTX, &v2beta_org.ListOrganizationDomainsRequest{ + OrganizationId: orgId, + }) + require.NoError(t, err) + found := false + for _, res := range queryRes.Domains { + if res.DomainName == domain { + found = true + } + } + require.False(t, found, "deleted domain found") + }, retryDuration, tick, "timeout waiting for expected organizations being created") + + // 4. redelete organisation domain + _, err = Client.DeleteOrganizationDomain(CTX, &v2beta_org.DeleteOrganizationDomainRequest{ + OrganizationId: orgId, + Domain: domain, + }) + // TODO remove error for deleting org domain already deleted + // require.NoError(t, err) + require.Contains(t, err.Error(), "Domain doesn't exist on organization") + // check details + // gotCD = deleteOrgDomainRes.GetDetails().GetChangeDate().AsTime() + // now = time.Now() + // assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + + // 5. check organization domain deleted + queryRes, err := Client.ListOrganizationDomains(CTX, &v2beta_org.ListOrganizationDomainsRequest{ + OrganizationId: orgId, + }) + require.NoError(t, err) + found := false + for _, res := range queryRes.Domains { + if res.DomainName == domain { + found = true + } + } + require.False(t, found, "deleted domain found") + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.testFunc() + }) + } +} + +func TestServer_ValidateOrganizationDomain(t *testing.T) { + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return + } + orgId := orgs[0].Id + + _, err = Instance.Client.Admin.UpdateDomainPolicy(CTX, &admin.UpdateDomainPolicyRequest{ + ValidateOrgDomains: true, + }) + if err != nil && !strings.Contains(err.Error(), "Organisation is already deactivated") { + require.NoError(t, err) + } + + domain := gofakeit.URL() + _, err = Client.AddOrganizationDomain(CTX, &v2beta_org.AddOrganizationDomainRequest{ + OrganizationId: orgId, + Domain: domain, + }) + require.NoError(t, err) + + tests := []struct { + name string + ctx context.Context + req *v2beta_org.GenerateOrganizationDomainValidationRequest + err error + }{ + { + name: "validate org http happy path", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + req: &v2beta_org.GenerateOrganizationDomainValidationRequest{ + OrganizationId: orgId, + Domain: domain, + Type: v2beta_org.DomainValidationType_DOMAIN_VALIDATION_TYPE_HTTP, + }, + }, + { + name: "validate org http non existnetn org id", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + req: &v2beta_org.GenerateOrganizationDomainValidationRequest{ + OrganizationId: "non existent org id", + Domain: domain, + Type: v2beta_org.DomainValidationType_DOMAIN_VALIDATION_TYPE_HTTP, + }, + // BUG: this should be 'organization does not exist' + err: errors.New("Domain doesn't exist on organization"), + }, + { + name: "validate org dns happy path", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + req: &v2beta_org.GenerateOrganizationDomainValidationRequest{ + OrganizationId: orgId, + Domain: domain, + Type: v2beta_org.DomainValidationType_DOMAIN_VALIDATION_TYPE_DNS, + }, + }, + { + name: "validate org dns non existnetn org id", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + req: &v2beta_org.GenerateOrganizationDomainValidationRequest{ + OrganizationId: "non existent org id", + Domain: domain, + Type: v2beta_org.DomainValidationType_DOMAIN_VALIDATION_TYPE_DNS, + }, + // BUG: this should be 'organization does not exist' + err: errors.New("Domain doesn't exist on organization"), + }, + { + name: "validate org non existnetn domain", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + req: &v2beta_org.GenerateOrganizationDomainValidationRequest{ + OrganizationId: orgId, + Domain: "non existent domain", + Type: v2beta_org.DomainValidationType_DOMAIN_VALIDATION_TYPE_HTTP, + }, + err: errors.New("Domain doesn't exist on organization"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.GenerateOrganizationDomainValidation(tt.ctx, tt.req) + if tt.err != nil { + require.Contains(t, err.Error(), tt.err.Error()) + return + } + require.NoError(t, err) + + require.NotEmpty(t, got.Token) + require.Contains(t, got.Url, domain) + }) + } +} + +func TestServer_SetOrganizationMetadata(t *testing.T) { + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return + } + orgId := orgs[0].Id + + tests := []struct { + name string + ctx context.Context + setupFunc func() + orgId string + key string + value string + err error + }{ + { + name: "set org metadata", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + orgId: orgId, + key: "key1", + value: "value1", + }, + { + name: "set org metadata on non existant org", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + orgId: "non existant orgid", + key: "key2", + value: "value2", + err: errors.New("Organisation not found"), + }, + { + name: "update org metadata", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + setupFunc: func() { + _, err := Client.SetOrganizationMetadata(CTX, &v2beta_org.SetOrganizationMetadataRequest{ + OrganizationId: orgId, + Metadata: []*v2beta_org.Metadata{ + { + Key: "key3", + Value: []byte("value3"), + }, + }, + }) + require.NoError(t, err) + }, + orgId: orgId, + key: "key4", + value: "value4", + }, + { + name: "update org metadata with same value", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + setupFunc: func() { + _, err := Client.SetOrganizationMetadata(CTX, &v2beta_org.SetOrganizationMetadataRequest{ + OrganizationId: orgId, + Metadata: []*v2beta_org.Metadata{ + { + Key: "key5", + Value: []byte("value5"), + }, + }, + }) + require.NoError(t, err) + }, + orgId: orgId, + key: "key5", + value: "value5", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.setupFunc != nil { + tt.setupFunc() + } + got, err := Client.SetOrganizationMetadata(tt.ctx, &v2beta_org.SetOrganizationMetadataRequest{ + OrganizationId: tt.orgId, + Metadata: []*v2beta_org.Metadata{ + { + Key: tt.key, + Value: []byte(tt.value), + }, + }, + }) + if tt.err != nil { + require.Contains(t, err.Error(), tt.err.Error()) + return + } + require.NoError(t, err) + + // check details + gotCD := got.GetSetDate().AsTime() + now := time.Now() + assert.WithinRange(t, gotCD, now.Add(-time.Minute), now.Add(time.Minute)) + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 10*time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + // check metadata + listMetadataRes, err := Client.ListOrganizationMetadata(tt.ctx, &v2beta_org.ListOrganizationMetadataRequest{ + OrganizationId: orgId, + }) + require.NoError(t, err) + foundMetadata := false + foundMetadataKeyCount := 0 + for _, res := range listMetadataRes.Metadata { + if res.Key == tt.key { + foundMetadataKeyCount += 1 + } + if res.Key == tt.key && + string(res.Value) == tt.value { + foundMetadata = true + } + } + require.True(ttt, foundMetadata, "unable to find added metadata") + require.Equal(ttt, 1, foundMetadataKeyCount, "same metadata key found multiple times") + }, retryDuration, tick, "timeout waiting for expected organizations being created") + }) + } +} + +func TestServer_ListOrganizationMetadata(t *testing.T) { + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return + } + orgId := orgs[0].Id + + tests := []struct { + name string + ctx context.Context + setupFunc func() + orgId string + keyValuPars []struct { + key string + value string + } + }{ + { + name: "list org metadata happy path", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + setupFunc: func() { + _, err := Client.SetOrganizationMetadata(CTX, &v2beta_org.SetOrganizationMetadataRequest{ + OrganizationId: orgId, + Metadata: []*v2beta_org.Metadata{ + { + Key: "key1", + Value: []byte("value1"), + }, + }, + }) + require.NoError(t, err) + }, + orgId: orgId, + keyValuPars: []struct{ key, value string }{ + { + key: "key1", + value: "value1", + }, + }, + }, + { + name: "list multiple org metadata happy path", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + setupFunc: func() { + _, err := Client.SetOrganizationMetadata(CTX, &v2beta_org.SetOrganizationMetadataRequest{ + OrganizationId: orgId, + Metadata: []*v2beta_org.Metadata{ + { + Key: "key2", + Value: []byte("value2"), + }, + { + Key: "key3", + Value: []byte("value3"), + }, + { + Key: "key4", + Value: []byte("value4"), + }, + }, + }) + require.NoError(t, err) + }, + orgId: orgId, + keyValuPars: []struct{ key, value string }{ + { + key: "key2", + value: "value2", + }, + { + key: "key3", + value: "value3", + }, + { + key: "key4", + value: "value4", + }, + }, + }, + { + name: "list org metadata for non existent org", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + orgId: "non existent orgid", + keyValuPars: []struct{ key, value string }{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.setupFunc != nil { + tt.setupFunc() + } + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 10*time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, err := Client.ListOrganizationMetadata(tt.ctx, &v2beta_org.ListOrganizationMetadataRequest{ + OrganizationId: tt.orgId, + }) + require.NoError(t, err) + + foundMetadataCount := 0 + for _, kv := range tt.keyValuPars { + for _, res := range got.Metadata { + if res.Key == kv.key && + string(res.Value) == kv.value { + foundMetadataCount += 1 + } + } + } + require.Equal(t, len(tt.keyValuPars), foundMetadataCount) + }, retryDuration, tick, "timeout waiting for expected organizations being created") + }) + } +} + +func TestServer_DeleteOrganizationMetadata(t *testing.T) { + orgs, _, err := createOrgs(CTX, Client, 1) + if err != nil { + assert.Fail(t, "unable to create org") + return + } + orgId := orgs[0].Id + + tests := []struct { + name string + ctx context.Context + setupFunc func() + orgId string + metadataToDelete []struct { + key string + value string + } + metadataToRemain []struct { + key string + value string + } + err error + }{ + { + name: "delete org metadata happy path", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + setupFunc: func() { + _, err := Client.SetOrganizationMetadata(CTX, &v2beta_org.SetOrganizationMetadataRequest{ + OrganizationId: orgId, + Metadata: []*v2beta_org.Metadata{ + { + Key: "key1", + Value: []byte("value1"), + }, + }, + }) + require.NoError(t, err) + }, + orgId: orgId, + metadataToDelete: []struct{ key, value string }{ + { + key: "key1", + value: "value1", + }, + }, + }, + { + name: "delete multiple org metadata happy path", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + setupFunc: func() { + _, err := Client.SetOrganizationMetadata(CTX, &v2beta_org.SetOrganizationMetadataRequest{ + OrganizationId: orgId, + Metadata: []*v2beta_org.Metadata{ + { + Key: "key2", + Value: []byte("value2"), + }, + { + Key: "key3", + Value: []byte("value3"), + }, + }, + }) + require.NoError(t, err) + }, + orgId: orgId, + metadataToDelete: []struct{ key, value string }{ + { + key: "key2", + value: "value2", + }, + { + key: "key3", + value: "value3", + }, + }, + }, + { + name: "delete some org metadata but not all", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + setupFunc: func() { + _, err := Client.SetOrganizationMetadata(CTX, &v2beta_org.SetOrganizationMetadataRequest{ + OrganizationId: orgId, + Metadata: []*v2beta_org.Metadata{ + { + Key: "key4", + Value: []byte("value4"), + }, + // key5 should not be deleted + { + Key: "key5", + Value: []byte("value5"), + }, + { + Key: "key6", + Value: []byte("value6"), + }, + }, + }) + require.NoError(t, err) + }, + orgId: orgId, + metadataToDelete: []struct{ key, value string }{ + { + key: "key4", + value: "value4", + }, + { + key: "key6", + value: "value6", + }, + }, + metadataToRemain: []struct{ key, value string }{ + { + key: "key5", + value: "value5", + }, + }, + }, + { + name: "delete org metadata that does not exist", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + setupFunc: func() { + _, err := Client.SetOrganizationMetadata(CTX, &v2beta_org.SetOrganizationMetadataRequest{ + OrganizationId: orgId, + Metadata: []*v2beta_org.Metadata{ + { + Key: "key88", + Value: []byte("value74"), + }, + { + Key: "key5888", + Value: []byte("value8885"), + }, + }, + }) + require.NoError(t, err) + }, + orgId: orgId, + // TODO: this error message needs to be either removed or changed + err: errors.New("Metadata list is empty"), + }, + { + name: "delete org metadata for org that does not exist", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + setupFunc: func() { + _, err := Client.SetOrganizationMetadata(CTX, &v2beta_org.SetOrganizationMetadataRequest{ + OrganizationId: orgId, + Metadata: []*v2beta_org.Metadata{ + { + Key: "key88", + Value: []byte("value74"), + }, + { + Key: "key5888", + Value: []byte("value8885"), + }, + }, + }) + require.NoError(t, err) + }, + orgId: "non existant org id", + // TODO: this error message needs to be either removed or changed + err: errors.New("Metadata list is empty"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.setupFunc != nil { + tt.setupFunc() + } + + // check metadata exists + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, 10*time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + listOrgMetadataRes, err := Client.ListOrganizationMetadata(tt.ctx, &v2beta_org.ListOrganizationMetadataRequest{ + OrganizationId: tt.orgId, + }) + require.NoError(ttt, err) + foundMetadataCount := 0 + for _, kv := range tt.metadataToDelete { + for _, res := range listOrgMetadataRes.Metadata { + if res.Key == kv.key && + string(res.Value) == kv.value { + foundMetadataCount += 1 + } + } + } + require.Equal(ttt, len(tt.metadataToDelete), foundMetadataCount) + }, retryDuration, tick, "timeout waiting for expected organizations being created") + + keys := make([]string, len(tt.metadataToDelete)) + for i, kvp := range tt.metadataToDelete { + keys[i] = kvp.key + } + + // run delete + _, err = Client.DeleteOrganizationMetadata(tt.ctx, &v2beta_org.DeleteOrganizationMetadataRequest{ + OrganizationId: tt.orgId, + Keys: keys, + }) + if tt.err != nil { + require.Contains(t, err.Error(), tt.err.Error()) + return + } + require.NoError(t, err) + + retryDuration, tick = integration.WaitForAndTickWithMaxDuration(CTX, 10*time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + // check metadata was definitely deleted + listOrgMetadataRes, err := Client.ListOrganizationMetadata(tt.ctx, &v2beta_org.ListOrganizationMetadataRequest{ + OrganizationId: tt.orgId, + }) + require.NoError(ttt, err) + foundMetadataCount := 0 + for _, kv := range tt.metadataToDelete { + for _, res := range listOrgMetadataRes.Metadata { + if res.Key == kv.key && + string(res.Value) == kv.value { + foundMetadataCount += 1 + } + } + } + require.Equal(ttt, foundMetadataCount, 0) + }, retryDuration, tick, "timeout waiting for expected organizations being created") + + // check metadata that should not be delted was not deleted + listOrgMetadataRes, err := Client.ListOrganizationMetadata(tt.ctx, &v2beta_org.ListOrganizationMetadataRequest{ + OrganizationId: tt.orgId, + }) + require.NoError(t, err) + foundMetadataCount := 0 + for _, kv := range tt.metadataToRemain { + for _, res := range listOrgMetadataRes.Metadata { + if res.Key == kv.key && + string(res.Value) == kv.value { + foundMetadataCount += 1 + } + } + } + require.Equal(t, len(tt.metadataToRemain), foundMetadataCount) + }) + } +} + +func createOrgs(ctx context.Context, client v2beta_org.OrganizationServiceClient, noOfOrgs int) ([]*v2beta_org.CreateOrganizationResponse, []string, error) { + var err error + orgs := make([]*v2beta_org.CreateOrganizationResponse, noOfOrgs) + orgsName := make([]string, noOfOrgs) + + for i := range noOfOrgs { + orgName := gofakeit.Name() + orgsName[i] = orgName + orgs[i], err = client.CreateOrganization(ctx, + &v2beta_org.CreateOrganizationRequest{ + Name: orgName, + }, + ) + if err != nil { + return nil, nil, err + } + } + + return orgs, orgsName, nil +} + +func assertCreatedAdmin(t *testing.T, expected, got *v2beta_org.CreatedAdmin) { if expected.GetUserId() != "" { assert.NotEmpty(t, got.GetUserId()) } else { diff --git a/internal/api/grpc/org/v2beta/org.go b/internal/api/grpc/org/v2beta/org.go index ab2da2b766..edc667409a 100644 --- a/internal/api/grpc/org/v2beta/org.go +++ b/internal/api/grpc/org/v2beta/org.go @@ -2,16 +2,24 @@ package org import ( "context" + "errors" + "connectrpc.com/connect" + "google.golang.org/protobuf/types/known/timestamppb" + + metadata "github.com/zitadel/zitadel/internal/api/grpc/metadata/v2beta" object "github.com/zitadel/zitadel/internal/api/grpc/object/v2beta" user "github.com/zitadel/zitadel/internal/api/grpc/user/v2beta" "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/zerrors" + filter "github.com/zitadel/zitadel/pkg/grpc/filter/v2beta" org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" + v2beta_org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" ) -func (s *Server) AddOrganization(ctx context.Context, request *org.AddOrganizationRequest) (*org.AddOrganizationResponse, error) { - orgSetup, err := addOrganizationRequestToCommand(request) +func (s *Server) CreateOrganization(ctx context.Context, request *connect.Request[v2beta_org.CreateOrganizationRequest]) (*connect.Response[v2beta_org.CreateOrganizationResponse], error) { + orgSetup, err := createOrganizationRequestToCommand(request.Msg) if err != nil { return nil, err } @@ -22,8 +30,182 @@ func (s *Server) AddOrganization(ctx context.Context, request *org.AddOrganizati return createdOrganizationToPb(createdOrg) } -func addOrganizationRequestToCommand(request *org.AddOrganizationRequest) (*command.OrgSetup, error) { - admins, err := addOrganizationRequestAdminsToCommand(request.GetAdmins()) +func (s *Server) UpdateOrganization(ctx context.Context, request *connect.Request[v2beta_org.UpdateOrganizationRequest]) (*connect.Response[v2beta_org.UpdateOrganizationResponse], error) { + org, err := s.command.ChangeOrg(ctx, request.Msg.GetId(), request.Msg.GetName()) + if err != nil { + return nil, err + } + + return connect.NewResponse(&v2beta_org.UpdateOrganizationResponse{ + ChangeDate: timestamppb.New(org.EventDate), + }), nil +} + +func (s *Server) ListOrganizations(ctx context.Context, request *connect.Request[v2beta_org.ListOrganizationsRequest]) (*connect.Response[v2beta_org.ListOrganizationsResponse], error) { + queries, err := listOrgRequestToModel(s.systemDefaults, request.Msg) + if err != nil { + return nil, err + } + orgs, err := s.query.SearchOrgs(ctx, queries, s.checkPermission) + if err != nil { + return nil, err + } + return connect.NewResponse(&v2beta_org.ListOrganizationsResponse{ + Organizations: OrgViewsToPb(orgs.Orgs), + Pagination: &filter.PaginationResponse{ + TotalResult: orgs.Count, + AppliedLimit: uint64(request.Msg.GetPagination().GetLimit()), + }, + }), nil +} + +func (s *Server) DeleteOrganization(ctx context.Context, request *connect.Request[v2beta_org.DeleteOrganizationRequest]) (*connect.Response[v2beta_org.DeleteOrganizationResponse], error) { + details, err := s.command.RemoveOrg(ctx, request.Msg.GetId()) + if err != nil { + var notFoundError *zerrors.NotFoundError + if errors.As(err, ¬FoundError) { + return connect.NewResponse(&v2beta_org.DeleteOrganizationResponse{}), nil + } + return nil, err + } + return connect.NewResponse(&v2beta_org.DeleteOrganizationResponse{ + DeletionDate: timestamppb.New(details.EventDate), + }), nil +} + +func (s *Server) SetOrganizationMetadata(ctx context.Context, request *connect.Request[v2beta_org.SetOrganizationMetadataRequest]) (*connect.Response[v2beta_org.SetOrganizationMetadataResponse], error) { + result, err := s.command.BulkSetOrgMetadata(ctx, request.Msg.GetOrganizationId(), BulkSetOrgMetadataToDomain(request.Msg)...) + if err != nil { + return nil, err + } + return connect.NewResponse(&org.SetOrganizationMetadataResponse{ + SetDate: timestamppb.New(result.EventDate), + }), nil +} + +func (s *Server) ListOrganizationMetadata(ctx context.Context, request *connect.Request[v2beta_org.ListOrganizationMetadataRequest]) (*connect.Response[v2beta_org.ListOrganizationMetadataResponse], error) { + metadataQueries, err := ListOrgMetadataToDomain(s.systemDefaults, request.Msg) + if err != nil { + return nil, err + } + res, err := s.query.SearchOrgMetadata(ctx, true, request.Msg.GetOrganizationId(), metadataQueries, false) + if err != nil { + return nil, err + } + return connect.NewResponse(&v2beta_org.ListOrganizationMetadataResponse{ + Metadata: metadata.OrgMetadataListToPb(res.Metadata), + Pagination: &filter.PaginationResponse{ + TotalResult: res.Count, + AppliedLimit: uint64(request.Msg.GetPagination().GetLimit()), + }, + }), nil +} + +func (s *Server) DeleteOrganizationMetadata(ctx context.Context, request *connect.Request[v2beta_org.DeleteOrganizationMetadataRequest]) (*connect.Response[v2beta_org.DeleteOrganizationMetadataResponse], error) { + result, err := s.command.BulkRemoveOrgMetadata(ctx, request.Msg.GetOrganizationId(), request.Msg.Keys...) + if err != nil { + return nil, err + } + return connect.NewResponse(&v2beta_org.DeleteOrganizationMetadataResponse{ + DeletionDate: timestamppb.New(result.EventDate), + }), nil +} + +func (s *Server) DeactivateOrganization(ctx context.Context, request *connect.Request[org.DeactivateOrganizationRequest]) (*connect.Response[org.DeactivateOrganizationResponse], error) { + objectDetails, err := s.command.DeactivateOrg(ctx, request.Msg.GetId()) + if err != nil { + return nil, err + } + return connect.NewResponse(&org.DeactivateOrganizationResponse{ + ChangeDate: timestamppb.New(objectDetails.EventDate), + }), nil +} + +func (s *Server) ActivateOrganization(ctx context.Context, request *connect.Request[org.ActivateOrganizationRequest]) (*connect.Response[org.ActivateOrganizationResponse], error) { + objectDetails, err := s.command.ReactivateOrg(ctx, request.Msg.GetId()) + if err != nil { + return nil, err + } + return connect.NewResponse(&org.ActivateOrganizationResponse{ + ChangeDate: timestamppb.New(objectDetails.EventDate), + }), err +} + +func (s *Server) AddOrganizationDomain(ctx context.Context, request *connect.Request[org.AddOrganizationDomainRequest]) (*connect.Response[org.AddOrganizationDomainResponse], error) { + userIDs, err := s.getClaimedUserIDsOfOrgDomain(ctx, request.Msg.GetDomain(), request.Msg.GetOrganizationId()) + if err != nil { + return nil, err + } + details, err := s.command.AddOrgDomain(ctx, request.Msg.GetOrganizationId(), request.Msg.GetDomain(), userIDs) + if err != nil { + return nil, err + } + return connect.NewResponse(&org.AddOrganizationDomainResponse{ + CreationDate: timestamppb.New(details.EventDate), + }), nil +} + +func (s *Server) ListOrganizationDomains(ctx context.Context, req *connect.Request[org.ListOrganizationDomainsRequest]) (*connect.Response[org.ListOrganizationDomainsResponse], error) { + queries, err := ListOrgDomainsRequestToModel(s.systemDefaults, req.Msg) + if err != nil { + return nil, err + } + orgIDQuery, err := query.NewOrgDomainOrgIDSearchQuery(req.Msg.GetOrganizationId()) + if err != nil { + return nil, err + } + queries.Queries = append(queries.Queries, orgIDQuery) + + domains, err := s.query.SearchOrgDomains(ctx, queries, false) + if err != nil { + return nil, err + } + return connect.NewResponse(&org.ListOrganizationDomainsResponse{ + Domains: object.DomainsToPb(domains.Domains), + Pagination: &filter.PaginationResponse{ + TotalResult: domains.Count, + AppliedLimit: uint64(req.Msg.GetPagination().GetLimit()), + }, + }), nil +} + +func (s *Server) DeleteOrganizationDomain(ctx context.Context, req *connect.Request[org.DeleteOrganizationDomainRequest]) (*connect.Response[org.DeleteOrganizationDomainResponse], error) { + details, err := s.command.RemoveOrgDomain(ctx, RemoveOrgDomainRequestToDomain(ctx, req.Msg)) + if err != nil { + return nil, err + } + return connect.NewResponse(&org.DeleteOrganizationDomainResponse{ + DeletionDate: timestamppb.New(details.EventDate), + }), err +} + +func (s *Server) GenerateOrganizationDomainValidation(ctx context.Context, req *connect.Request[org.GenerateOrganizationDomainValidationRequest]) (*connect.Response[org.GenerateOrganizationDomainValidationResponse], error) { + token, url, err := s.command.GenerateOrgDomainValidation(ctx, GenerateOrgDomainValidationRequestToDomain(ctx, req.Msg)) + if err != nil { + return nil, err + } + return connect.NewResponse(&org.GenerateOrganizationDomainValidationResponse{ + Token: token, + Url: url, + }), nil +} + +func (s *Server) VerifyOrganizationDomain(ctx context.Context, request *connect.Request[org.VerifyOrganizationDomainRequest]) (*connect.Response[org.VerifyOrganizationDomainResponse], error) { + userIDs, err := s.getClaimedUserIDsOfOrgDomain(ctx, request.Msg.GetDomain(), request.Msg.GetOrganizationId()) + if err != nil { + return nil, err + } + details, err := s.command.ValidateOrgDomain(ctx, ValidateOrgDomainRequestToDomain(ctx, request.Msg), userIDs) + if err != nil { + return nil, err + } + return connect.NewResponse(&org.VerifyOrganizationDomainResponse{ + ChangeDate: timestamppb.New(details.EventDate), + }), nil +} + +func createOrganizationRequestToCommand(request *v2beta_org.CreateOrganizationRequest) (*command.OrgSetup, error) { + admins, err := createOrganizationRequestAdminsToCommand(request.GetAdmins()) if err != nil { return nil, err } @@ -31,13 +213,14 @@ func addOrganizationRequestToCommand(request *org.AddOrganizationRequest) (*comm Name: request.GetName(), CustomDomain: "", Admins: admins, + OrgID: request.GetId(), }, nil } -func addOrganizationRequestAdminsToCommand(requestAdmins []*org.AddOrganizationRequest_Admin) (admins []*command.OrgSetupAdmin, err error) { +func createOrganizationRequestAdminsToCommand(requestAdmins []*v2beta_org.CreateOrganizationRequest_Admin) (admins []*command.OrgSetupAdmin, err error) { admins = make([]*command.OrgSetupAdmin, len(requestAdmins)) for i, admin := range requestAdmins { - admins[i], err = addOrganizationRequestAdminToCommand(admin) + admins[i], err = createOrganizationRequestAdminToCommand(admin) if err != nil { return nil, err } @@ -45,14 +228,14 @@ func addOrganizationRequestAdminsToCommand(requestAdmins []*org.AddOrganizationR return admins, nil } -func addOrganizationRequestAdminToCommand(admin *org.AddOrganizationRequest_Admin) (*command.OrgSetupAdmin, error) { +func createOrganizationRequestAdminToCommand(admin *v2beta_org.CreateOrganizationRequest_Admin) (*command.OrgSetupAdmin, error) { switch a := admin.GetUserType().(type) { - case *org.AddOrganizationRequest_Admin_UserId: + case *v2beta_org.CreateOrganizationRequest_Admin_UserId: return &command.OrgSetupAdmin{ ID: a.UserId, Roles: admin.GetRoles(), }, nil - case *org.AddOrganizationRequest_Admin_Human: + case *v2beta_org.CreateOrganizationRequest_Admin_Human: human, err := user.AddUserRequestToAddHuman(a.Human) if err != nil { return nil, err @@ -62,22 +245,10 @@ func addOrganizationRequestAdminToCommand(admin *org.AddOrganizationRequest_Admi Roles: admin.GetRoles(), }, nil default: - return nil, zerrors.ThrowUnimplementedf(nil, "ORGv2-SD2r1", "userType oneOf %T in method AddOrganization not implemented", a) + return nil, zerrors.ThrowUnimplementedf(nil, "ORGv2-SL2r8", "userType oneOf %T in method AddOrganization not implemented", a) } } -func createdOrganizationToPb(createdOrg *command.CreatedOrg) (_ *org.AddOrganizationResponse, err error) { - admins := make([]*org.AddOrganizationResponse_CreatedAdmin, len(createdOrg.CreatedAdmins)) - for i, admin := range createdOrg.CreatedAdmins { - admins[i] = &org.AddOrganizationResponse_CreatedAdmin{ - UserId: admin.ID, - EmailCode: admin.EmailCode, - PhoneCode: admin.PhoneCode, - } - } - return &org.AddOrganizationResponse{ - Details: object.DomainToDetailsPb(createdOrg.ObjectDetails), - OrganizationId: createdOrg.ObjectDetails.ResourceOwner, - CreatedAdmins: admins, - }, nil +func (s *Server) getClaimedUserIDsOfOrgDomain(ctx context.Context, orgDomain, orgID string) ([]string, error) { + return s.query.SearchClaimedUserIDsOfOrgDomain(ctx, orgDomain, orgID) } diff --git a/internal/api/grpc/org/v2beta/org_test.go b/internal/api/grpc/org/v2beta/org_test.go index 5024b59c1d..85dec79be4 100644 --- a/internal/api/grpc/org/v2beta/org_test.go +++ b/internal/api/grpc/org/v2beta/org_test.go @@ -4,6 +4,7 @@ import ( "testing" "time" + "connectrpc.com/connect" "github.com/muhlemmer/gu" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -12,14 +13,13 @@ import ( "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/zerrors" - object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" ) -func Test_addOrganizationRequestToCommand(t *testing.T) { +func Test_createOrganizationRequestToCommand(t *testing.T) { type args struct { - request *org.AddOrganizationRequest + request *org.CreateOrganizationRequest } tests := []struct { name string @@ -30,23 +30,38 @@ func Test_addOrganizationRequestToCommand(t *testing.T) { { name: "nil user", args: args{ - request: &org.AddOrganizationRequest{ + request: &org.CreateOrganizationRequest{ Name: "name", - Admins: []*org.AddOrganizationRequest_Admin{ + Admins: []*org.CreateOrganizationRequest_Admin{ {}, }, }, }, - wantErr: zerrors.ThrowUnimplementedf(nil, "ORGv2-SD2r1", "userType oneOf %T in method AddOrganization not implemented", nil), + wantErr: zerrors.ThrowUnimplementedf(nil, "ORGv2-SL2r8", "userType oneOf %T in method AddOrganization not implemented", nil), + }, + { + name: "custom org ID", + args: args{ + request: &org.CreateOrganizationRequest{ + Name: "custom org ID", + Id: gu.Ptr("org-ID"), + }, + }, + want: &command.OrgSetup{ + Name: "custom org ID", + CustomDomain: "", + Admins: []*command.OrgSetupAdmin{}, + OrgID: "org-ID", + }, }, { name: "user ID", args: args{ - request: &org.AddOrganizationRequest{ + request: &org.CreateOrganizationRequest{ Name: "name", - Admins: []*org.AddOrganizationRequest_Admin{ + Admins: []*org.CreateOrganizationRequest_Admin{ { - UserType: &org.AddOrganizationRequest_Admin_UserId{ + UserType: &org.CreateOrganizationRequest_Admin_UserId{ UserId: "userID", }, Roles: nil, @@ -67,11 +82,11 @@ func Test_addOrganizationRequestToCommand(t *testing.T) { { name: "human user", args: args{ - request: &org.AddOrganizationRequest{ + request: &org.CreateOrganizationRequest{ Name: "name", - Admins: []*org.AddOrganizationRequest_Admin{ + Admins: []*org.CreateOrganizationRequest_Admin{ { - UserType: &org.AddOrganizationRequest_Admin_Human{ + UserType: &org.CreateOrganizationRequest_Admin_Human{ Human: &user.AddHumanUserRequest{ Profile: &user.SetHumanProfile{ GivenName: "firstname", @@ -109,7 +124,7 @@ func Test_addOrganizationRequestToCommand(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := addOrganizationRequestToCommand(tt.args.request) + got, err := createOrganizationRequestToCommand(tt.args.request) require.ErrorIs(t, err, tt.wantErr) assert.Equal(t, tt.want, got) }) @@ -124,7 +139,7 @@ func Test_createdOrganizationToPb(t *testing.T) { tests := []struct { name string args args - want *org.AddOrganizationResponse + want *connect.Response[org.CreateOrganizationResponse] wantErr error }{ { @@ -136,8 +151,8 @@ func Test_createdOrganizationToPb(t *testing.T) { EventDate: now, ResourceOwner: "orgID", }, - CreatedAdmins: []*command.CreatedOrgAdmin{ - { + OrgAdmins: []command.OrgAdmin{ + &command.CreatedOrgAdmin{ ID: "id", EmailCode: gu.Ptr("emailCode"), PhoneCode: gu.Ptr("phoneCode"), @@ -145,21 +160,21 @@ func Test_createdOrganizationToPb(t *testing.T) { }, }, }, - want: &org.AddOrganizationResponse{ - Details: &object.Details{ - Sequence: 1, - ChangeDate: timestamppb.New(now), - ResourceOwner: "orgID", - }, - OrganizationId: "orgID", - CreatedAdmins: []*org.AddOrganizationResponse_CreatedAdmin{ + want: connect.NewResponse(&org.CreateOrganizationResponse{ + CreationDate: timestamppb.New(now), + Id: "orgID", + OrganizationAdmins: []*org.OrganizationAdmin{ { - UserId: "id", - EmailCode: gu.Ptr("emailCode"), - PhoneCode: gu.Ptr("phoneCode"), + OrganizationAdmin: &org.OrganizationAdmin_CreatedAdmin{ + CreatedAdmin: &org.CreatedAdmin{ + UserId: "id", + EmailCode: gu.Ptr("emailCode"), + PhoneCode: gu.Ptr("phoneCode"), + }, + }, }, }, - }, + }), }, } for _, tt := range tests { diff --git a/internal/api/grpc/org/v2beta/server.go b/internal/api/grpc/org/v2beta/server.go index 89dba81702..8f9091c7c3 100644 --- a/internal/api/grpc/org/v2beta/server.go +++ b/internal/api/grpc/org/v2beta/server.go @@ -1,20 +1,25 @@ package org import ( - "google.golang.org/grpc" + "net/http" + + "connectrpc.com/connect" + "google.golang.org/protobuf/reflect/protoreflect" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/grpc/server" "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/config/systemdefaults" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query" org "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/org/v2beta/orgconnect" ) -var _ org.OrganizationServiceServer = (*Server)(nil) +var _ orgconnect.OrganizationServiceHandler = (*Server)(nil) type Server struct { - org.UnimplementedOrganizationServiceServer + systemDefaults systemdefaults.SystemDefaults command *command.Commands query *query.Queries checkPermission domain.PermissionCheck @@ -23,19 +28,25 @@ type Server struct { type Config struct{} func CreateServer( + systemDefaults systemdefaults.SystemDefaults, command *command.Commands, query *query.Queries, checkPermission domain.PermissionCheck, ) *Server { return &Server{ + systemDefaults: systemDefaults, command: command, query: query, checkPermission: checkPermission, } } -func (s *Server) RegisterServer(grpcServer *grpc.Server) { - org.RegisterOrganizationServiceServer(grpcServer, s) +func (s *Server) RegisterConnectServer(interceptors ...connect.Interceptor) (string, http.Handler) { + return orgconnect.NewOrganizationServiceHandler(s, connect.WithInterceptors(interceptors...)) +} + +func (s *Server) FileDescriptor() protoreflect.FileDescriptor { + return org.File_zitadel_org_v2beta_org_service_proto } func (s *Server) AppName() string { diff --git a/internal/api/grpc/project/v2beta/integration_test/project_grant_test.go b/internal/api/grpc/project/v2beta/integration_test/project_grant_test.go new file mode 100644 index 0000000000..34fa10e3de --- /dev/null +++ b/internal/api/grpc/project/v2beta/integration_test/project_grant_test.go @@ -0,0 +1,1351 @@ +//go:build integration + +package project_test + +import ( + "context" + "testing" + "time" + + "github.com/brianvoe/gofakeit/v6" + "github.com/stretchr/testify/assert" + + "github.com/zitadel/zitadel/internal/integration" + project "github.com/zitadel/zitadel/pkg/grpc/project/v2beta" +) + +func TestServer_CreateProjectGrant(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + type want struct { + creationDate bool + } + tests := []struct { + name string + ctx context.Context + prepare func(request *project.CreateProjectGrantRequest) + req *project.CreateProjectGrantRequest + want + wantErr bool + }{ + { + name: "empty projectID", + ctx: iamOwnerCtx, + req: &project.CreateProjectGrantRequest{}, + wantErr: true, + }, + { + name: "empty granted organization", + ctx: iamOwnerCtx, + req: &project.CreateProjectGrantRequest{ + ProjectId: "something", + GrantedOrganizationId: "", + }, + wantErr: true, + }, + { + name: "project not existing", + ctx: iamOwnerCtx, + prepare: func(request *project.CreateProjectGrantRequest) { + request.ProjectId = "something" + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + }, + req: &project.CreateProjectGrantRequest{}, + wantErr: true, + }, + { + name: "org not existing", + ctx: iamOwnerCtx, + prepare: func(request *project.CreateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + request.GrantedOrganizationId = "something" + }, + req: &project.CreateProjectGrantRequest{}, + wantErr: true, + }, + { + name: "already existing, error", + ctx: iamOwnerCtx, + prepare: func(request *project.CreateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + req: &project.CreateProjectGrantRequest{}, + wantErr: true, + }, + { + name: "same organization, error", + ctx: iamOwnerCtx, + prepare: func(request *project.CreateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + request.GrantedOrganizationId = orgResp.GetOrganizationId() + }, + req: &project.CreateProjectGrantRequest{}, + wantErr: true, + }, + { + name: "empty, ok", + ctx: iamOwnerCtx, + prepare: func(request *project.CreateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + }, + req: &project.CreateProjectGrantRequest{}, + want: want{ + creationDate: true, + }, + }, + { + name: "with roles, not existing", + ctx: iamOwnerCtx, + prepare: func(request *project.CreateProjectGrantRequest) { + roles := []string{gofakeit.Name(), gofakeit.Name(), gofakeit.Name()} + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + request.RoleKeys = roles + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + }, + req: &project.CreateProjectGrantRequest{}, + wantErr: true, + }, + { + name: "with roles, ok", + ctx: iamOwnerCtx, + prepare: func(request *project.CreateProjectGrantRequest) { + roles := []string{gofakeit.Name(), gofakeit.Name(), gofakeit.Name()} + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + for _, role := range roles { + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), role, role, "") + } + + request.RoleKeys = roles + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + }, + req: &project.CreateProjectGrantRequest{}, + want: want{ + creationDate: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.prepare != nil { + tt.prepare(tt.req) + } + + creationDate := time.Now().UTC() + got, err := instance.Client.Projectv2Beta.CreateProjectGrant(tt.ctx, tt.req) + changeDate := time.Now().UTC() + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertCreateProjectGrantResponse(t, creationDate, changeDate, tt.want.creationDate, got) + }) + } +} + +func TestServer_CreateProjectGrant_Permission(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + userResp := instance.CreateMachineUser(iamOwnerCtx) + patResp := instance.CreatePersonalAccessToken(iamOwnerCtx, userResp.GetUserId()) + projectResp := createProject(iamOwnerCtx, instance, t, instance.DefaultOrg.GetId(), false, false) + instance.CreateProjectMembership(t, iamOwnerCtx, projectResp.GetId(), userResp.GetUserId()) + projectOwnerCtx := integration.WithAuthorizationToken(CTX, patResp.Token) + + type want struct { + creationDate bool + } + tests := []struct { + name string + ctx context.Context + prepare func(request *project.CreateProjectGrantRequest) + req *project.CreateProjectGrantRequest + want + wantErr bool + }{ + { + name: "unauthenticated", + ctx: CTX, + prepare: func(request *project.CreateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + grantedOrgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + request.ProjectId = projectResp.GetId() + request.GrantedOrganizationId = grantedOrgResp.GetOrganizationId() + }, + req: &project.CreateProjectGrantRequest{}, + wantErr: true, + }, + { + name: "no permission", + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + prepare: func(request *project.CreateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + grantedOrgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + request.ProjectId = projectResp.GetId() + request.GrantedOrganizationId = grantedOrgResp.GetOrganizationId() + }, + req: &project.CreateProjectGrantRequest{}, + wantErr: true, + }, + { + name: "project owner, other project", + ctx: projectOwnerCtx, + prepare: func(request *project.CreateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + grantedOrgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + request.ProjectId = projectResp.GetId() + request.GrantedOrganizationId = grantedOrgResp.GetOrganizationId() + }, + req: &project.CreateProjectGrantRequest{}, + wantErr: true, + }, + { + name: "project owner, ok", + ctx: projectOwnerCtx, + prepare: func(request *project.CreateProjectGrantRequest) { + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + }, + req: &project.CreateProjectGrantRequest{}, + want: want{ + creationDate: true, + }, + }, + { + name: "organization owner, other org", + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + prepare: func(request *project.CreateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + grantedOrgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + request.ProjectId = projectResp.GetId() + request.GrantedOrganizationId = grantedOrgResp.GetOrganizationId() + }, + req: &project.CreateProjectGrantRequest{}, + wantErr: true, + }, + { + name: "organization owner, ok", + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + prepare: func(request *project.CreateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + }, + req: &project.CreateProjectGrantRequest{}, + want: want{ + creationDate: true, + }, + }, + { + name: "instance owner", + ctx: iamOwnerCtx, + prepare: func(request *project.CreateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + grantedOrgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + request.ProjectId = projectResp.GetId() + request.GrantedOrganizationId = grantedOrgResp.GetOrganizationId() + }, + req: &project.CreateProjectGrantRequest{}, + want: want{ + creationDate: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.prepare != nil { + tt.prepare(tt.req) + } + + creationDate := time.Now().UTC() + got, err := instance.Client.Projectv2Beta.CreateProjectGrant(tt.ctx, tt.req) + changeDate := time.Now().UTC() + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertCreateProjectGrantResponse(t, creationDate, changeDate, tt.want.creationDate, got) + }) + } +} + +func assertCreateProjectGrantResponse(t *testing.T, creationDate, changeDate time.Time, expectedCreationDate bool, actualResp *project.CreateProjectGrantResponse) { + if expectedCreationDate { + if !changeDate.IsZero() { + assert.WithinRange(t, actualResp.GetCreationDate().AsTime(), creationDate, changeDate) + } else { + assert.WithinRange(t, actualResp.GetCreationDate().AsTime(), creationDate, time.Now().UTC()) + } + } else { + assert.Nil(t, actualResp.CreationDate) + } +} + +func TestServer_UpdateProjectGrant(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + type args struct { + ctx context.Context + req *project.UpdateProjectGrantRequest + } + type want struct { + change bool + changeDate bool + } + tests := []struct { + name string + prepare func(request *project.UpdateProjectGrantRequest) + args args + want want + wantErr bool + }{ + { + name: "not existing", + prepare: func(request *project.UpdateProjectGrantRequest) { + request.ProjectId = "notexisting" + request.GrantedOrganizationId = "notexisting" + return + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.UpdateProjectGrantRequest{ + RoleKeys: []string{"notexisting"}, + }, + }, + wantErr: true, + }, + { + name: "no change, ok", + prepare: func(request *project.UpdateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.UpdateProjectGrantRequest{}, + }, + want: want{ + change: false, + changeDate: true, + }, + }, + { + name: "change roles, ok", + prepare: func(request *project.UpdateProjectGrantRequest) { + roles := []string{gofakeit.Animal(), gofakeit.Animal(), gofakeit.Animal()} + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + for _, role := range roles { + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), role, role, "") + } + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId(), roles...) + request.RoleKeys = roles + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.UpdateProjectGrantRequest{}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + { + name: "change roles, not existing", + prepare: func(request *project.UpdateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + request.RoleKeys = []string{gofakeit.Animal(), gofakeit.Animal(), gofakeit.Animal()} + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.UpdateProjectGrantRequest{}, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creationDate := time.Now().UTC() + if tt.prepare != nil { + tt.prepare(tt.args.req) + } + + got, err := instance.Client.Projectv2Beta.UpdateProjectGrant(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(t, err) + return + } + changeDate := time.Time{} + if tt.want.change { + changeDate = time.Now().UTC() + } + assert.NoError(t, err) + assertUpdateProjectGrantResponse(t, creationDate, changeDate, tt.want.changeDate, got) + }) + } +} + +func TestServer_UpdateProjectGrant_Permission(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + userResp := instance.CreateMachineUser(iamOwnerCtx) + patResp := instance.CreatePersonalAccessToken(iamOwnerCtx, userResp.GetUserId()) + projectID := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false).GetId() + instance.CreateProjectGrant(iamOwnerCtx, t, projectID, orgResp.GetOrganizationId()) + instance.CreateProjectGrantMembership(t, iamOwnerCtx, projectID, orgResp.GetOrganizationId(), userResp.GetUserId()) + projectGrantOwnerCtx := integration.WithAuthorizationToken(CTX, patResp.Token) + + type args struct { + ctx context.Context + req *project.UpdateProjectGrantRequest + } + type want struct { + change bool + changeDate bool + } + tests := []struct { + name string + prepare func(request *project.UpdateProjectGrantRequest) + args args + want want + wantErr bool + }{ + { + name: "unauthenticated", + prepare: func(request *project.UpdateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: CTX, + req: &project.UpdateProjectGrantRequest{ + RoleKeys: []string{"nopermission"}, + }, + }, + wantErr: true, + }, + { + name: "no permission", + prepare: func(request *project.UpdateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + req: &project.UpdateProjectGrantRequest{ + RoleKeys: []string{"nopermission"}, + }, + }, + wantErr: true, + }, + { + name: "project grant owner, no permission", + prepare: func(request *project.UpdateProjectGrantRequest) { + roles := []string{gofakeit.Animal(), gofakeit.Animal(), gofakeit.Animal()} + request.ProjectId = projectID + request.GrantedOrganizationId = orgResp.GetOrganizationId() + + for _, role := range roles { + instance.AddProjectRole(iamOwnerCtx, t, projectID, role, role, "") + } + + request.RoleKeys = roles + }, + args: args{ + ctx: projectGrantOwnerCtx, + req: &project.UpdateProjectGrantRequest{}, + }, + wantErr: true, + }, + { + name: "organization owner, other org", + prepare: func(request *project.UpdateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + req: &project.UpdateProjectGrantRequest{ + RoleKeys: []string{"nopermission"}, + }, + }, + wantErr: true, + }, + { + name: "organization owner, ok", + prepare: func(request *project.UpdateProjectGrantRequest) { + roles := []string{gofakeit.Animal(), gofakeit.Animal(), gofakeit.Animal()} + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + for _, role := range roles { + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), role, role, "") + } + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId(), roles...) + request.RoleKeys = roles + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + req: &project.UpdateProjectGrantRequest{}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + { + name: "instance owner, ok", + prepare: func(request *project.UpdateProjectGrantRequest) { + roles := []string{gofakeit.Animal(), gofakeit.Animal(), gofakeit.Animal()} + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + for _, role := range roles { + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), role, role, "") + } + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId(), roles...) + request.RoleKeys = roles + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.UpdateProjectGrantRequest{}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creationDate := time.Now().UTC() + if tt.prepare != nil { + tt.prepare(tt.args.req) + } + + got, err := instance.Client.Projectv2Beta.UpdateProjectGrant(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(t, err) + return + } + changeDate := time.Time{} + if tt.want.change { + changeDate = time.Now().UTC() + } + assert.NoError(t, err) + assertUpdateProjectGrantResponse(t, creationDate, changeDate, tt.want.changeDate, got) + }) + } +} + +func assertUpdateProjectGrantResponse(t *testing.T, creationDate, changeDate time.Time, expectedChangeDate bool, actualResp *project.UpdateProjectGrantResponse) { + if expectedChangeDate { + if !changeDate.IsZero() { + assert.WithinRange(t, actualResp.GetChangeDate().AsTime(), creationDate, changeDate) + } else { + assert.WithinRange(t, actualResp.GetChangeDate().AsTime(), creationDate, time.Now().UTC()) + } + } else { + assert.Nil(t, actualResp.ChangeDate) + } +} + +func TestServer_DeleteProjectGrant(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + tests := []struct { + name string + ctx context.Context + prepare func(request *project.DeleteProjectGrantRequest) (time.Time, time.Time) + req *project.DeleteProjectGrantRequest + wantDeletionDate bool + wantErr bool + }{ + { + name: "empty project id", + ctx: iamOwnerCtx, + req: &project.DeleteProjectGrantRequest{ + ProjectId: "", + }, + wantErr: true, + }, + { + name: "empty grantedorg id", + ctx: iamOwnerCtx, + req: &project.DeleteProjectGrantRequest{ + ProjectId: "notempty", + GrantedOrganizationId: "", + }, + wantErr: true, + }, + { + name: "delete, not existing", + ctx: iamOwnerCtx, + req: &project.DeleteProjectGrantRequest{ + ProjectId: "notexisting", + GrantedOrganizationId: "notexisting", + }, + wantDeletionDate: false, + }, + { + name: "delete", + ctx: iamOwnerCtx, + prepare: func(request *project.DeleteProjectGrantRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + return creationDate, time.Time{} + }, + req: &project.DeleteProjectGrantRequest{}, + wantDeletionDate: true, + }, + { + name: "delete deactivated", + ctx: iamOwnerCtx, + prepare: func(request *project.DeleteProjectGrantRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + instance.DeactivateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + return creationDate, time.Time{} + }, + req: &project.DeleteProjectGrantRequest{}, + wantDeletionDate: true, + }, + { + name: "delete, already removed", + ctx: iamOwnerCtx, + prepare: func(request *project.DeleteProjectGrantRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + instance.DeleteProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + return creationDate, time.Now().UTC() + }, + req: &project.DeleteProjectGrantRequest{}, + wantDeletionDate: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var creationDate, deletionDate time.Time + if tt.prepare != nil { + creationDate, deletionDate = tt.prepare(tt.req) + } + got, err := instance.Client.Projectv2Beta.DeleteProjectGrant(tt.ctx, tt.req) + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertDeleteProjectGrantResponse(t, creationDate, deletionDate, tt.wantDeletionDate, got) + }) + } +} + +func TestServer_DeleteProjectGrant_Permission(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + tests := []struct { + name string + ctx context.Context + prepare func(request *project.DeleteProjectGrantRequest) (time.Time, time.Time) + req *project.DeleteProjectGrantRequest + wantDeletionDate bool + wantErr bool + }{ + { + name: "unauthenticated", + ctx: CTX, + prepare: func(request *project.DeleteProjectGrantRequest) (time.Time, time.Time) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + return time.Time{}, time.Time{} + }, + req: &project.DeleteProjectGrantRequest{}, + wantErr: true, + }, + { + name: "no permission", + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + prepare: func(request *project.DeleteProjectGrantRequest) (time.Time, time.Time) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + return time.Time{}, time.Time{} + }, + req: &project.DeleteProjectGrantRequest{}, + wantErr: true, + }, + { + name: "organization owner, other org", + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + prepare: func(request *project.DeleteProjectGrantRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + return creationDate, time.Time{} + }, + req: &project.DeleteProjectGrantRequest{}, + wantErr: true, + }, + { + name: "organization owner, ok", + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + prepare: func(request *project.DeleteProjectGrantRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + return creationDate, time.Time{} + }, + req: &project.DeleteProjectGrantRequest{}, + wantDeletionDate: true, + }, + { + name: "organization owner, ok", + ctx: iamOwnerCtx, + prepare: func(request *project.DeleteProjectGrantRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + return creationDate, time.Time{} + }, + req: &project.DeleteProjectGrantRequest{}, + wantDeletionDate: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var creationDate, deletionDate time.Time + if tt.prepare != nil { + creationDate, deletionDate = tt.prepare(tt.req) + } + got, err := instance.Client.Projectv2Beta.DeleteProjectGrant(tt.ctx, tt.req) + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertDeleteProjectGrantResponse(t, creationDate, deletionDate, tt.wantDeletionDate, got) + }) + } +} + +func assertDeleteProjectGrantResponse(t *testing.T, creationDate, deletionDate time.Time, expectedDeletionDate bool, actualResp *project.DeleteProjectGrantResponse) { + if expectedDeletionDate { + if !deletionDate.IsZero() { + assert.WithinRange(t, actualResp.GetDeletionDate().AsTime(), creationDate, deletionDate) + } else { + assert.WithinRange(t, actualResp.GetDeletionDate().AsTime(), creationDate, time.Now().UTC()) + } + } else { + assert.Nil(t, actualResp.DeletionDate) + } +} + +func TestServer_DeactivateProjectGrant(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + type args struct { + ctx context.Context + req *project.DeactivateProjectGrantRequest + } + type want struct { + change bool + changeDate bool + } + tests := []struct { + name string + prepare func(request *project.DeactivateProjectGrantRequest) + args args + want want + wantErr bool + }{ + { + name: "missing permission", + prepare: func(request *project.DeactivateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + req: &project.DeactivateProjectGrantRequest{}, + }, + wantErr: true, + }, + { + name: "not existing", + prepare: func(request *project.DeactivateProjectGrantRequest) { + request.ProjectId = "notexisting" + request.GrantedOrganizationId = "notexisting" + return + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.DeactivateProjectGrantRequest{}, + }, + wantErr: true, + }, + { + name: "no change, ok", + prepare: func(request *project.DeactivateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + instance.DeactivateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.DeactivateProjectGrantRequest{}, + }, + want: want{ + change: false, + changeDate: true, + }, + }, + { + name: "change, ok", + prepare: func(request *project.DeactivateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.DeactivateProjectGrantRequest{}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creationDate := time.Now().UTC() + tt.prepare(tt.args.req) + + got, err := instance.Client.Projectv2Beta.DeactivateProjectGrant(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(t, err) + return + } + changeDate := time.Time{} + if tt.want.change { + changeDate = time.Now().UTC() + } + assert.NoError(t, err) + assertDeactivateProjectGrantResponse(t, creationDate, changeDate, tt.want.changeDate, got) + }) + } +} + +func TestServer_DeactivateProjectGrant_Permission(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + type args struct { + ctx context.Context + req *project.DeactivateProjectGrantRequest + } + type want struct { + change bool + changeDate bool + } + tests := []struct { + name string + prepare func(request *project.DeactivateProjectGrantRequest) + args args + want want + wantErr bool + }{ + { + name: "unauthenticated", + prepare: func(request *project.DeactivateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: CTX, + req: &project.DeactivateProjectGrantRequest{}, + }, + wantErr: true, + }, + { + name: "no permission", + prepare: func(request *project.DeactivateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + req: &project.DeactivateProjectGrantRequest{}, + }, + wantErr: true, + }, + { + name: "organization owner, other org", + prepare: func(request *project.DeactivateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + req: &project.DeactivateProjectGrantRequest{}, + }, + wantErr: true, + }, + { + name: "organization owner, ok", + prepare: func(request *project.DeactivateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + req: &project.DeactivateProjectGrantRequest{}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + { + name: "instance owner, ok", + prepare: func(request *project.DeactivateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.DeactivateProjectGrantRequest{}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creationDate := time.Now().UTC() + tt.prepare(tt.args.req) + + got, err := instance.Client.Projectv2Beta.DeactivateProjectGrant(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(t, err) + return + } + changeDate := time.Time{} + if tt.want.change { + changeDate = time.Now().UTC() + } + assert.NoError(t, err) + assertDeactivateProjectGrantResponse(t, creationDate, changeDate, tt.want.changeDate, got) + }) + } +} + +func assertDeactivateProjectGrantResponse(t *testing.T, creationDate, changeDate time.Time, expectedChangeDate bool, actualResp *project.DeactivateProjectGrantResponse) { + if expectedChangeDate { + if !changeDate.IsZero() { + assert.WithinRange(t, actualResp.GetChangeDate().AsTime(), creationDate, changeDate) + } else { + assert.WithinRange(t, actualResp.GetChangeDate().AsTime(), creationDate, time.Now().UTC()) + } + } else { + assert.Nil(t, actualResp.ChangeDate) + } +} + +func TestServer_ActivateProjectGrant(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + type args struct { + ctx context.Context + req *project.ActivateProjectGrantRequest + } + type want struct { + change bool + changeDate bool + } + tests := []struct { + name string + prepare func(request *project.ActivateProjectGrantRequest) + args args + want want + wantErr bool + }{ + { + name: "not existing", + prepare: func(request *project.ActivateProjectGrantRequest) { + request.ProjectId = "notexisting" + request.GrantedOrganizationId = "notexisting" + return + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.ActivateProjectGrantRequest{}, + }, + wantErr: true, + }, + { + name: "no change, ok", + prepare: func(request *project.ActivateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.ActivateProjectGrantRequest{}, + }, + want: want{ + change: false, + changeDate: true, + }, + }, + { + name: "change, ok", + prepare: func(request *project.ActivateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + instance.DeactivateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.ActivateProjectGrantRequest{}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creationDate := time.Now().UTC() + tt.prepare(tt.args.req) + + got, err := instance.Client.Projectv2Beta.ActivateProjectGrant(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(t, err) + return + } + changeDate := time.Time{} + if tt.want.change { + changeDate = time.Now().UTC() + } + assert.NoError(t, err) + assertActivateProjectGrantResponse(t, creationDate, changeDate, tt.want.changeDate, got) + }) + } +} + +func TestServer_ActivateProjectGrant_Permission(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + type args struct { + ctx context.Context + req *project.ActivateProjectGrantRequest + } + type want struct { + change bool + changeDate bool + } + tests := []struct { + name string + prepare func(request *project.ActivateProjectGrantRequest) + args args + want want + wantErr bool + }{ + { + name: "unauthenticated", + prepare: func(request *project.ActivateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: CTX, + req: &project.ActivateProjectGrantRequest{}, + }, + wantErr: true, + }, + { + name: "no permission", + prepare: func(request *project.ActivateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + instance.DeactivateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + req: &project.ActivateProjectGrantRequest{}, + }, + wantErr: true, + }, + { + name: "organization owner, other org", + prepare: func(request *project.ActivateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + instance.DeactivateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + req: &project.ActivateProjectGrantRequest{}, + }, + wantErr: true, + }, + { + name: "organization owner, ok", + prepare: func(request *project.ActivateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + instance.DeactivateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + req: &project.ActivateProjectGrantRequest{}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + { + name: "instance owner, ok", + prepare: func(request *project.ActivateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + instance.DeactivateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.ActivateProjectGrantRequest{}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creationDate := time.Now().UTC() + tt.prepare(tt.args.req) + + got, err := instance.Client.Projectv2Beta.ActivateProjectGrant(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(t, err) + return + } + changeDate := time.Time{} + if tt.want.change { + changeDate = time.Now().UTC() + } + assert.NoError(t, err) + assertActivateProjectGrantResponse(t, creationDate, changeDate, tt.want.changeDate, got) + }) + } +} + +func assertActivateProjectGrantResponse(t *testing.T, creationDate, changeDate time.Time, expectedChangeDate bool, actualResp *project.ActivateProjectGrantResponse) { + if expectedChangeDate { + if !changeDate.IsZero() { + assert.WithinRange(t, actualResp.GetChangeDate().AsTime(), creationDate, changeDate) + } else { + assert.WithinRange(t, actualResp.GetChangeDate().AsTime(), creationDate, time.Now().UTC()) + } + } else { + assert.Nil(t, actualResp.ChangeDate) + } +} diff --git a/internal/api/grpc/project/v2beta/integration_test/project_role_test.go b/internal/api/grpc/project/v2beta/integration_test/project_role_test.go new file mode 100644 index 0000000000..5e2f0e447e --- /dev/null +++ b/internal/api/grpc/project/v2beta/integration_test/project_role_test.go @@ -0,0 +1,698 @@ +//go:build integration + +package project_test + +import ( + "context" + "testing" + "time" + + "github.com/brianvoe/gofakeit/v6" + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + + "github.com/zitadel/zitadel/internal/integration" + project "github.com/zitadel/zitadel/pkg/grpc/project/v2beta" +) + +func TestServer_AddProjectRole(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + alreadyExistingProject := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + alreadyExistingProjectRoleName := gofakeit.AppName() + instance.AddProjectRole(iamOwnerCtx, t, alreadyExistingProject.GetId(), alreadyExistingProjectRoleName, alreadyExistingProjectRoleName, "") + + type want struct { + creationDate bool + } + tests := []struct { + name string + ctx context.Context + prepare func(request *project.AddProjectRoleRequest) + req *project.AddProjectRoleRequest + want + wantErr bool + }{ + { + name: "empty key", + ctx: iamOwnerCtx, + prepare: func(request *project.AddProjectRoleRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + }, + req: &project.AddProjectRoleRequest{ + RoleKey: "", + DisplayName: gofakeit.AppName(), + }, + wantErr: true, + }, + { + name: "empty displayname", + ctx: iamOwnerCtx, + prepare: func(request *project.AddProjectRoleRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + }, + req: &project.AddProjectRoleRequest{ + RoleKey: gofakeit.AppName(), + DisplayName: "", + }, + wantErr: true, + }, + { + name: "already existing, error", + ctx: iamOwnerCtx, + prepare: func(request *project.AddProjectRoleRequest) { + request.ProjectId = alreadyExistingProject.GetId() + }, + req: &project.AddProjectRoleRequest{ + RoleKey: alreadyExistingProjectRoleName, + DisplayName: alreadyExistingProjectRoleName, + }, + wantErr: true, + }, + { + name: "empty, ok", + ctx: iamOwnerCtx, + prepare: func(request *project.AddProjectRoleRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + }, + req: &project.AddProjectRoleRequest{ + RoleKey: gofakeit.Name(), + DisplayName: gofakeit.Name(), + }, + want: want{ + creationDate: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.prepare != nil { + tt.prepare(tt.req) + } + + creationDate := time.Now().UTC() + got, err := instance.Client.Projectv2Beta.AddProjectRole(tt.ctx, tt.req) + changeDate := time.Now().UTC() + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertAddProjectRoleResponse(t, creationDate, changeDate, tt.want.creationDate, got) + }) + } +} + +func TestServer_AddProjectRole_Permission(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + alreadyExistingProject := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + alreadyExistingProjectRoleName := gofakeit.AppName() + instance.AddProjectRole(iamOwnerCtx, t, alreadyExistingProject.GetId(), alreadyExistingProjectRoleName, alreadyExistingProjectRoleName, "") + + type want struct { + creationDate bool + } + tests := []struct { + name string + ctx context.Context + prepare func(request *project.AddProjectRoleRequest) + req *project.AddProjectRoleRequest + want + wantErr bool + }{ + { + name: "unauthenticated", + ctx: CTX, + prepare: func(request *project.AddProjectRoleRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + }, + req: &project.AddProjectRoleRequest{ + RoleKey: gofakeit.AppName(), + DisplayName: gofakeit.AppName(), + }, + wantErr: true, + }, + { + name: "no permission", + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + prepare: func(request *project.AddProjectRoleRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + }, + req: &project.AddProjectRoleRequest{ + RoleKey: gofakeit.AppName(), + DisplayName: gofakeit.AppName(), + }, + wantErr: true, + }, + { + name: "organization owner, other org", + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + prepare: func(request *project.AddProjectRoleRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + }, + req: &project.AddProjectRoleRequest{ + RoleKey: gofakeit.AppName(), + DisplayName: gofakeit.AppName(), + }, + wantErr: true, + }, + { + name: "organization owner, ok", + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + prepare: func(request *project.AddProjectRoleRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + }, + req: &project.AddProjectRoleRequest{ + RoleKey: gofakeit.AppName(), + DisplayName: gofakeit.AppName(), + }, + want: want{ + creationDate: true, + }, + }, + { + name: "instance owner, ok", + ctx: iamOwnerCtx, + prepare: func(request *project.AddProjectRoleRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.ProjectId = projectResp.GetId() + }, + req: &project.AddProjectRoleRequest{ + RoleKey: gofakeit.AppName(), + DisplayName: gofakeit.AppName(), + }, + want: want{ + creationDate: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.prepare != nil { + tt.prepare(tt.req) + } + + creationDate := time.Now().UTC() + got, err := instance.Client.Projectv2Beta.AddProjectRole(tt.ctx, tt.req) + changeDate := time.Now().UTC() + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertAddProjectRoleResponse(t, creationDate, changeDate, tt.want.creationDate, got) + }) + } +} + +func assertAddProjectRoleResponse(t *testing.T, creationDate, changeDate time.Time, expectedCreationDate bool, actualResp *project.AddProjectRoleResponse) { + if expectedCreationDate { + if !changeDate.IsZero() { + assert.WithinRange(t, actualResp.GetCreationDate().AsTime(), creationDate, changeDate) + } else { + assert.WithinRange(t, actualResp.GetCreationDate().AsTime(), creationDate, time.Now().UTC()) + } + } else { + assert.Nil(t, actualResp.CreationDate) + } +} + +func TestServer_UpdateProjectRole(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + type args struct { + ctx context.Context + req *project.UpdateProjectRoleRequest + } + type want struct { + change bool + changeDate bool + } + tests := []struct { + name string + prepare func(request *project.UpdateProjectRoleRequest) + args args + want want + wantErr bool + }{ + { + name: "missing permission", + prepare: func(request *project.UpdateProjectRoleRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + roleName := gofakeit.AppName() + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName, roleName, "") + request.ProjectId = projectResp.GetId() + request.RoleKey = roleName + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + req: &project.UpdateProjectRoleRequest{ + DisplayName: gu.Ptr("changed"), + }, + }, + wantErr: true, + }, + { + name: "not existing", + prepare: func(request *project.UpdateProjectRoleRequest) { + request.RoleKey = "notexisting" + return + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.UpdateProjectRoleRequest{ + DisplayName: gu.Ptr("changed"), + }, + }, + wantErr: true, + }, + { + name: "no change, ok", + prepare: func(request *project.UpdateProjectRoleRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + roleName := gofakeit.AppName() + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName, roleName, "") + request.ProjectId = projectResp.GetId() + request.RoleKey = roleName + request.DisplayName = gu.Ptr(roleName) + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.UpdateProjectRoleRequest{}, + }, + want: want{ + change: false, + changeDate: true, + }, + }, + { + name: "change display name, ok", + prepare: func(request *project.UpdateProjectRoleRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + roleName := gofakeit.AppName() + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName, roleName, "") + request.ProjectId = projectResp.GetId() + request.RoleKey = roleName + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.UpdateProjectRoleRequest{ + DisplayName: gu.Ptr(gofakeit.AppName()), + }, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + { + name: "change full, ok", + prepare: func(request *project.UpdateProjectRoleRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + roleName := gofakeit.AppName() + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName, roleName, "") + request.ProjectId = projectResp.GetId() + request.RoleKey = roleName + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.UpdateProjectRoleRequest{ + DisplayName: gu.Ptr(gofakeit.AppName()), + Group: gu.Ptr(gofakeit.AppName()), + }, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creationDate := time.Now().UTC() + tt.prepare(tt.args.req) + + got, err := instance.Client.Projectv2Beta.UpdateProjectRole(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(t, err) + return + } + changeDate := time.Time{} + if tt.want.change { + changeDate = time.Now().UTC() + } + assert.NoError(t, err) + assertUpdateProjectRoleResponse(t, creationDate, changeDate, tt.want.changeDate, got) + }) + } +} + +func TestServer_UpdateProjectRole_Permission(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + type args struct { + ctx context.Context + req *project.UpdateProjectRoleRequest + } + type want struct { + change bool + changeDate bool + } + tests := []struct { + name string + prepare func(request *project.UpdateProjectRoleRequest) + args args + want want + wantErr bool + }{ + { + name: "unauthenicated", + prepare: func(request *project.UpdateProjectRoleRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + roleName := gofakeit.AppName() + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName, roleName, "") + request.ProjectId = projectResp.GetId() + request.RoleKey = roleName + }, + args: args{ + ctx: CTX, + req: &project.UpdateProjectRoleRequest{ + DisplayName: gu.Ptr("changed"), + }, + }, + wantErr: true, + }, + { + name: "no permission", + prepare: func(request *project.UpdateProjectRoleRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + roleName := gofakeit.AppName() + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName, roleName, "") + request.ProjectId = projectResp.GetId() + request.RoleKey = roleName + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + req: &project.UpdateProjectRoleRequest{ + DisplayName: gu.Ptr("changed"), + }, + }, + wantErr: true, + }, + { + name: "organization owner, other org", + prepare: func(request *project.UpdateProjectRoleRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + roleName := gofakeit.AppName() + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName, roleName, "") + request.ProjectId = projectResp.GetId() + request.RoleKey = roleName + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + req: &project.UpdateProjectRoleRequest{ + DisplayName: gu.Ptr("changed"), + }, + }, + wantErr: true, + }, + { + name: "organization owner, ok", + prepare: func(request *project.UpdateProjectRoleRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + roleName := gofakeit.AppName() + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName, roleName, "") + request.ProjectId = projectResp.GetId() + request.RoleKey = roleName + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + req: &project.UpdateProjectRoleRequest{ + DisplayName: gu.Ptr(gofakeit.AppName()), + }, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + { + name: "instance owner, ok", + prepare: func(request *project.UpdateProjectRoleRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + roleName := gofakeit.AppName() + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName, roleName, "") + request.ProjectId = projectResp.GetId() + request.RoleKey = roleName + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.UpdateProjectRoleRequest{ + DisplayName: gu.Ptr(gofakeit.AppName()), + }, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creationDate := time.Now().UTC() + tt.prepare(tt.args.req) + + got, err := instance.Client.Projectv2Beta.UpdateProjectRole(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(t, err) + return + } + changeDate := time.Time{} + if tt.want.change { + changeDate = time.Now().UTC() + } + assert.NoError(t, err) + assertUpdateProjectRoleResponse(t, creationDate, changeDate, tt.want.changeDate, got) + }) + } +} + +func assertUpdateProjectRoleResponse(t *testing.T, creationDate, changeDate time.Time, expectedChangeDate bool, actualResp *project.UpdateProjectRoleResponse) { + if expectedChangeDate { + if !changeDate.IsZero() { + assert.WithinRange(t, actualResp.GetChangeDate().AsTime(), creationDate, changeDate) + } else { + assert.WithinRange(t, actualResp.GetChangeDate().AsTime(), creationDate, time.Now().UTC()) + } + } else { + assert.Nil(t, actualResp.ChangeDate) + } +} + +func TestServer_DeleteProjectRole(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + tests := []struct { + name string + ctx context.Context + prepare func(request *project.RemoveProjectRoleRequest) (time.Time, time.Time) + req *project.RemoveProjectRoleRequest + wantDeletionDate bool + wantErr bool + }{ + { + name: "empty id", + ctx: iamOwnerCtx, + req: &project.RemoveProjectRoleRequest{ + ProjectId: "", + RoleKey: "notexisting", + }, + wantErr: true, + }, + { + name: "delete, not existing", + ctx: iamOwnerCtx, + req: &project.RemoveProjectRoleRequest{ + ProjectId: "notexisting", + RoleKey: "notexisting", + }, + wantDeletionDate: false, + }, + { + name: "delete", + ctx: iamOwnerCtx, + prepare: func(request *project.RemoveProjectRoleRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + roleName := gofakeit.AppName() + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName, roleName, "") + request.ProjectId = projectResp.GetId() + request.RoleKey = roleName + return creationDate, time.Time{} + }, + req: &project.RemoveProjectRoleRequest{}, + wantDeletionDate: true, + }, + { + name: "delete, already removed", + ctx: iamOwnerCtx, + prepare: func(request *project.RemoveProjectRoleRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + roleName := gofakeit.AppName() + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName, roleName, "") + request.ProjectId = projectResp.GetId() + request.RoleKey = roleName + instance.RemoveProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName) + return creationDate, time.Now().UTC() + }, + req: &project.RemoveProjectRoleRequest{}, + wantDeletionDate: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var creationDate, deletionDate time.Time + if tt.prepare != nil { + creationDate, deletionDate = tt.prepare(tt.req) + } + got, err := instance.Client.Projectv2Beta.RemoveProjectRole(tt.ctx, tt.req) + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertRemoveProjectRoleResponse(t, creationDate, deletionDate, tt.wantDeletionDate, got) + }) + } +} + +func TestServer_DeleteProjectRole_Permission(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + tests := []struct { + name string + ctx context.Context + prepare func(request *project.RemoveProjectRoleRequest) (time.Time, time.Time) + req *project.RemoveProjectRoleRequest + wantDeletionDate bool + wantErr bool + }{ + { + name: "unauthenticated", + ctx: CTX, + prepare: func(request *project.RemoveProjectRoleRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + roleName := gofakeit.AppName() + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName, roleName, "") + request.ProjectId = projectResp.GetId() + request.RoleKey = roleName + return creationDate, time.Time{} + }, + req: &project.RemoveProjectRoleRequest{}, + wantErr: true, + }, + { + name: "no permission", + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + prepare: func(request *project.RemoveProjectRoleRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + roleName := gofakeit.AppName() + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName, roleName, "") + request.ProjectId = projectResp.GetId() + request.RoleKey = roleName + return creationDate, time.Time{} + }, + req: &project.RemoveProjectRoleRequest{}, + wantErr: true, + }, + { + name: "organization owner, other org", + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + prepare: func(request *project.RemoveProjectRoleRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + roleName := gofakeit.AppName() + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName, roleName, "") + request.ProjectId = projectResp.GetId() + request.RoleKey = roleName + return creationDate, time.Time{} + }, + req: &project.RemoveProjectRoleRequest{}, + wantErr: true, + }, + { + name: "organization owner, ok", + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + prepare: func(request *project.RemoveProjectRoleRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + roleName := gofakeit.AppName() + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName, roleName, "") + request.ProjectId = projectResp.GetId() + request.RoleKey = roleName + return creationDate, time.Time{} + }, + req: &project.RemoveProjectRoleRequest{}, + wantDeletionDate: true, + }, + { + name: "instance owner, ok", + ctx: iamOwnerCtx, + prepare: func(request *project.RemoveProjectRoleRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + roleName := gofakeit.AppName() + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName, roleName, "") + request.ProjectId = projectResp.GetId() + request.RoleKey = roleName + return creationDate, time.Time{} + }, + req: &project.RemoveProjectRoleRequest{}, + wantDeletionDate: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var creationDate, deletionDate time.Time + if tt.prepare != nil { + creationDate, deletionDate = tt.prepare(tt.req) + } + got, err := instance.Client.Projectv2Beta.RemoveProjectRole(tt.ctx, tt.req) + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertRemoveProjectRoleResponse(t, creationDate, deletionDate, tt.wantDeletionDate, got) + }) + } +} + +func assertRemoveProjectRoleResponse(t *testing.T, creationDate, deletionDate time.Time, expectedDeletionDate bool, actualResp *project.RemoveProjectRoleResponse) { + if expectedDeletionDate { + if !deletionDate.IsZero() { + assert.WithinRange(t, actualResp.GetRemovalDate().AsTime(), creationDate, deletionDate) + } else { + assert.WithinRange(t, actualResp.GetRemovalDate().AsTime(), creationDate, time.Now().UTC()) + } + } else { + assert.Nil(t, actualResp.RemovalDate) + } +} diff --git a/internal/api/grpc/project/v2beta/integration_test/project_test.go b/internal/api/grpc/project/v2beta/integration_test/project_test.go new file mode 100644 index 0000000000..5412f6eb58 --- /dev/null +++ b/internal/api/grpc/project/v2beta/integration_test/project_test.go @@ -0,0 +1,1078 @@ +//go:build integration + +package project_test + +import ( + "context" + "testing" + "time" + + "github.com/brianvoe/gofakeit/v6" + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + + "github.com/zitadel/zitadel/internal/integration" + project "github.com/zitadel/zitadel/pkg/grpc/project/v2beta" +) + +func TestServer_CreateProject(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + alreadyExistingProjectName := gofakeit.AppName() + instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), alreadyExistingProjectName, false, false) + + type want struct { + id bool + creationDate bool + } + tests := []struct { + name string + ctx context.Context + req *project.CreateProjectRequest + want + wantErr bool + }{ + { + name: "empty name", + ctx: iamOwnerCtx, + req: &project.CreateProjectRequest{ + Name: "", + }, + wantErr: true, + }, + { + name: "empty organization", + ctx: iamOwnerCtx, + req: &project.CreateProjectRequest{ + Name: gofakeit.Name(), + OrganizationId: "", + }, + wantErr: true, + }, + { + name: "already existing, error", + ctx: iamOwnerCtx, + req: &project.CreateProjectRequest{ + Name: alreadyExistingProjectName, + OrganizationId: orgResp.GetOrganizationId(), + }, + wantErr: true, + }, + { + name: "empty, ok", + ctx: iamOwnerCtx, + req: &project.CreateProjectRequest{ + Name: gofakeit.Name(), + OrganizationId: orgResp.GetOrganizationId(), + }, + want: want{ + id: true, + creationDate: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creationDate := time.Now().UTC() + got, err := instance.Client.Projectv2Beta.CreateProject(tt.ctx, tt.req) + changeDate := time.Now().UTC() + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertCreateProjectResponse(t, creationDate, changeDate, tt.want.creationDate, tt.want.id, got) + }) + } +} + +func TestServer_CreateProject_Permission(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + type want struct { + id bool + creationDate bool + } + tests := []struct { + name string + ctx context.Context + req *project.CreateProjectRequest + want + wantErr bool + }{ + { + name: "unauthenticated", + ctx: CTX, + req: &project.CreateProjectRequest{ + Name: gofakeit.Name(), + OrganizationId: orgResp.GetOrganizationId(), + }, + wantErr: true, + }, + { + name: "missing permission", + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + req: &project.CreateProjectRequest{ + Name: gofakeit.Name(), + OrganizationId: orgResp.GetOrganizationId(), + }, + wantErr: true, + }, + { + name: "missing permission, other organization", + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + req: &project.CreateProjectRequest{ + Name: gofakeit.Name(), + OrganizationId: orgResp.GetOrganizationId(), + }, + wantErr: true, + }, + { + name: "organization owner, ok", + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + req: &project.CreateProjectRequest{ + Name: gofakeit.Name(), + OrganizationId: instance.DefaultOrg.GetId(), + }, + want: want{ + id: true, + creationDate: true, + }, + }, + { + name: "instance owner, ok", + ctx: iamOwnerCtx, + req: &project.CreateProjectRequest{ + Name: gofakeit.Name(), + OrganizationId: orgResp.GetOrganizationId(), + }, + want: want{ + id: true, + creationDate: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creationDate := time.Now().UTC() + got, err := instance.Client.Projectv2Beta.CreateProject(tt.ctx, tt.req) + changeDate := time.Now().UTC() + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertCreateProjectResponse(t, creationDate, changeDate, tt.want.creationDate, tt.want.id, got) + }) + } +} + +func assertCreateProjectResponse(t *testing.T, creationDate, changeDate time.Time, expectedCreationDate, expectedID bool, actualResp *project.CreateProjectResponse) { + if expectedCreationDate { + if !changeDate.IsZero() { + assert.WithinRange(t, actualResp.GetCreationDate().AsTime(), creationDate, changeDate) + } else { + assert.WithinRange(t, actualResp.GetCreationDate().AsTime(), creationDate, time.Now().UTC()) + } + } else { + assert.Nil(t, actualResp.CreationDate) + } + + if expectedID { + assert.NotEmpty(t, actualResp.GetId()) + } else { + assert.Nil(t, actualResp.Id) + } +} + +func TestServer_UpdateProject(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + type args struct { + ctx context.Context + req *project.UpdateProjectRequest + } + type want struct { + change bool + changeDate bool + } + tests := []struct { + name string + prepare func(request *project.UpdateProjectRequest) + args args + want want + wantErr bool + }{ + { + name: "not existing", + prepare: func(request *project.UpdateProjectRequest) { + request.Id = "notexisting" + return + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.UpdateProjectRequest{ + Name: gu.Ptr(gofakeit.Name()), + }, + }, + wantErr: true, + }, + { + name: "no change, ok", + prepare: func(request *project.UpdateProjectRequest) { + name := gofakeit.AppName() + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name, false, false).GetId() + request.Id = projectID + request.Name = gu.Ptr(name) + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.UpdateProjectRequest{}, + }, + want: want{ + change: false, + changeDate: true, + }, + }, + { + name: "change name, ok", + prepare: func(request *project.UpdateProjectRequest) { + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false).GetId() + request.Id = projectID + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.UpdateProjectRequest{ + Name: gu.Ptr(gofakeit.AppName()), + }, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + { + name: "change full, ok", + prepare: func(request *project.UpdateProjectRequest) { + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false).GetId() + request.Id = projectID + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.UpdateProjectRequest{ + Name: gu.Ptr(gofakeit.AppName()), + ProjectRoleAssertion: gu.Ptr(true), + ProjectRoleCheck: gu.Ptr(true), + HasProjectCheck: gu.Ptr(true), + PrivateLabelingSetting: gu.Ptr(project.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_ALLOW_LOGIN_USER_RESOURCE_OWNER_POLICY), + }, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creationDate := time.Now().UTC() + tt.prepare(tt.args.req) + + got, err := instance.Client.Projectv2Beta.UpdateProject(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(t, err) + return + } + changeDate := time.Time{} + if tt.want.change { + changeDate = time.Now().UTC() + } + assert.NoError(t, err) + assertUpdateProjectResponse(t, creationDate, changeDate, tt.want.changeDate, got) + }) + } +} + +func TestServer_UpdateProject_Permission(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + userResp := instance.CreateMachineUser(iamOwnerCtx) + patResp := instance.CreatePersonalAccessToken(iamOwnerCtx, userResp.GetUserId()) + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false).GetId() + instance.CreateProjectMembership(t, iamOwnerCtx, projectID, userResp.GetUserId()) + projectOwnerCtx := integration.WithAuthorizationToken(CTX, patResp.Token) + + type args struct { + ctx context.Context + req *project.UpdateProjectRequest + } + type want struct { + change bool + changeDate bool + } + tests := []struct { + name string + prepare func(request *project.UpdateProjectRequest) + args args + want want + wantErr bool + }{ + { + name: "unauthenticated", + prepare: func(request *project.UpdateProjectRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.Id = projectResp.GetId() + }, + args: args{ + ctx: CTX, + req: &project.UpdateProjectRequest{ + Name: gu.Ptr(gofakeit.Name()), + }, + }, + wantErr: true, + }, + { + name: "missing permission", + prepare: func(request *project.UpdateProjectRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.Id = projectResp.GetId() + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + req: &project.UpdateProjectRequest{ + Name: gu.Ptr(gofakeit.Name()), + }, + }, + wantErr: true, + }, + { + name: "project owner, no permission", + prepare: func(request *project.UpdateProjectRequest) { + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false).GetId() + request.Id = projectID + }, + args: args{ + ctx: projectOwnerCtx, + req: &project.UpdateProjectRequest{ + Name: gu.Ptr(gofakeit.AppName()), + }, + }, + wantErr: true, + }, + { + name: " roject owner, ok", + prepare: func(request *project.UpdateProjectRequest) { + request.Id = projectID + }, + args: args{ + ctx: projectOwnerCtx, + req: &project.UpdateProjectRequest{ + Name: gu.Ptr(gofakeit.AppName()), + }, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + { + name: "missing permission, other organization", + prepare: func(request *project.UpdateProjectRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.Id = projectResp.GetId() + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + req: &project.UpdateProjectRequest{ + Name: gu.Ptr(gofakeit.Name()), + }, + }, + wantErr: true, + }, + { + name: "organization owner, ok", + prepare: func(request *project.UpdateProjectRequest) { + projectID := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false).GetId() + request.Id = projectID + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + req: &project.UpdateProjectRequest{ + Name: gu.Ptr(gofakeit.AppName()), + }, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + { + name: "instance owner, ok", + prepare: func(request *project.UpdateProjectRequest) { + projectID := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false).GetId() + request.Id = projectID + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.UpdateProjectRequest{ + Name: gu.Ptr(gofakeit.AppName()), + }, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creationDate := time.Now().UTC() + tt.prepare(tt.args.req) + + got, err := instance.Client.Projectv2Beta.UpdateProject(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(t, err) + return + } + changeDate := time.Time{} + if tt.want.change { + changeDate = time.Now().UTC() + } + assert.NoError(t, err) + assertUpdateProjectResponse(t, creationDate, changeDate, tt.want.changeDate, got) + }) + } +} + +func assertUpdateProjectResponse(t *testing.T, creationDate, changeDate time.Time, expectedChangeDate bool, actualResp *project.UpdateProjectResponse) { + if expectedChangeDate { + if !changeDate.IsZero() { + assert.WithinRange(t, actualResp.GetChangeDate().AsTime(), creationDate, changeDate) + } else { + assert.WithinRange(t, actualResp.GetChangeDate().AsTime(), creationDate, time.Now().UTC()) + } + } else { + assert.Nil(t, actualResp.ChangeDate) + } +} + +func TestServer_DeleteProject(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + tests := []struct { + name string + ctx context.Context + prepare func(request *project.DeleteProjectRequest) (time.Time, time.Time) + req *project.DeleteProjectRequest + wantDeletionDate bool + wantErr bool + }{ + { + name: "empty id", + ctx: iamOwnerCtx, + req: &project.DeleteProjectRequest{ + Id: "", + }, + wantErr: true, + }, + { + name: "delete, not existing", + ctx: iamOwnerCtx, + req: &project.DeleteProjectRequest{ + Id: "notexisting", + }, + wantDeletionDate: false, + }, + { + name: "delete", + ctx: iamOwnerCtx, + prepare: func(request *project.DeleteProjectRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false).GetId() + request.Id = projectID + return creationDate, time.Time{} + }, + req: &project.DeleteProjectRequest{}, + wantDeletionDate: true, + }, + { + name: "delete, already removed", + ctx: iamOwnerCtx, + prepare: func(request *project.DeleteProjectRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false).GetId() + request.Id = projectID + instance.DeleteProject(iamOwnerCtx, t, projectID) + return creationDate, time.Now().UTC() + }, + req: &project.DeleteProjectRequest{}, + wantDeletionDate: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var creationDate, deletionDate time.Time + if tt.prepare != nil { + creationDate, deletionDate = tt.prepare(tt.req) + } + got, err := instance.Client.Projectv2Beta.DeleteProject(tt.ctx, tt.req) + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertDeleteProjectResponse(t, creationDate, deletionDate, tt.wantDeletionDate, got) + }) + } +} + +func TestServer_DeleteProject_Permission(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + userResp := instance.CreateMachineUser(iamOwnerCtx) + patResp := instance.CreatePersonalAccessToken(iamOwnerCtx, userResp.GetUserId()) + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false).GetId() + instance.CreateProjectMembership(t, iamOwnerCtx, projectID, userResp.GetUserId()) + projectOwnerCtx := integration.WithAuthorizationToken(CTX, patResp.Token) + + tests := []struct { + name string + ctx context.Context + prepare func(request *project.DeleteProjectRequest) (time.Time, time.Time) + req *project.DeleteProjectRequest + wantDeletionDate bool + wantErr bool + }{ + { + name: "unauthenticated", + ctx: CTX, + prepare: func(request *project.DeleteProjectRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false).GetId() + request.Id = projectID + return creationDate, time.Time{} + }, + req: &project.DeleteProjectRequest{}, + wantErr: true, + }, + { + name: "missing permission", + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + prepare: func(request *project.DeleteProjectRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false).GetId() + request.Id = projectID + return creationDate, time.Time{} + }, + req: &project.DeleteProjectRequest{}, + wantErr: true, + }, + { + name: "project owner, no permission", + ctx: projectOwnerCtx, + prepare: func(request *project.DeleteProjectRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false).GetId() + request.Id = projectID + return creationDate, time.Time{} + }, + req: &project.DeleteProjectRequest{}, + wantErr: true, + }, + { + name: "project owner, ok", + ctx: projectOwnerCtx, + prepare: func(request *project.DeleteProjectRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + request.Id = projectID + return creationDate, time.Time{} + }, + req: &project.DeleteProjectRequest{}, + wantDeletionDate: true, + }, + { + name: "organization owner, other org", + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + prepare: func(request *project.DeleteProjectRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false).GetId() + request.Id = projectID + return creationDate, time.Time{} + }, + req: &project.DeleteProjectRequest{}, + wantErr: true, + }, + { + name: "organization owner", + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + prepare: func(request *project.DeleteProjectRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectID := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false).GetId() + request.Id = projectID + return creationDate, time.Time{} + }, + req: &project.DeleteProjectRequest{}, + wantDeletionDate: true, + }, + { + name: "instance owner", + ctx: iamOwnerCtx, + prepare: func(request *project.DeleteProjectRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectID := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false).GetId() + request.Id = projectID + return creationDate, time.Time{} + }, + req: &project.DeleteProjectRequest{}, + wantDeletionDate: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var creationDate, deletionDate time.Time + if tt.prepare != nil { + creationDate, deletionDate = tt.prepare(tt.req) + } + got, err := instance.Client.Projectv2Beta.DeleteProject(tt.ctx, tt.req) + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertDeleteProjectResponse(t, creationDate, deletionDate, tt.wantDeletionDate, got) + }) + } +} + +func assertDeleteProjectResponse(t *testing.T, creationDate, deletionDate time.Time, expectedDeletionDate bool, actualResp *project.DeleteProjectResponse) { + if expectedDeletionDate { + if !deletionDate.IsZero() { + assert.WithinRange(t, actualResp.GetDeletionDate().AsTime(), creationDate, deletionDate) + } else { + assert.WithinRange(t, actualResp.GetDeletionDate().AsTime(), creationDate, time.Now().UTC()) + } + } else { + assert.Nil(t, actualResp.DeletionDate) + } +} + +func TestServer_DeactivateProject(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + type args struct { + ctx context.Context + req *project.DeactivateProjectRequest + } + type want struct { + change bool + changeDate bool + } + tests := []struct { + name string + prepare func(request *project.DeactivateProjectRequest) + args args + want want + wantErr bool + }{ + { + name: "not existing", + prepare: func(request *project.DeactivateProjectRequest) { + request.Id = "notexisting" + return + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.DeactivateProjectRequest{}, + }, + wantErr: true, + }, + { + name: "no change, ok", + prepare: func(request *project.DeactivateProjectRequest) { + name := gofakeit.AppName() + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name, false, false).GetId() + request.Id = projectID + instance.DeactivateProject(iamOwnerCtx, t, projectID) + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.DeactivateProjectRequest{}, + }, + wantErr: true, + }, + { + name: "change, ok", + prepare: func(request *project.DeactivateProjectRequest) { + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false).GetId() + request.Id = projectID + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.DeactivateProjectRequest{}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creationDate := time.Now().UTC() + tt.prepare(tt.args.req) + + got, err := instance.Client.Projectv2Beta.DeactivateProject(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(t, err) + return + } + changeDate := time.Time{} + if tt.want.change { + changeDate = time.Now().UTC() + } + assert.NoError(t, err) + assertDeactivateProjectResponse(t, creationDate, changeDate, tt.want.changeDate, got) + }) + } +} + +func TestServer_DeactivateProject_Permission(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + type args struct { + ctx context.Context + req *project.DeactivateProjectRequest + } + type want struct { + change bool + changeDate bool + } + tests := []struct { + name string + prepare func(request *project.DeactivateProjectRequest) + args args + want want + wantErr bool + }{ + { + name: "unauthenticated", + prepare: func(request *project.DeactivateProjectRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.Id = projectResp.GetId() + }, + args: args{ + ctx: CTX, + req: &project.DeactivateProjectRequest{}, + }, + wantErr: true, + }, + { + name: "missing permission", + prepare: func(request *project.DeactivateProjectRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.Id = projectResp.GetId() + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + req: &project.DeactivateProjectRequest{}, + }, + wantErr: true, + }, + { + name: "organization owner, other org", + prepare: func(request *project.DeactivateProjectRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.Id = projectResp.GetId() + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + req: &project.DeactivateProjectRequest{}, + }, + wantErr: true, + }, + { + name: "organization owner", + prepare: func(request *project.DeactivateProjectRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + request.Id = projectResp.GetId() + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + req: &project.DeactivateProjectRequest{}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + { + name: "instance owner", + prepare: func(request *project.DeactivateProjectRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.Id = projectResp.GetId() + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.DeactivateProjectRequest{}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creationDate := time.Now().UTC() + tt.prepare(tt.args.req) + + got, err := instance.Client.Projectv2Beta.DeactivateProject(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(t, err) + return + } + changeDate := time.Time{} + if tt.want.change { + changeDate = time.Now().UTC() + } + assert.NoError(t, err) + assertDeactivateProjectResponse(t, creationDate, changeDate, tt.want.changeDate, got) + }) + } +} + +func assertDeactivateProjectResponse(t *testing.T, creationDate, changeDate time.Time, expectedChangeDate bool, actualResp *project.DeactivateProjectResponse) { + if expectedChangeDate { + if !changeDate.IsZero() { + assert.WithinRange(t, actualResp.GetChangeDate().AsTime(), creationDate, changeDate) + } else { + assert.WithinRange(t, actualResp.GetChangeDate().AsTime(), creationDate, time.Now().UTC()) + } + } else { + assert.Nil(t, actualResp.ChangeDate) + } +} + +func TestServer_ActivateProject(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + type args struct { + ctx context.Context + req *project.ActivateProjectRequest + } + type want struct { + change bool + changeDate bool + } + tests := []struct { + name string + prepare func(request *project.ActivateProjectRequest) + args args + want want + wantErr bool + }{ + { + name: "missing permission", + prepare: func(request *project.ActivateProjectRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.Id = projectResp.GetId() + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + req: &project.ActivateProjectRequest{}, + }, + wantErr: true, + }, + { + name: "not existing", + prepare: func(request *project.ActivateProjectRequest) { + request.Id = "notexisting" + return + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.ActivateProjectRequest{}, + }, + wantErr: true, + }, + { + name: "no change, ok", + prepare: func(request *project.ActivateProjectRequest) { + name := gofakeit.AppName() + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name, false, false).GetId() + request.Id = projectID + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.ActivateProjectRequest{}, + }, + wantErr: true, + }, + { + name: "change, ok", + prepare: func(request *project.ActivateProjectRequest) { + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false).GetId() + request.Id = projectID + instance.DeactivateProject(iamOwnerCtx, t, projectID) + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.ActivateProjectRequest{}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creationDate := time.Now().UTC() + tt.prepare(tt.args.req) + + got, err := instance.Client.Projectv2Beta.ActivateProject(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(t, err) + return + } + changeDate := time.Time{} + if tt.want.change { + changeDate = time.Now().UTC() + } + assert.NoError(t, err) + assertActivateProjectResponse(t, creationDate, changeDate, tt.want.changeDate, got) + }) + } +} + +func TestServer_ActivateProject_Permission(t *testing.T) { + iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + type args struct { + ctx context.Context + req *project.ActivateProjectRequest + } + type want struct { + change bool + changeDate bool + } + tests := []struct { + name string + prepare func(request *project.ActivateProjectRequest) + args args + want want + wantErr bool + }{ + { + name: "unauthenticated", + prepare: func(request *project.ActivateProjectRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.Id = projectResp.GetId() + instance.DeactivateProject(iamOwnerCtx, t, projectResp.GetId()) + }, + args: args{ + ctx: CTX, + req: &project.ActivateProjectRequest{}, + }, + wantErr: true, + }, + { + name: "missing permission", + prepare: func(request *project.ActivateProjectRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.Id = projectResp.GetId() + instance.DeactivateProject(iamOwnerCtx, t, projectResp.GetId()) + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + req: &project.ActivateProjectRequest{}, + }, + wantErr: true, + }, + { + name: "organization owner, other org", + prepare: func(request *project.ActivateProjectRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.Id = projectResp.GetId() + instance.DeactivateProject(iamOwnerCtx, t, projectResp.GetId()) + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + req: &project.ActivateProjectRequest{}, + }, + wantErr: true, + }, + { + name: "organization owner", + prepare: func(request *project.ActivateProjectRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + request.Id = projectResp.GetId() + instance.DeactivateProject(iamOwnerCtx, t, projectResp.GetId()) + }, + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + req: &project.ActivateProjectRequest{}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + { + name: "instance owner", + prepare: func(request *project.ActivateProjectRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + request.Id = projectResp.GetId() + instance.DeactivateProject(iamOwnerCtx, t, projectResp.GetId()) + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.ActivateProjectRequest{}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creationDate := time.Now().UTC() + tt.prepare(tt.args.req) + + got, err := instance.Client.Projectv2Beta.ActivateProject(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(t, err) + return + } + changeDate := time.Time{} + if tt.want.change { + changeDate = time.Now().UTC() + } + assert.NoError(t, err) + assertActivateProjectResponse(t, creationDate, changeDate, tt.want.changeDate, got) + }) + } +} + +func assertActivateProjectResponse(t *testing.T, creationDate, changeDate time.Time, expectedChangeDate bool, actualResp *project.ActivateProjectResponse) { + if expectedChangeDate { + if !changeDate.IsZero() { + assert.WithinRange(t, actualResp.GetChangeDate().AsTime(), creationDate, changeDate) + } else { + assert.WithinRange(t, actualResp.GetChangeDate().AsTime(), creationDate, time.Now().UTC()) + } + } else { + assert.Nil(t, actualResp.ChangeDate) + } +} diff --git a/internal/api/grpc/project/v2beta/integration_test/query_test.go b/internal/api/grpc/project/v2beta/integration_test/query_test.go new file mode 100644 index 0000000000..29fa212976 --- /dev/null +++ b/internal/api/grpc/project/v2beta/integration_test/query_test.go @@ -0,0 +1,1907 @@ +//go:build integration + +package project_test + +import ( + "context" + "testing" + "time" + + "github.com/brianvoe/gofakeit/v6" + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/internal/integration" + filter "github.com/zitadel/zitadel/pkg/grpc/filter/v2beta" + project "github.com/zitadel/zitadel/pkg/grpc/project/v2beta" +) + +func TestServer_GetProject(t *testing.T) { + iamOwnerCtx := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + + type args struct { + ctx context.Context + dep func(*project.GetProjectRequest, *project.GetProjectResponse) + req *project.GetProjectRequest + } + tests := []struct { + name string + args args + want *project.GetProjectResponse + wantErr bool + }{ + { + name: "missing permission", + args: args{ + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeNoPermission), + dep: func(request *project.GetProjectRequest, response *project.GetProjectResponse) { + orgID := instance.DefaultOrg.GetId() + resp := createProject(iamOwnerCtx, instance, t, orgID, false, false) + + request.Id = resp.GetId() + }, + req: &project.GetProjectRequest{}, + }, + wantErr: true, + }, + { + name: "missing permission, other org owner", + args: args{ + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.GetProjectRequest, response *project.GetProjectResponse) { + name := gofakeit.AppName() + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + resp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name, false, false) + + request.Id = resp.GetId() + }, + req: &project.GetProjectRequest{}, + }, + wantErr: true, + }, + { + name: "not found", + args: args{ + ctx: iamOwnerCtx, + req: &project.GetProjectRequest{Id: "notexisting"}, + }, + wantErr: true, + }, + { + name: "get, ok", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.GetProjectRequest, response *project.GetProjectResponse) { + orgID := instance.DefaultOrg.GetId() + resp := createProject(iamOwnerCtx, instance, t, orgID, false, false) + + request.Id = resp.GetId() + response.Project = resp + }, + req: &project.GetProjectRequest{}, + }, + want: &project.GetProjectResponse{ + Project: &project.Project{ + State: 1, + ProjectRoleAssertion: false, + ProjectAccessRequired: false, + AuthorizationRequired: false, + PrivateLabelingSetting: project.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_UNSPECIFIED, + }, + }, + }, + { + name: "get, ok, org owner", + args: args{ + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.GetProjectRequest, response *project.GetProjectResponse) { + orgID := instance.DefaultOrg.GetId() + resp := createProject(iamOwnerCtx, instance, t, orgID, false, false) + + request.Id = resp.GetId() + response.Project = resp + }, + req: &project.GetProjectRequest{}, + }, + want: &project.GetProjectResponse{ + Project: &project.Project{}, + }, + }, + { + name: "get, full, ok", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.GetProjectRequest, response *project.GetProjectResponse) { + orgID := instance.DefaultOrg.GetId() + resp := createProject(iamOwnerCtx, instance, t, orgID, true, true) + + request.Id = resp.GetId() + response.Project = resp + }, + req: &project.GetProjectRequest{}, + }, + want: &project.GetProjectResponse{ + Project: &project.Project{}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.args.dep != nil { + tt.args.dep(tt.args.req, tt.want) + } + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(iamOwnerCtx, 2*time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, err := instance.Client.Projectv2Beta.GetProject(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(ttt, err) + return + } + assert.NoError(ttt, err) + assert.EqualExportedValues(ttt, tt.want, got) + }, retryDuration, tick, "timeout waiting for expected project result") + }) + } +} + +func TestServer_ListProjects(t *testing.T) { + iamOwnerCtx := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + + userResp := instance.CreateMachineUser(iamOwnerCtx) + patResp := instance.CreatePersonalAccessToken(iamOwnerCtx, userResp.GetUserId()) + projectResp := createProject(iamOwnerCtx, instance, t, instance.DefaultOrg.GetId(), false, false) + instance.CreateProjectMembership(t, iamOwnerCtx, projectResp.GetId(), userResp.GetUserId()) + grantedProjectResp := createGrantedProject(iamOwnerCtx, instance, t, projectResp) + projectOwnerCtx := integration.WithAuthorizationToken(CTX, patResp.Token) + + type args struct { + ctx context.Context + dep func(*project.ListProjectsRequest, *project.ListProjectsResponse) + req *project.ListProjectsRequest + } + tests := []struct { + name string + args args + want *project.ListProjectsResponse + wantErr bool + }{ + { + name: "list by id, unauthenticated", + args: args{ + ctx: CTX, + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + name := gofakeit.AppName() + orgID := instance.DefaultOrg.GetId() + resp := instance.CreateProject(iamOwnerCtx, t, orgID, name, false, false) + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{resp.GetId()}, + }, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + wantErr: true, + }, + { + name: "list by id, no permission", + args: args{ + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeNoPermission), + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + name := gofakeit.AppName() + orgID := instance.DefaultOrg.GetId() + resp := instance.CreateProject(iamOwnerCtx, t, orgID, name, false, false) + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{resp.GetId()}, + }, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + wantErr: true, + }, + { + name: "list by id, missing permission", + args: args{ + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + name := gofakeit.AppName() + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + resp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name, false, false) + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{resp.GetId()}, + }, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Projects: []*project.Project{}, + }, + }, + { + name: "list, not found", + args: args{ + ctx: iamOwnerCtx, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{ + {Filter: &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{"notfound"}, + }, + }, + }, + }, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 0, + AppliedLimit: 100, + }, + }, + }, + { + name: "list single id", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + orgID := instance.DefaultOrg.GetId() + response.Projects[0] = createProject(iamOwnerCtx, instance, t, orgID, false, false) + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{response.Projects[0].GetId()}, + }, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Projects: []*project.Project{ + {}, + }, + }, + }, + { + name: "list single name", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + orgID := instance.DefaultOrg.GetId() + response.Projects[0] = createProject(iamOwnerCtx, instance, t, orgID, false, false) + request.Filters[0].Filter = &project.ProjectSearchFilter_ProjectNameFilter{ + ProjectNameFilter: &project.ProjectNameFilter{ + ProjectName: response.Projects[0].Name, + }, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Projects: []*project.Project{ + { + State: 1, + ProjectRoleAssertion: false, + ProjectAccessRequired: false, + AuthorizationRequired: false, + PrivateLabelingSetting: project.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_UNSPECIFIED, + }, + }, + }, + }, + { + name: "list multiple id", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + orgID := instance.DefaultOrg.GetId() + response.Projects[2] = createProject(iamOwnerCtx, instance, t, orgID, false, false) + response.Projects[1] = createProject(iamOwnerCtx, instance, t, orgID, true, false) + response.Projects[0] = createProject(iamOwnerCtx, instance, t, orgID, false, true) + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{response.Projects[0].GetId(), response.Projects[1].GetId(), response.Projects[2].GetId()}, + }, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + Projects: []*project.Project{ + {}, + {}, + {}, + }, + }, + }, + { + name: "list multiple id, limited permissions", + args: args{ + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + orgID := instance.DefaultOrg.GetId() + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + resp1 := createProject(iamOwnerCtx, instance, t, orgResp.GetOrganizationId(), false, false) + resp2 := createProject(iamOwnerCtx, instance, t, orgID, true, false) + resp3 := createProject(iamOwnerCtx, instance, t, orgResp.GetOrganizationId(), false, true) + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{resp1.GetId(), resp2.GetId(), resp3.GetId()}, + }, + } + + response.Projects[0] = resp2 + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + Projects: []*project.Project{ + {}, + }, + }, + }, + { + name: "list multiple id, limited permissions, project owner", + args: args{ + ctx: projectOwnerCtx, + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + orgID := instance.DefaultOrg.GetId() + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + resp1 := createProject(iamOwnerCtx, instance, t, orgResp.GetOrganizationId(), false, false) + resp2 := createProject(iamOwnerCtx, instance, t, orgID, true, false) + resp3 := createProject(iamOwnerCtx, instance, t, orgResp.GetOrganizationId(), false, true) + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{resp1.GetId(), resp2.GetId(), resp3.GetId(), projectResp.GetId()}}, + } + response.Projects[0] = grantedProjectResp + response.Projects[1] = projectResp + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 5, + AppliedLimit: 100, + }, + Projects: []*project.Project{ + {}, + {}, + }, + }, + }, + { + name: "list project and granted projects", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + orgID := instance.DefaultOrg.GetId() + projectResp := createProject(iamOwnerCtx, instance, t, orgID, true, true) + response.Projects[3] = projectResp + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetId()}, + }, + } + response.Projects[2] = createGrantedProject(iamOwnerCtx, instance, t, projectResp) + response.Projects[1] = createGrantedProject(iamOwnerCtx, instance, t, projectResp) + response.Projects[0] = createGrantedProject(iamOwnerCtx, instance, t, projectResp) + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 4, + AppliedLimit: 100, + }, + Projects: []*project.Project{ + {}, + {}, + {}, + {}, + }, + }, + }, + { + name: "list project and granted projects, organization", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + orgID := instance.DefaultOrg.GetId() + projectResp := createProject(iamOwnerCtx, instance, t, orgID, true, true) + + grantedProjectResp := createGrantedProject(iamOwnerCtx, instance, t, projectResp) + response.Projects[1] = grantedProjectResp + response.Projects[0] = createProject(iamOwnerCtx, instance, t, *grantedProjectResp.GrantedOrganizationId, true, true) + request.Filters[0].Filter = &project.ProjectSearchFilter_ProjectOrganizationIdFilter{ + ProjectOrganizationIdFilter: &filter.IDFilter{Id: *grantedProjectResp.GrantedOrganizationId}, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 2, + AppliedLimit: 100, + }, + Projects: []*project.Project{ + {}, + {}, + }, + }, + }, + { + name: "list project and granted projects, project resourceowner", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + orgID := instance.DefaultOrg.GetId() + projectResp := createProject(iamOwnerCtx, instance, t, orgID, true, true) + + grantedProjectResp := createGrantedProject(iamOwnerCtx, instance, t, projectResp) + response.Projects[0] = createProject(iamOwnerCtx, instance, t, *grantedProjectResp.GrantedOrganizationId, true, true) + request.Filters[0].Filter = &project.ProjectSearchFilter_ProjectResourceOwnerFilter{ + ProjectResourceOwnerFilter: &filter.IDFilter{Id: *grantedProjectResp.GrantedOrganizationId}, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Projects: []*project.Project{ + {}, + }, + }, + }, + { + name: "list granted project, project id", + args: args{ + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + orgID := instance.DefaultOrg.GetId() + + orgName := gofakeit.AppName() + projectName := gofakeit.AppName() + orgResp := instance.CreateOrganization(iamOwnerCtx, orgName, gofakeit.Email()) + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), projectName, true, true) + projectGrantResp := instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), orgID) + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{Ids: []string{projectResp.GetId()}}, + } + response.Projects[0] = &project.Project{ + Id: projectResp.GetId(), + Name: projectName, + OrganizationId: orgResp.GetOrganizationId(), + CreationDate: projectGrantResp.GetCreationDate(), + ChangeDate: projectGrantResp.GetCreationDate(), + State: 1, + ProjectRoleAssertion: false, + ProjectAccessRequired: true, + AuthorizationRequired: true, + PrivateLabelingSetting: project.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_UNSPECIFIED, + GrantedOrganizationId: gu.Ptr(orgID), + GrantedOrganizationName: gu.Ptr(instance.DefaultOrg.GetName()), + GrantedState: 1, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 2, + AppliedLimit: 100, + }, + Projects: []*project.Project{ + {}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.args.dep != nil { + tt.args.dep(tt.args.req, tt.want) + } + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(iamOwnerCtx, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, listErr := instance.Client.Projectv2Beta.ListProjects(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(ttt, listErr) + return + } + require.NoError(ttt, listErr) + + // always first check length, otherwise its failed anyway + if assert.Len(ttt, got.Projects, len(tt.want.Projects)) { + for i := range tt.want.Projects { + assert.EqualExportedValues(ttt, tt.want.Projects[i], got.Projects[i]) + } + } + assertPaginationResponse(ttt, tt.want.Pagination, got.Pagination) + }, retryDuration, tick, "timeout waiting for expected execution result") + }) + } +} + +func TestServer_ListProjects_PermissionV2(t *testing.T) { + // removed as permission v2 is not implemented yet for project grant level permissions + // ensureFeaturePermissionV2Enabled(t, instancePermissionV2) + iamOwnerCtx := instancePermissionV2.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + orgID := instancePermissionV2.DefaultOrg.GetId() + + type args struct { + ctx context.Context + dep func(*project.ListProjectsRequest, *project.ListProjectsResponse) + req *project.ListProjectsRequest + } + tests := []struct { + name string + args args + want *project.ListProjectsResponse + wantErr bool + }{ + { + name: "list by id, unauthenticated", + args: args{ + ctx: CTX, + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + resp := createProject(iamOwnerCtx, instancePermissionV2, t, orgID, false, false) + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{resp.GetId()}, + }, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + wantErr: true, + }, + { + name: "list by id, no permission", + args: args{ + ctx: instancePermissionV2.WithAuthorizationToken(CTX, integration.UserTypeNoPermission), + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + resp := createProject(iamOwnerCtx, instancePermissionV2, t, orgID, false, false) + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{resp.GetId()}, + }, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + wantErr: true, + }, + { + name: "list by id, missing permission", + args: args{ + ctx: instancePermissionV2.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + orgResp := instancePermissionV2.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + resp := createProject(iamOwnerCtx, instancePermissionV2, t, orgResp.GetOrganizationId(), false, false) + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{resp.GetId()}, + }, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Projects: []*project.Project{}, + }, + }, + { + name: "list, not found", + args: args{ + ctx: iamOwnerCtx, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{ + {Filter: &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{"notfound"}, + }, + }, + }, + }, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 0, + AppliedLimit: 100, + }, + }, + }, + { + name: "list single id", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + response.Projects[0] = createProject(iamOwnerCtx, instancePermissionV2, t, orgID, false, false) + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{response.Projects[0].GetId()}, + }, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Projects: []*project.Project{ + {}, + }, + }, + }, + { + name: "list single name", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + response.Projects[0] = createProject(iamOwnerCtx, instancePermissionV2, t, orgID, false, false) + request.Filters[0].Filter = &project.ProjectSearchFilter_ProjectNameFilter{ + ProjectNameFilter: &project.ProjectNameFilter{ + ProjectName: response.Projects[0].Name, + }, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Projects: []*project.Project{ + {}, + }, + }, + }, + { + name: "list multiple id", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + response.Projects[2] = createProject(iamOwnerCtx, instancePermissionV2, t, orgID, false, false) + response.Projects[1] = createProject(iamOwnerCtx, instancePermissionV2, t, orgID, true, false) + response.Projects[0] = createProject(iamOwnerCtx, instancePermissionV2, t, orgID, false, true) + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{response.Projects[0].GetId(), response.Projects[1].GetId(), response.Projects[2].GetId()}, + }, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + Projects: []*project.Project{ + {}, + {}, + {}, + }, + }, + }, + { + name: "list project and granted projects", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + projectResp := createProject(iamOwnerCtx, instancePermissionV2, t, orgID, true, true) + response.Projects[3] = projectResp + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetId()}, + }, + } + response.Projects[2] = createGrantedProject(iamOwnerCtx, instancePermissionV2, t, projectResp) + response.Projects[1] = createGrantedProject(iamOwnerCtx, instancePermissionV2, t, projectResp) + response.Projects[0] = createGrantedProject(iamOwnerCtx, instancePermissionV2, t, projectResp) + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 4, + AppliedLimit: 100, + }, + Projects: []*project.Project{ + {}, + {}, + {}, + {}, + }, + }, + }, + { + name: "list project and granted projects, organization", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + projectResp := createProject(iamOwnerCtx, instancePermissionV2, t, orgID, true, true) + + grantedProjectResp := createGrantedProject(iamOwnerCtx, instancePermissionV2, t, projectResp) + response.Projects[1] = grantedProjectResp + response.Projects[0] = createProject(iamOwnerCtx, instancePermissionV2, t, *grantedProjectResp.GrantedOrganizationId, true, true) + request.Filters[0].Filter = &project.ProjectSearchFilter_ProjectOrganizationIdFilter{ + ProjectOrganizationIdFilter: &filter.IDFilter{Id: *grantedProjectResp.GrantedOrganizationId}, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 2, + AppliedLimit: 100, + }, + Projects: []*project.Project{ + {}, + {}, + }, + }, + }, + { + name: "list project and granted projects, project resourceowner", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + projectResp := createProject(iamOwnerCtx, instancePermissionV2, t, orgID, true, true) + + grantedProjectResp := createGrantedProject(iamOwnerCtx, instancePermissionV2, t, projectResp) + response.Projects[0] = createProject(iamOwnerCtx, instancePermissionV2, t, *grantedProjectResp.GrantedOrganizationId, true, true) + request.Filters[0].Filter = &project.ProjectSearchFilter_ProjectResourceOwnerFilter{ + ProjectResourceOwnerFilter: &filter.IDFilter{Id: *grantedProjectResp.GrantedOrganizationId}, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Projects: []*project.Project{ + {}, + }, + }, + }, + { + name: "list multiple id, limited permissions", + args: args{ + ctx: instancePermissionV2.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + orgResp := instancePermissionV2.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + resp1 := createProject(iamOwnerCtx, instancePermissionV2, t, orgResp.GetOrganizationId(), false, false) + resp2 := createProject(iamOwnerCtx, instancePermissionV2, t, orgID, true, false) + resp3 := createProject(iamOwnerCtx, instancePermissionV2, t, orgResp.GetOrganizationId(), false, true) + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{resp1.GetId(), resp2.GetId(), resp3.GetId()}, + }, + } + + response.Projects[0] = resp2 + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + Projects: []*project.Project{ + {}, + }, + }, + }, + { + name: "list granted project, project id", + args: args{ + ctx: instancePermissionV2.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + orgID := instancePermissionV2.DefaultOrg.GetId() + + orgName := gofakeit.AppName() + projectName := gofakeit.AppName() + orgResp := instancePermissionV2.CreateOrganization(iamOwnerCtx, orgName, gofakeit.Email()) + projectResp := instancePermissionV2.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), projectName, true, true) + projectGrantResp := instancePermissionV2.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), orgID) + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{Ids: []string{projectResp.GetId()}}, + } + + response.Projects[0] = &project.Project{ + Id: projectResp.GetId(), + Name: projectName, + OrganizationId: orgResp.GetOrganizationId(), + CreationDate: projectGrantResp.GetCreationDate(), + ChangeDate: projectGrantResp.GetCreationDate(), + State: 1, + ProjectRoleAssertion: false, + ProjectAccessRequired: true, + AuthorizationRequired: true, + PrivateLabelingSetting: project.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_UNSPECIFIED, + GrantedOrganizationId: gu.Ptr(orgID), + GrantedOrganizationName: gu.Ptr(instancePermissionV2.DefaultOrg.GetName()), + GrantedState: 1, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 2, + AppliedLimit: 100, + }, + Projects: []*project.Project{{}}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.args.dep != nil { + tt.args.dep(tt.args.req, tt.want) + } + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(iamOwnerCtx, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, listErr := instancePermissionV2.Client.Projectv2Beta.ListProjects(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(ttt, listErr) + return + } + require.NoError(ttt, listErr) + + // always first check length, otherwise its failed anyway + if assert.Len(ttt, got.Projects, len(tt.want.Projects)) { + for i := range tt.want.Projects { + assert.EqualExportedValues(ttt, tt.want.Projects[i], got.Projects[i]) + } + } + assertPaginationResponse(ttt, tt.want.Pagination, got.Pagination) + }, retryDuration, tick, "timeout waiting for expected execution result") + }) + } +} + +func createProject(ctx context.Context, instance *integration.Instance, t *testing.T, orgID string, projectRoleCheck, hasProjectCheck bool) *project.Project { + name := gofakeit.AppName() + resp := instance.CreateProject(ctx, t, orgID, name, projectRoleCheck, hasProjectCheck) + return &project.Project{ + Id: resp.GetId(), + Name: name, + OrganizationId: orgID, + CreationDate: resp.GetCreationDate(), + ChangeDate: resp.GetCreationDate(), + State: 1, + ProjectRoleAssertion: false, + ProjectAccessRequired: hasProjectCheck, + AuthorizationRequired: projectRoleCheck, + PrivateLabelingSetting: project.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_UNSPECIFIED, + } +} + +func createGrantedProject(ctx context.Context, instance *integration.Instance, t *testing.T, projectToGrant *project.Project) *project.Project { + grantedOrgName := gofakeit.AppName() + grantedOrg := instance.CreateOrganization(ctx, grantedOrgName, gofakeit.Email()) + projectGrantResp := instance.CreateProjectGrant(ctx, t, projectToGrant.GetId(), grantedOrg.GetOrganizationId()) + + return &project.Project{ + Id: projectToGrant.GetId(), + Name: projectToGrant.GetName(), + OrganizationId: projectToGrant.GetOrganizationId(), + CreationDate: projectGrantResp.GetCreationDate(), + ChangeDate: projectGrantResp.GetCreationDate(), + State: 1, + ProjectRoleAssertion: false, + ProjectAccessRequired: projectToGrant.GetProjectAccessRequired(), + AuthorizationRequired: projectToGrant.GetAuthorizationRequired(), + PrivateLabelingSetting: project.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_UNSPECIFIED, + GrantedOrganizationId: gu.Ptr(grantedOrg.GetOrganizationId()), + GrantedOrganizationName: gu.Ptr(grantedOrgName), + GrantedState: 1, + } +} + +func assertPaginationResponse(t *assert.CollectT, expected *filter.PaginationResponse, actual *filter.PaginationResponse) { + assert.Equal(t, expected.AppliedLimit, actual.AppliedLimit) + assert.Equal(t, expected.TotalResult, actual.TotalResult) +} + +func TestServer_ListProjectGrants(t *testing.T) { + iamOwnerCtx := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + + userResp := instance.CreateMachineUser(iamOwnerCtx) + patResp := instance.CreatePersonalAccessToken(iamOwnerCtx, userResp.GetUserId()) + projectResp := createProject(iamOwnerCtx, instance, t, instance.DefaultOrg.GetId(), false, false) + projectGrantResp := createProjectGrant(iamOwnerCtx, instance, t, instance.DefaultOrg.GetId(), projectResp.GetId(), projectResp.GetName()) + instance.CreateProjectGrantMembership(t, iamOwnerCtx, projectResp.GetId(), projectGrantResp.GetGrantedOrganizationId(), userResp.GetUserId()) + projectGrantOwnerCtx := integration.WithAuthorizationToken(CTX, patResp.Token) + + type args struct { + ctx context.Context + dep func(*project.ListProjectGrantsRequest, *project.ListProjectGrantsResponse) + req *project.ListProjectGrantsRequest + } + tests := []struct { + name string + args args + want *project.ListProjectGrantsResponse + wantErr bool + }{{ + name: "list by id, unauthenticated", + args: args{ + ctx: CTX, + dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetId()}, + }, + } + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.Filters[1].Filter = &project.ProjectGrantSearchFilter_ProjectGrantResourceOwnerFilter{ + ProjectGrantResourceOwnerFilter: &filter.IDFilter{Id: grantedOrg.GetOrganizationId()}, + } + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + req: &project.ListProjectGrantsRequest{ + Filters: []*project.ProjectGrantSearchFilter{{}, {}}, + }, + }, + wantErr: true, + }, + { + name: "list by id, no permission", + args: args{ + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeNoPermission), + dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetId()}, + }, + } + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.Filters[1].Filter = &project.ProjectGrantSearchFilter_ProjectGrantResourceOwnerFilter{ + ProjectGrantResourceOwnerFilter: &filter.IDFilter{Id: grantedOrg.GetOrganizationId()}, + } + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + req: &project.ListProjectGrantsRequest{ + Filters: []*project.ProjectGrantSearchFilter{{}, {}}, + }, + }, + wantErr: true, + }, + { + name: "list, not found", + args: args{ + ctx: iamOwnerCtx, + req: &project.ListProjectGrantsRequest{ + Filters: []*project.ProjectGrantSearchFilter{ + {Filter: &project.ProjectGrantSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{"notfound"}, + }, + }, + }, + }, + }, + }, + want: &project.ListProjectGrantsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 0, + AppliedLimit: 100, + }, + }, + }, + { + name: "list by id", + args: args{ + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { + name := gofakeit.AppName() + orgID := instance.DefaultOrg.GetId() + projectResp := instance.CreateProject(iamOwnerCtx, t, orgID, name, false, false) + request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetId()}, + }, + } + + response.ProjectGrants[0] = createProjectGrant(iamOwnerCtx, instance, t, orgID, projectResp.GetId(), name) + }, + req: &project.ListProjectGrantsRequest{ + Filters: []*project.ProjectGrantSearchFilter{{}}, + }, + }, + want: &project.ListProjectGrantsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + ProjectGrants: []*project.ProjectGrant{ + {}, + }, + }, + }, + { + name: "list by id, missing permission", + args: args{ + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { + name := gofakeit.AppName() + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name, false, false) + request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetId()}, + }, + } + + createProjectGrant(iamOwnerCtx, instance, t, orgResp.GetOrganizationId(), projectResp.GetId(), name) + }, + req: &project.ListProjectGrantsRequest{ + Filters: []*project.ProjectGrantSearchFilter{{}}, + }, + }, + want: &project.ListProjectGrantsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + ProjectGrants: []*project.ProjectGrant{}, + }, + }, + { + name: "list multiple id", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { + name := gofakeit.AppName() + orgID := instance.DefaultOrg.GetId() + projectResp := instance.CreateProject(iamOwnerCtx, t, orgID, name, false, false) + request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetId()}, + }, + } + + response.ProjectGrants[2] = createProjectGrant(iamOwnerCtx, instance, t, orgID, projectResp.GetId(), name) + response.ProjectGrants[1] = createProjectGrant(iamOwnerCtx, instance, t, orgID, projectResp.GetId(), name) + response.ProjectGrants[0] = createProjectGrant(iamOwnerCtx, instance, t, orgID, projectResp.GetId(), name) + }, + req: &project.ListProjectGrantsRequest{ + Filters: []*project.ProjectGrantSearchFilter{{}}, + }, + }, + want: &project.ListProjectGrantsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + ProjectGrants: []*project.ProjectGrant{ + {}, {}, {}, + }, + }, + }, + { + name: "list multiple id, limited permissions", + args: args{ + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { + name1 := gofakeit.AppName() + name2 := gofakeit.AppName() + name3 := gofakeit.AppName() + orgID := instance.DefaultOrg.GetId() + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + project1Resp := instance.CreateProject(iamOwnerCtx, t, orgID, name1, false, false) + project2Resp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name2, false, false) + project3Resp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name3, false, false) + request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{project1Resp.GetId(), project2Resp.GetId(), project3Resp.GetId()}, + }, + } + + response.ProjectGrants[0] = createProjectGrant(iamOwnerCtx, instance, t, orgID, project1Resp.GetId(), name1) + createProjectGrant(iamOwnerCtx, instance, t, orgResp.GetOrganizationId(), project2Resp.GetId(), name2) + createProjectGrant(iamOwnerCtx, instance, t, orgResp.GetOrganizationId(), project3Resp.GetId(), name3) + }, + req: &project.ListProjectGrantsRequest{ + Filters: []*project.ProjectGrantSearchFilter{{}}, + }, + }, + want: &project.ListProjectGrantsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + ProjectGrants: []*project.ProjectGrant{ + {}, + }, + }, + }, + { + name: "list multiple id, limited permissions, project grant owner", + args: args{ + ctx: projectGrantOwnerCtx, + dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { + name1 := gofakeit.AppName() + name2 := gofakeit.AppName() + name3 := gofakeit.AppName() + orgID := instance.DefaultOrg.GetId() + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + project1Resp := instance.CreateProject(iamOwnerCtx, t, orgID, name1, false, false) + project2Resp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name2, false, false) + project3Resp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name3, false, false) + request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{project1Resp.GetId(), project2Resp.GetId(), project3Resp.GetId(), projectResp.GetId()}}, + } + + createProjectGrant(iamOwnerCtx, instance, t, orgID, project1Resp.GetId(), name1) + createProjectGrant(iamOwnerCtx, instance, t, orgResp.GetOrganizationId(), project2Resp.GetId(), name2) + createProjectGrant(iamOwnerCtx, instance, t, orgResp.GetOrganizationId(), project3Resp.GetId(), name3) + response.ProjectGrants[0] = projectGrantResp + }, + req: &project.ListProjectGrantsRequest{ + Filters: []*project.ProjectGrantSearchFilter{{}}, + }, + }, + want: &project.ListProjectGrantsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 4, + AppliedLimit: 100, + }, + ProjectGrants: []*project.ProjectGrant{ + {}, + }, + }, + }, + { + name: "list single id with role", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { + name := gofakeit.AppName() + orgID := instance.DefaultOrg.GetId() + projectResp := instance.CreateProject(iamOwnerCtx, t, orgID, name, false, false) + request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetId()}, + }, + } + projectRoleResp := addProjectRole(iamOwnerCtx, instance, t, projectResp.GetId()) + response.ProjectGrants[0] = createProjectGrant(iamOwnerCtx, instance, t, orgID, projectResp.GetId(), name, projectRoleResp.Key) + }, + req: &project.ListProjectGrantsRequest{ + Filters: []*project.ProjectGrantSearchFilter{{}}, + }, + }, + want: &project.ListProjectGrantsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + ProjectGrants: []*project.ProjectGrant{ + {}, + }, + }, + }, + { + name: "list single id with removed role", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { + name := gofakeit.AppName() + orgID := instance.DefaultOrg.GetId() + projectResp := instance.CreateProject(iamOwnerCtx, t, orgID, name, false, false) + request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetId()}, + }, + } + projectRoleResp := addProjectRole(iamOwnerCtx, instance, t, projectResp.GetId()) + response.ProjectGrants[0] = createProjectGrant(iamOwnerCtx, instance, t, orgID, projectResp.GetId(), name, projectRoleResp.Key) + + removeRoleResp := instance.RemoveProjectRole(iamOwnerCtx, t, projectResp.GetId(), projectRoleResp.Key) + response.ProjectGrants[0].GrantedRoleKeys = nil + response.ProjectGrants[0].ChangeDate = removeRoleResp.GetRemovalDate() + }, + req: &project.ListProjectGrantsRequest{ + Filters: []*project.ProjectGrantSearchFilter{{}}, + }, + }, + want: &project.ListProjectGrantsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + ProjectGrants: []*project.ProjectGrant{ + {}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.args.dep != nil { + tt.args.dep(tt.args.req, tt.want) + } + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(iamOwnerCtx, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, listErr := instance.Client.Projectv2Beta.ListProjectGrants(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(ttt, listErr, "Error: "+listErr.Error()) + return + } + require.NoError(ttt, listErr) + + // always first check length, otherwise its failed anyway + if assert.Len(ttt, got.ProjectGrants, len(tt.want.ProjectGrants)) { + for i := range tt.want.ProjectGrants { + assert.EqualExportedValues(ttt, tt.want.ProjectGrants[i], got.ProjectGrants[i]) + } + } + assertPaginationResponse(ttt, tt.want.Pagination, got.Pagination) + }, retryDuration, tick, "timeout waiting for expected execution result") + }) + } +} + +func TestServer_ListProjectGrants_PermissionV2(t *testing.T) { + // removed as permission v2 is not implemented yet for project grant level permissions + // ensureFeaturePermissionV2Enabled(t, instancePermissionV2) + iamOwnerCtx := instancePermissionV2.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + + type args struct { + ctx context.Context + dep func(*project.ListProjectGrantsRequest, *project.ListProjectGrantsResponse) + req *project.ListProjectGrantsRequest + } + tests := []struct { + name string + args args + want *project.ListProjectGrantsResponse + wantErr bool + }{ + { + name: "list by id, unauthenticated", + args: args{ + ctx: CTX, + dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { + projectResp := instancePermissionV2.CreateProject(iamOwnerCtx, t, instancePermissionV2.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetId()}, + }, + } + grantedOrg := instancePermissionV2.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.Filters[1].Filter = &project.ProjectGrantSearchFilter_ProjectGrantResourceOwnerFilter{ + ProjectGrantResourceOwnerFilter: &filter.IDFilter{Id: grantedOrg.GetOrganizationId()}, + } + + instancePermissionV2.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + req: &project.ListProjectGrantsRequest{ + Filters: []*project.ProjectGrantSearchFilter{{}, {}}, + }, + }, + wantErr: true, + }, + { + name: "list by id, no permission", + args: args{ + ctx: instancePermissionV2.WithAuthorizationToken(CTX, integration.UserTypeNoPermission), + dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { + projectResp := instancePermissionV2.CreateProject(iamOwnerCtx, t, instancePermissionV2.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetId()}, + }, + } + grantedOrg := instancePermissionV2.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.Filters[1].Filter = &project.ProjectGrantSearchFilter_ProjectGrantResourceOwnerFilter{ + ProjectGrantResourceOwnerFilter: &filter.IDFilter{Id: grantedOrg.GetOrganizationId()}, + } + + instancePermissionV2.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + req: &project.ListProjectGrantsRequest{ + Filters: []*project.ProjectGrantSearchFilter{{}, {}}, + }, + }, + wantErr: true, + }, + { + name: "list by id", + args: args{ + ctx: instancePermissionV2.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { + name := gofakeit.AppName() + orgID := instancePermissionV2.DefaultOrg.GetId() + projectResp := instancePermissionV2.CreateProject(iamOwnerCtx, t, orgID, name, false, false) + request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetId()}, + }, + } + + response.ProjectGrants[0] = createProjectGrant(iamOwnerCtx, instancePermissionV2, t, orgID, projectResp.GetId(), name) + }, + req: &project.ListProjectGrantsRequest{ + Filters: []*project.ProjectGrantSearchFilter{{}}, + }, + }, + want: &project.ListProjectGrantsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + ProjectGrants: []*project.ProjectGrant{ + {}, + }, + }, + }, + { + name: "list by id, missing permission", + args: args{ + ctx: instancePermissionV2.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { + name := gofakeit.AppName() + orgResp := instancePermissionV2.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + projectResp := instancePermissionV2.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name, false, false) + request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetId()}, + }, + } + + createProjectGrant(iamOwnerCtx, instancePermissionV2, t, orgResp.GetOrganizationId(), projectResp.GetId(), name) + }, + req: &project.ListProjectGrantsRequest{ + Filters: []*project.ProjectGrantSearchFilter{{}}, + }, + }, + want: &project.ListProjectGrantsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + ProjectGrants: []*project.ProjectGrant{}, + }, + }, + { + name: "list multiple id", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { + name := gofakeit.AppName() + orgID := instancePermissionV2.DefaultOrg.GetId() + projectResp := instancePermissionV2.CreateProject(iamOwnerCtx, t, orgID, name, false, false) + request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetId()}, + }, + } + + response.ProjectGrants[2] = createProjectGrant(iamOwnerCtx, instancePermissionV2, t, orgID, projectResp.GetId(), name) + response.ProjectGrants[1] = createProjectGrant(iamOwnerCtx, instancePermissionV2, t, orgID, projectResp.GetId(), name) + response.ProjectGrants[0] = createProjectGrant(iamOwnerCtx, instancePermissionV2, t, orgID, projectResp.GetId(), name) + }, + req: &project.ListProjectGrantsRequest{ + Filters: []*project.ProjectGrantSearchFilter{{}}, + }, + }, + want: &project.ListProjectGrantsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + ProjectGrants: []*project.ProjectGrant{ + {}, {}, {}, + }, + }, + }, + { + name: "list multiple id, limited permissions", + args: args{ + ctx: instancePermissionV2.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { + name1 := gofakeit.AppName() + name2 := gofakeit.AppName() + name3 := gofakeit.AppName() + orgID := instancePermissionV2.DefaultOrg.GetId() + orgResp := instancePermissionV2.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + project1Resp := instancePermissionV2.CreateProject(iamOwnerCtx, t, orgID, name1, false, false) + project2Resp := instancePermissionV2.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name2, false, false) + project3Resp := instancePermissionV2.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name3, false, false) + request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{project1Resp.GetId(), project2Resp.GetId(), project3Resp.GetId()}, + }, + } + + response.ProjectGrants[0] = createProjectGrant(iamOwnerCtx, instancePermissionV2, t, orgID, project1Resp.GetId(), name1) + createProjectGrant(iamOwnerCtx, instancePermissionV2, t, orgResp.GetOrganizationId(), project2Resp.GetId(), name2) + createProjectGrant(iamOwnerCtx, instancePermissionV2, t, orgResp.GetOrganizationId(), project3Resp.GetId(), name3) + }, + req: &project.ListProjectGrantsRequest{ + Filters: []*project.ProjectGrantSearchFilter{{}}, + }, + }, + want: &project.ListProjectGrantsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + ProjectGrants: []*project.ProjectGrant{ + {}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.args.dep != nil { + tt.args.dep(tt.args.req, tt.want) + } + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(iamOwnerCtx, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, listErr := instancePermissionV2.Client.Projectv2Beta.ListProjectGrants(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(ttt, listErr, "Error: "+listErr.Error()) + return + } + require.NoError(ttt, listErr) + + // always first check length, otherwise its failed anyway + if assert.Len(ttt, got.ProjectGrants, len(tt.want.ProjectGrants)) { + for i := range tt.want.ProjectGrants { + assert.EqualExportedValues(ttt, tt.want.ProjectGrants[i], got.ProjectGrants[i]) + } + } + assertPaginationResponse(ttt, tt.want.Pagination, got.Pagination) + }, retryDuration, tick, "timeout waiting for expected execution result") + }) + } +} + +func createProjectGrant(ctx context.Context, instance *integration.Instance, t *testing.T, orgID, projectID, projectName string, roles ...string) *project.ProjectGrant { + grantedOrgName := gofakeit.AppName() + grantedOrg := instance.CreateOrganization(ctx, grantedOrgName, gofakeit.Email()) + projectGrantResp := instance.CreateProjectGrant(ctx, t, projectID, grantedOrg.GetOrganizationId(), roles...) + + return &project.ProjectGrant{ + OrganizationId: orgID, + CreationDate: projectGrantResp.GetCreationDate(), + ChangeDate: projectGrantResp.GetCreationDate(), + GrantedOrganizationId: grantedOrg.GetOrganizationId(), + GrantedOrganizationName: grantedOrgName, + ProjectId: projectID, + ProjectName: projectName, + State: 1, + GrantedRoleKeys: roles, + } +} + +func TestServer_ListProjectRoles(t *testing.T) { + iamOwnerCtx := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + type args struct { + ctx context.Context + dep func(*project.ListProjectRolesRequest, *project.ListProjectRolesResponse) + req *project.ListProjectRolesRequest + } + tests := []struct { + name string + args args + want *project.ListProjectRolesResponse + wantErr bool + }{ + { + name: "list by id, unauthenticated", + args: args{ + ctx: CTX, + dep: func(request *project.ListProjectRolesRequest, response *project.ListProjectRolesResponse) { + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + + request.ProjectId = projectResp.GetId() + addProjectRole(iamOwnerCtx, instance, t, projectResp.GetId()) + }, + req: &project.ListProjectRolesRequest{ + Filters: []*project.ProjectRoleSearchFilter{{}}, + }, + }, + wantErr: true, + }, + { + name: "list by id, no permission", + args: args{ + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeNoPermission), + dep: func(request *project.ListProjectRolesRequest, response *project.ListProjectRolesResponse) { + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + + request.ProjectId = projectResp.GetId() + addProjectRole(iamOwnerCtx, instance, t, projectResp.GetId()) + }, + req: &project.ListProjectRolesRequest{ + Filters: []*project.ProjectRoleSearchFilter{{}}, + }, + }, + wantErr: true, + }, + { + name: "list, not found", + args: args{ + ctx: iamOwnerCtx, + req: &project.ListProjectRolesRequest{ + ProjectId: "notfound", + }, + }, + want: &project.ListProjectRolesResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 0, + AppliedLimit: 100, + }, + }, + }, + { + name: "list single id, missing permission", + args: args{ + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.ListProjectRolesRequest, response *project.ListProjectRolesResponse) { + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + + request.ProjectId = projectResp.GetId() + addProjectRole(iamOwnerCtx, instance, t, projectResp.GetId()) + }, + req: &project.ListProjectRolesRequest{}, + }, + want: &project.ListProjectRolesResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + ProjectRoles: []*project.ProjectRole{}, + }, + }, + { + name: "list single id", + args: args{ + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.ListProjectRolesRequest, response *project.ListProjectRolesResponse) { + orgID := instance.DefaultOrg.GetId() + projectResp := instance.CreateProject(iamOwnerCtx, t, orgID, gofakeit.AppName(), false, false) + + request.ProjectId = projectResp.GetId() + response.ProjectRoles[0] = addProjectRole(iamOwnerCtx, instance, t, projectResp.GetId()) + }, + req: &project.ListProjectRolesRequest{}, + }, + want: &project.ListProjectRolesResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + ProjectRoles: []*project.ProjectRole{ + {}, + }, + }, + }, + { + name: "list multiple id", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectRolesRequest, response *project.ListProjectRolesResponse) { + orgID := instance.DefaultOrg.GetId() + projectResp := instance.CreateProject(iamOwnerCtx, t, orgID, gofakeit.AppName(), false, false) + + request.ProjectId = projectResp.GetId() + response.ProjectRoles[2] = addProjectRole(iamOwnerCtx, instance, t, projectResp.GetId()) + response.ProjectRoles[1] = addProjectRole(iamOwnerCtx, instance, t, projectResp.GetId()) + response.ProjectRoles[0] = addProjectRole(iamOwnerCtx, instance, t, projectResp.GetId()) + }, + req: &project.ListProjectRolesRequest{}, + }, + want: &project.ListProjectRolesResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + ProjectRoles: []*project.ProjectRole{ + {}, {}, {}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.args.dep != nil { + tt.args.dep(tt.args.req, tt.want) + } + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(iamOwnerCtx, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, listErr := instance.Client.Projectv2Beta.ListProjectRoles(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(ttt, listErr, "Error: "+listErr.Error()) + return + } + require.NoError(ttt, listErr) + + // always first check length, otherwise its failed anyway + if assert.Len(ttt, got.ProjectRoles, len(tt.want.ProjectRoles)) { + for i := range tt.want.ProjectRoles { + assert.EqualExportedValues(ttt, tt.want.ProjectRoles[i], got.ProjectRoles[i]) + } + } + assertPaginationResponse(ttt, tt.want.Pagination, got.Pagination) + }, retryDuration, tick, "timeout waiting for expected execution result") + }) + } +} + +func TestServer_ListProjectRoles_PermissionV2(t *testing.T) { + ensureFeaturePermissionV2Enabled(t, instancePermissionV2) + iamOwnerCtx := instancePermissionV2.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + + type args struct { + ctx context.Context + dep func(*project.ListProjectRolesRequest, *project.ListProjectRolesResponse) + req *project.ListProjectRolesRequest + } + tests := []struct { + name string + args args + want *project.ListProjectRolesResponse + wantErr bool + }{ + { + name: "list by id, unauthenticated", + args: args{ + ctx: CTX, + dep: func(request *project.ListProjectRolesRequest, response *project.ListProjectRolesResponse) { + projectResp := instancePermissionV2.CreateProject(iamOwnerCtx, t, instancePermissionV2.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + + request.ProjectId = projectResp.GetId() + addProjectRole(iamOwnerCtx, instancePermissionV2, t, projectResp.GetId()) + }, + req: &project.ListProjectRolesRequest{ + Filters: []*project.ProjectRoleSearchFilter{{}}, + }, + }, + wantErr: true, + }, + { + name: "list by id, no permission", + args: args{ + ctx: instancePermissionV2.WithAuthorizationToken(CTX, integration.UserTypeNoPermission), + dep: func(request *project.ListProjectRolesRequest, response *project.ListProjectRolesResponse) { + projectResp := instancePermissionV2.CreateProject(iamOwnerCtx, t, instancePermissionV2.DefaultOrg.GetId(), gofakeit.AppName(), false, false) + + request.ProjectId = projectResp.GetId() + addProjectRole(iamOwnerCtx, instancePermissionV2, t, projectResp.GetId()) + }, + req: &project.ListProjectRolesRequest{ + Filters: []*project.ProjectRoleSearchFilter{{}}, + }, + }, + wantErr: true, + }, + { + name: "list, not found", + args: args{ + ctx: iamOwnerCtx, + req: &project.ListProjectRolesRequest{ + ProjectId: "notfound", + }, + }, + want: &project.ListProjectRolesResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 0, + AppliedLimit: 100, + }, + }, + }, + { + name: "list single id, missing permission", + args: args{ + ctx: instancePermissionV2.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.ListProjectRolesRequest, response *project.ListProjectRolesResponse) { + orgResp := instancePermissionV2.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + projectResp := instancePermissionV2.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + + request.ProjectId = projectResp.GetId() + addProjectRole(iamOwnerCtx, instancePermissionV2, t, projectResp.GetId()) + }, + req: &project.ListProjectRolesRequest{}, + }, + want: &project.ListProjectRolesResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 0, + AppliedLimit: 100, + }, + ProjectRoles: []*project.ProjectRole{}, + }, + }, + { + name: "list single id", + args: args{ + ctx: instancePermissionV2.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.ListProjectRolesRequest, response *project.ListProjectRolesResponse) { + orgID := instancePermissionV2.DefaultOrg.GetId() + projectResp := instancePermissionV2.CreateProject(iamOwnerCtx, t, orgID, gofakeit.AppName(), false, false) + + request.ProjectId = projectResp.GetId() + response.ProjectRoles[0] = addProjectRole(iamOwnerCtx, instancePermissionV2, t, projectResp.GetId()) + }, + req: &project.ListProjectRolesRequest{}, + }, + want: &project.ListProjectRolesResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + ProjectRoles: []*project.ProjectRole{ + {}, + }, + }, + }, + { + name: "list multiple id", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectRolesRequest, response *project.ListProjectRolesResponse) { + orgID := instancePermissionV2.DefaultOrg.GetId() + projectResp := instancePermissionV2.CreateProject(iamOwnerCtx, t, orgID, gofakeit.AppName(), false, false) + + request.ProjectId = projectResp.GetId() + response.ProjectRoles[2] = addProjectRole(iamOwnerCtx, instancePermissionV2, t, projectResp.GetId()) + response.ProjectRoles[1] = addProjectRole(iamOwnerCtx, instancePermissionV2, t, projectResp.GetId()) + response.ProjectRoles[0] = addProjectRole(iamOwnerCtx, instancePermissionV2, t, projectResp.GetId()) + }, + req: &project.ListProjectRolesRequest{}, + }, + want: &project.ListProjectRolesResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + ProjectRoles: []*project.ProjectRole{ + {}, {}, {}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.args.dep != nil { + tt.args.dep(tt.args.req, tt.want) + } + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(iamOwnerCtx, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, listErr := instancePermissionV2.Client.Projectv2Beta.ListProjectRoles(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(ttt, listErr, "Error: "+listErr.Error()) + return + } + require.NoError(ttt, listErr) + + // always first check length, otherwise its failed anyway + if assert.Len(ttt, got.ProjectRoles, len(tt.want.ProjectRoles)) { + for i := range tt.want.ProjectRoles { + assert.EqualExportedValues(ttt, tt.want.ProjectRoles[i], got.ProjectRoles[i]) + } + } + assertPaginationResponse(ttt, tt.want.Pagination, got.Pagination) + }, retryDuration, tick, "timeout waiting for expected execution result") + }) + } +} + +func addProjectRole(ctx context.Context, instance *integration.Instance, t *testing.T, projectID string) *project.ProjectRole { + name := gofakeit.Animal() + projectRoleResp := instance.AddProjectRole(ctx, t, projectID, name, name, name) + + return &project.ProjectRole{ + ProjectId: projectID, + CreationDate: projectRoleResp.GetCreationDate(), + ChangeDate: projectRoleResp.GetCreationDate(), + Key: name, + DisplayName: name, + Group: name, + } +} diff --git a/internal/api/grpc/project/v2beta/integration_test/server_test.go b/internal/api/grpc/project/v2beta/integration_test/server_test.go new file mode 100644 index 0000000000..59d9745222 --- /dev/null +++ b/internal/api/grpc/project/v2beta/integration_test/server_test.go @@ -0,0 +1,63 @@ +//go:build integration + +package project_test + +import ( + "context" + "os" + "testing" + "time" + + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/internal/integration" + "github.com/zitadel/zitadel/pkg/grpc/feature/v2" +) + +var ( + CTX context.Context + instance *integration.Instance + instancePermissionV2 *integration.Instance +) + +func TestMain(m *testing.M) { + os.Exit(func() int { + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) + defer cancel() + CTX = ctx + instance = integration.NewInstance(ctx) + instancePermissionV2 = integration.NewInstance(CTX) + return m.Run() + }()) +} + +func ensureFeaturePermissionV2Enabled(t *testing.T, instance *integration.Instance) { + ctx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + f, err := instance.Client.FeatureV2.GetInstanceFeatures(ctx, &feature.GetInstanceFeaturesRequest{ + Inheritance: true, + }) + require.NoError(t, err) + if f.PermissionCheckV2.GetEnabled() { + return + } + _, err = instance.Client.FeatureV2.SetInstanceFeatures(ctx, &feature.SetInstanceFeaturesRequest{ + PermissionCheckV2: gu.Ptr(true), + }) + require.NoError(t, err) + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, 5*time.Minute) + require.EventuallyWithT(t, + func(ttt *assert.CollectT) { + f, err := instance.Client.FeatureV2.GetInstanceFeatures(ctx, &feature.GetInstanceFeaturesRequest{ + Inheritance: true, + }) + assert.NoError(ttt, err) + if f.PermissionCheckV2.GetEnabled() { + return + } + }, + retryDuration, + tick, + "timed out waiting for ensuring instance feature") +} diff --git a/internal/api/grpc/project/v2beta/project.go b/internal/api/grpc/project/v2beta/project.go new file mode 100644 index 0000000000..95f08ea61e --- /dev/null +++ b/internal/api/grpc/project/v2beta/project.go @@ -0,0 +1,162 @@ +package project + +import ( + "context" + + "connectrpc.com/connect" + "github.com/muhlemmer/gu" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore/v1/models" + "github.com/zitadel/zitadel/internal/query" + project_pb "github.com/zitadel/zitadel/pkg/grpc/project/v2beta" +) + +func (s *Server) CreateProject(ctx context.Context, req *connect.Request[project_pb.CreateProjectRequest]) (*connect.Response[project_pb.CreateProjectResponse], error) { + add := projectCreateToCommand(req.Msg) + project, err := s.command.AddProject(ctx, add) + if err != nil { + return nil, err + } + var creationDate *timestamppb.Timestamp + if !project.EventDate.IsZero() { + creationDate = timestamppb.New(project.EventDate) + } + return connect.NewResponse(&project_pb.CreateProjectResponse{ + Id: add.AggregateID, + CreationDate: creationDate, + }), nil +} + +func projectCreateToCommand(req *project_pb.CreateProjectRequest) *command.AddProject { + var aggregateID string + if req.Id != nil { + aggregateID = *req.Id + } + return &command.AddProject{ + ObjectRoot: models.ObjectRoot{ + ResourceOwner: req.OrganizationId, + AggregateID: aggregateID, + }, + Name: req.Name, + ProjectRoleAssertion: req.ProjectRoleAssertion, + ProjectRoleCheck: req.AuthorizationRequired, + HasProjectCheck: req.ProjectAccessRequired, + PrivateLabelingSetting: privateLabelingSettingToDomain(req.PrivateLabelingSetting), + } +} + +func privateLabelingSettingToDomain(setting project_pb.PrivateLabelingSetting) domain.PrivateLabelingSetting { + switch setting { + case project_pb.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_ALLOW_LOGIN_USER_RESOURCE_OWNER_POLICY: + return domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy + case project_pb.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_ENFORCE_PROJECT_RESOURCE_OWNER_POLICY: + return domain.PrivateLabelingSettingEnforceProjectResourceOwnerPolicy + case project_pb.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_UNSPECIFIED: + return domain.PrivateLabelingSettingUnspecified + default: + return domain.PrivateLabelingSettingUnspecified + } +} + +func (s *Server) UpdateProject(ctx context.Context, req *connect.Request[project_pb.UpdateProjectRequest]) (*connect.Response[project_pb.UpdateProjectResponse], error) { + project, err := s.command.ChangeProject(ctx, projectUpdateToCommand(req.Msg)) + if err != nil { + return nil, err + } + var changeDate *timestamppb.Timestamp + if !project.EventDate.IsZero() { + changeDate = timestamppb.New(project.EventDate) + } + return connect.NewResponse(&project_pb.UpdateProjectResponse{ + ChangeDate: changeDate, + }), nil +} + +func projectUpdateToCommand(req *project_pb.UpdateProjectRequest) *command.ChangeProject { + var labeling *domain.PrivateLabelingSetting + if req.PrivateLabelingSetting != nil { + labeling = gu.Ptr(privateLabelingSettingToDomain(*req.PrivateLabelingSetting)) + } + return &command.ChangeProject{ + ObjectRoot: models.ObjectRoot{ + AggregateID: req.Id, + }, + Name: req.Name, + ProjectRoleAssertion: req.ProjectRoleAssertion, + ProjectRoleCheck: req.ProjectRoleCheck, + HasProjectCheck: req.HasProjectCheck, + PrivateLabelingSetting: labeling, + } +} + +func (s *Server) DeleteProject(ctx context.Context, req *connect.Request[project_pb.DeleteProjectRequest]) (*connect.Response[project_pb.DeleteProjectResponse], error) { + userGrantIDs, err := s.userGrantsFromProject(ctx, req.Msg.GetId()) + if err != nil { + return nil, err + } + + deletedAt, err := s.command.DeleteProject(ctx, req.Msg.GetId(), "", userGrantIDs...) + if err != nil { + return nil, err + } + var deletionDate *timestamppb.Timestamp + if !deletedAt.IsZero() { + deletionDate = timestamppb.New(deletedAt) + } + return connect.NewResponse(&project_pb.DeleteProjectResponse{ + DeletionDate: deletionDate, + }), nil +} + +func (s *Server) userGrantsFromProject(ctx context.Context, projectID string) ([]string, error) { + projectQuery, err := query.NewUserGrantProjectIDSearchQuery(projectID) + if err != nil { + return nil, err + } + userGrants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{ + Queries: []query.SearchQuery{projectQuery}, + }, false, nil) + if err != nil { + return nil, err + } + return userGrantsToIDs(userGrants.UserGrants), nil +} + +func (s *Server) DeactivateProject(ctx context.Context, req *connect.Request[project_pb.DeactivateProjectRequest]) (*connect.Response[project_pb.DeactivateProjectResponse], error) { + details, err := s.command.DeactivateProject(ctx, req.Msg.GetId(), "") + if err != nil { + return nil, err + } + var changeDate *timestamppb.Timestamp + if !details.EventDate.IsZero() { + changeDate = timestamppb.New(details.EventDate) + } + return connect.NewResponse(&project_pb.DeactivateProjectResponse{ + ChangeDate: changeDate, + }), nil +} + +func (s *Server) ActivateProject(ctx context.Context, req *connect.Request[project_pb.ActivateProjectRequest]) (*connect.Response[project_pb.ActivateProjectResponse], error) { + details, err := s.command.ReactivateProject(ctx, req.Msg.GetId(), "") + if err != nil { + return nil, err + } + var changeDate *timestamppb.Timestamp + if !details.EventDate.IsZero() { + changeDate = timestamppb.New(details.EventDate) + } + return connect.NewResponse(&project_pb.ActivateProjectResponse{ + ChangeDate: changeDate, + }), nil +} + +func userGrantsToIDs(userGrants []*query.UserGrant) []string { + converted := make([]string, len(userGrants)) + for i, grant := range userGrants { + converted[i] = grant.ID + } + return converted +} diff --git a/internal/api/grpc/project/v2beta/project_grant.go b/internal/api/grpc/project/v2beta/project_grant.go new file mode 100644 index 0000000000..c2ccef1751 --- /dev/null +++ b/internal/api/grpc/project/v2beta/project_grant.go @@ -0,0 +1,127 @@ +package project + +import ( + "context" + + "connectrpc.com/connect" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/eventstore/v1/models" + "github.com/zitadel/zitadel/internal/query" + project_pb "github.com/zitadel/zitadel/pkg/grpc/project/v2beta" +) + +func (s *Server) CreateProjectGrant(ctx context.Context, req *connect.Request[project_pb.CreateProjectGrantRequest]) (*connect.Response[project_pb.CreateProjectGrantResponse], error) { + add := projectGrantCreateToCommand(req.Msg) + project, err := s.command.AddProjectGrant(ctx, add) + if err != nil { + return nil, err + } + var creationDate *timestamppb.Timestamp + if !project.EventDate.IsZero() { + creationDate = timestamppb.New(project.EventDate) + } + return connect.NewResponse(&project_pb.CreateProjectGrantResponse{ + CreationDate: creationDate, + }), nil +} + +func projectGrantCreateToCommand(req *project_pb.CreateProjectGrantRequest) *command.AddProjectGrant { + return &command.AddProjectGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: req.ProjectId, + }, + GrantID: req.GrantedOrganizationId, + GrantedOrgID: req.GrantedOrganizationId, + RoleKeys: req.RoleKeys, + } +} + +func (s *Server) UpdateProjectGrant(ctx context.Context, req *connect.Request[project_pb.UpdateProjectGrantRequest]) (*connect.Response[project_pb.UpdateProjectGrantResponse], error) { + project, err := s.command.ChangeProjectGrant(ctx, projectGrantUpdateToCommand(req.Msg)) + if err != nil { + return nil, err + } + var changeDate *timestamppb.Timestamp + if !project.EventDate.IsZero() { + changeDate = timestamppb.New(project.EventDate) + } + return connect.NewResponse(&project_pb.UpdateProjectGrantResponse{ + ChangeDate: changeDate, + }), nil +} + +func projectGrantUpdateToCommand(req *project_pb.UpdateProjectGrantRequest) *command.ChangeProjectGrant { + return &command.ChangeProjectGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: req.ProjectId, + }, + GrantedOrgID: req.GrantedOrganizationId, + RoleKeys: req.RoleKeys, + } +} + +func (s *Server) DeactivateProjectGrant(ctx context.Context, req *connect.Request[project_pb.DeactivateProjectGrantRequest]) (*connect.Response[project_pb.DeactivateProjectGrantResponse], error) { + details, err := s.command.DeactivateProjectGrant(ctx, req.Msg.GetProjectId(), "", req.Msg.GetGrantedOrganizationId(), "") + if err != nil { + return nil, err + } + var changeDate *timestamppb.Timestamp + if !details.EventDate.IsZero() { + changeDate = timestamppb.New(details.EventDate) + } + return connect.NewResponse(&project_pb.DeactivateProjectGrantResponse{ + ChangeDate: changeDate, + }), nil +} + +func (s *Server) ActivateProjectGrant(ctx context.Context, req *connect.Request[project_pb.ActivateProjectGrantRequest]) (*connect.Response[project_pb.ActivateProjectGrantResponse], error) { + details, err := s.command.ReactivateProjectGrant(ctx, req.Msg.GetProjectId(), "", req.Msg.GetGrantedOrganizationId(), "") + if err != nil { + return nil, err + } + var changeDate *timestamppb.Timestamp + if !details.EventDate.IsZero() { + changeDate = timestamppb.New(details.EventDate) + } + return connect.NewResponse(&project_pb.ActivateProjectGrantResponse{ + ChangeDate: changeDate, + }), nil +} + +func (s *Server) DeleteProjectGrant(ctx context.Context, req *connect.Request[project_pb.DeleteProjectGrantRequest]) (*connect.Response[project_pb.DeleteProjectGrantResponse], error) { + userGrantIDs, err := s.userGrantsFromProjectGrant(ctx, req.Msg.GetProjectId(), req.Msg.GetGrantedOrganizationId()) + if err != nil { + return nil, err + } + details, err := s.command.DeleteProjectGrant(ctx, req.Msg.GetProjectId(), "", req.Msg.GetGrantedOrganizationId(), "", userGrantIDs...) + if err != nil { + return nil, err + } + var deletionDate *timestamppb.Timestamp + if !details.EventDate.IsZero() { + deletionDate = timestamppb.New(details.EventDate) + } + return connect.NewResponse(&project_pb.DeleteProjectGrantResponse{ + DeletionDate: deletionDate, + }), nil +} + +func (s *Server) userGrantsFromProjectGrant(ctx context.Context, projectID, grantedOrganizationID string) ([]string, error) { + projectQuery, err := query.NewUserGrantProjectIDSearchQuery(projectID) + if err != nil { + return nil, err + } + grantQuery, err := query.NewUserGrantGrantIDSearchQuery(grantedOrganizationID) + if err != nil { + return nil, err + } + userGrants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{ + Queries: []query.SearchQuery{projectQuery, grantQuery}, + }, false, nil) + if err != nil { + return nil, err + } + return userGrantsToIDs(userGrants.UserGrants), nil +} diff --git a/internal/api/grpc/project/v2beta/project_role.go b/internal/api/grpc/project/v2beta/project_role.go new file mode 100644 index 0000000000..b7105b38c6 --- /dev/null +++ b/internal/api/grpc/project/v2beta/project_role.go @@ -0,0 +1,133 @@ +package project + +import ( + "context" + + "connectrpc.com/connect" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/eventstore/v1/models" + "github.com/zitadel/zitadel/internal/query" + project_pb "github.com/zitadel/zitadel/pkg/grpc/project/v2beta" +) + +func (s *Server) AddProjectRole(ctx context.Context, req *connect.Request[project_pb.AddProjectRoleRequest]) (*connect.Response[project_pb.AddProjectRoleResponse], error) { + role, err := s.command.AddProjectRole(ctx, addProjectRoleRequestToCommand(req.Msg)) + if err != nil { + return nil, err + } + var creationDate *timestamppb.Timestamp + if !role.EventDate.IsZero() { + creationDate = timestamppb.New(role.EventDate) + } + return connect.NewResponse(&project_pb.AddProjectRoleResponse{ + CreationDate: creationDate, + }), nil +} + +func addProjectRoleRequestToCommand(req *project_pb.AddProjectRoleRequest) *command.AddProjectRole { + group := "" + if req.Group != nil { + group = *req.Group + } + + return &command.AddProjectRole{ + ObjectRoot: models.ObjectRoot{ + AggregateID: req.ProjectId, + }, + Key: req.RoleKey, + DisplayName: req.DisplayName, + Group: group, + } +} + +func (s *Server) UpdateProjectRole(ctx context.Context, req *connect.Request[project_pb.UpdateProjectRoleRequest]) (*connect.Response[project_pb.UpdateProjectRoleResponse], error) { + role, err := s.command.ChangeProjectRole(ctx, updateProjectRoleRequestToCommand(req.Msg)) + if err != nil { + return nil, err + } + var changeDate *timestamppb.Timestamp + if !role.EventDate.IsZero() { + changeDate = timestamppb.New(role.EventDate) + } + return connect.NewResponse(&project_pb.UpdateProjectRoleResponse{ + ChangeDate: changeDate, + }), nil +} + +func updateProjectRoleRequestToCommand(req *project_pb.UpdateProjectRoleRequest) *command.ChangeProjectRole { + displayName := "" + if req.DisplayName != nil { + displayName = *req.DisplayName + } + group := "" + if req.Group != nil { + group = *req.Group + } + + return &command.ChangeProjectRole{ + ObjectRoot: models.ObjectRoot{ + AggregateID: req.ProjectId, + }, + Key: req.RoleKey, + DisplayName: displayName, + Group: group, + } +} + +func (s *Server) RemoveProjectRole(ctx context.Context, req *connect.Request[project_pb.RemoveProjectRoleRequest]) (*connect.Response[project_pb.RemoveProjectRoleResponse], error) { + userGrantIDs, err := s.userGrantsFromProjectAndRole(ctx, req.Msg.GetProjectId(), req.Msg.GetRoleKey()) + if err != nil { + return nil, err + } + projectGrantIDs, err := s.projectGrantsFromProjectAndRole(ctx, req.Msg.GetProjectId(), req.Msg.GetRoleKey()) + if err != nil { + return nil, err + } + details, err := s.command.RemoveProjectRole(ctx, req.Msg.GetProjectId(), req.Msg.GetRoleKey(), "", projectGrantIDs, userGrantIDs...) + if err != nil { + return nil, err + } + var deletionDate *timestamppb.Timestamp + if !details.EventDate.IsZero() { + deletionDate = timestamppb.New(details.EventDate) + } + return connect.NewResponse(&project_pb.RemoveProjectRoleResponse{ + RemovalDate: deletionDate, + }), nil +} + +func (s *Server) userGrantsFromProjectAndRole(ctx context.Context, projectID, roleKey string) ([]string, error) { + projectQuery, err := query.NewUserGrantProjectIDSearchQuery(projectID) + if err != nil { + return nil, err + } + rolesQuery, err := query.NewUserGrantRoleQuery(roleKey) + if err != nil { + return nil, err + } + userGrants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{ + Queries: []query.SearchQuery{projectQuery, rolesQuery}, + }, false, nil) + if err != nil { + return nil, err + } + return userGrantsToIDs(userGrants.UserGrants), nil +} + +func (s *Server) projectGrantsFromProjectAndRole(ctx context.Context, projectID, roleKey string) ([]string, error) { + projectGrants, err := s.query.SearchProjectGrantsByProjectIDAndRoleKey(ctx, projectID, roleKey) + if err != nil { + return nil, err + } + return projectGrantsToIDs(projectGrants), nil +} + +func projectGrantsToIDs(projectGrants *query.ProjectGrants) []string { + converted := make([]string, len(projectGrants.ProjectGrants)) + for i, grant := range projectGrants.ProjectGrants { + converted[i] = grant.GrantID + } + return converted +} diff --git a/internal/api/grpc/project/v2beta/query.go b/internal/api/grpc/project/v2beta/query.go new file mode 100644 index 0000000000..c736c5a086 --- /dev/null +++ b/internal/api/grpc/project/v2beta/query.go @@ -0,0 +1,429 @@ +package project + +import ( + "context" + + "connectrpc.com/connect" + "google.golang.org/protobuf/types/known/timestamppb" + + filter "github.com/zitadel/zitadel/internal/api/grpc/filter/v2beta" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/zerrors" + filter_pb "github.com/zitadel/zitadel/pkg/grpc/filter/v2beta" + project_pb "github.com/zitadel/zitadel/pkg/grpc/project/v2beta" +) + +func (s *Server) GetProject(ctx context.Context, req *connect.Request[project_pb.GetProjectRequest]) (*connect.Response[project_pb.GetProjectResponse], error) { + project, err := s.query.GetProjectByIDWithPermission(ctx, true, req.Msg.GetId(), s.checkPermission) + if err != nil { + return nil, err + } + return connect.NewResponse(&project_pb.GetProjectResponse{ + Project: projectToPb(project), + }), nil +} + +func (s *Server) ListProjects(ctx context.Context, req *connect.Request[project_pb.ListProjectsRequest]) (*connect.Response[project_pb.ListProjectsResponse], error) { + queries, err := s.listProjectRequestToModel(req.Msg) + if err != nil { + return nil, err + } + resp, err := s.query.SearchGrantedProjects(ctx, queries, s.checkPermission) + if err != nil { + return nil, err + } + return connect.NewResponse(&project_pb.ListProjectsResponse{ + Projects: grantedProjectsToPb(resp.GrantedProjects), + Pagination: filter.QueryToPaginationPb(queries.SearchRequest, resp.SearchResponse), + }), nil +} + +func (s *Server) listProjectRequestToModel(req *project_pb.ListProjectsRequest) (*query.ProjectAndGrantedProjectSearchQueries, error) { + offset, limit, asc, err := filter.PaginationPbToQuery(s.systemDefaults, req.Pagination) + if err != nil { + return nil, err + } + queries, err := projectFiltersToQuery(req.Filters) + if err != nil { + return nil, err + } + return &query.ProjectAndGrantedProjectSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + SortingColumn: grantedProjectFieldNameToSortingColumn(req.SortingColumn), + }, + Queries: queries, + }, nil +} + +func grantedProjectFieldNameToSortingColumn(field *project_pb.ProjectFieldName) query.Column { + if field == nil { + return query.GrantedProjectColumnCreationDate + } + switch *field { + case project_pb.ProjectFieldName_PROJECT_FIELD_NAME_CREATION_DATE: + return query.GrantedProjectColumnCreationDate + case project_pb.ProjectFieldName_PROJECT_FIELD_NAME_ID: + return query.GrantedProjectColumnID + case project_pb.ProjectFieldName_PROJECT_FIELD_NAME_NAME: + return query.GrantedProjectColumnName + case project_pb.ProjectFieldName_PROJECT_FIELD_NAME_CHANGE_DATE: + return query.GrantedProjectColumnChangeDate + case project_pb.ProjectFieldName_PROJECT_FIELD_NAME_UNSPECIFIED: + return query.GrantedProjectColumnCreationDate + default: + return query.GrantedProjectColumnCreationDate + } +} + +func projectFiltersToQuery(queries []*project_pb.ProjectSearchFilter) (_ []query.SearchQuery, err error) { + q := make([]query.SearchQuery, len(queries)) + for i, qry := range queries { + q[i], err = projectFilterToModel(qry) + if err != nil { + return nil, err + } + } + return q, nil +} + +func projectFilterToModel(filter *project_pb.ProjectSearchFilter) (query.SearchQuery, error) { + switch q := filter.Filter.(type) { + case *project_pb.ProjectSearchFilter_ProjectNameFilter: + return projectNameFilterToQuery(q.ProjectNameFilter) + case *project_pb.ProjectSearchFilter_InProjectIdsFilter: + return projectInIDsFilterToQuery(q.InProjectIdsFilter) + case *project_pb.ProjectSearchFilter_ProjectResourceOwnerFilter: + return projectResourceOwnerFilterToQuery(q.ProjectResourceOwnerFilter) + case *project_pb.ProjectSearchFilter_ProjectOrganizationIdFilter: + return projectOrganizationIDFilterToQuery(q.ProjectOrganizationIdFilter) + case *project_pb.ProjectSearchFilter_ProjectGrantResourceOwnerFilter: + return projectGrantResourceOwnerFilterToQuery(q.ProjectGrantResourceOwnerFilter) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "ORG-vR9nC", "List.Query.Invalid") + } +} + +func projectNameFilterToQuery(q *project_pb.ProjectNameFilter) (query.SearchQuery, error) { + return query.NewGrantedProjectNameSearchQuery(filter.TextMethodPbToQuery(q.Method), q.GetProjectName()) +} + +func projectInIDsFilterToQuery(q *filter_pb.InIDsFilter) (query.SearchQuery, error) { + return query.NewGrantedProjectIDSearchQuery(q.Ids) +} + +func projectResourceOwnerFilterToQuery(q *filter_pb.IDFilter) (query.SearchQuery, error) { + return query.NewGrantedProjectResourceOwnerSearchQuery(q.Id) +} + +func projectOrganizationIDFilterToQuery(q *filter_pb.IDFilter) (query.SearchQuery, error) { + return query.NewGrantedProjectOrganizationIDSearchQuery(q.Id) +} + +func projectGrantResourceOwnerFilterToQuery(q *filter_pb.IDFilter) (query.SearchQuery, error) { + return query.NewGrantedProjectGrantResourceOwnerSearchQuery(q.Id) +} + +func grantedProjectsToPb(projects []*query.GrantedProject) []*project_pb.Project { + o := make([]*project_pb.Project, len(projects)) + for i, org := range projects { + o[i] = grantedProjectToPb(org) + } + return o +} + +func projectToPb(project *query.Project) *project_pb.Project { + return &project_pb.Project{ + Id: project.ID, + OrganizationId: project.ResourceOwner, + CreationDate: timestamppb.New(project.CreationDate), + ChangeDate: timestamppb.New(project.ChangeDate), + State: projectStateToPb(project.State), + Name: project.Name, + PrivateLabelingSetting: privateLabelingSettingToPb(project.PrivateLabelingSetting), + ProjectAccessRequired: project.HasProjectCheck, + ProjectRoleAssertion: project.ProjectRoleAssertion, + AuthorizationRequired: project.ProjectRoleCheck, + } +} + +func grantedProjectToPb(project *query.GrantedProject) *project_pb.Project { + var grantedOrganizationID, grantedOrganizationName *string + if project.GrantedOrgID != "" { + grantedOrganizationID = &project.GrantedOrgID + } + if project.OrgName != "" { + grantedOrganizationName = &project.OrgName + } + + return &project_pb.Project{ + Id: project.ProjectID, + OrganizationId: project.ResourceOwner, + CreationDate: timestamppb.New(project.CreationDate), + ChangeDate: timestamppb.New(project.ChangeDate), + State: projectStateToPb(project.ProjectState), + Name: project.ProjectName, + PrivateLabelingSetting: privateLabelingSettingToPb(project.PrivateLabelingSetting), + ProjectAccessRequired: project.HasProjectCheck, + ProjectRoleAssertion: project.ProjectRoleAssertion, + AuthorizationRequired: project.ProjectRoleCheck, + GrantedOrganizationId: grantedOrganizationID, + GrantedOrganizationName: grantedOrganizationName, + GrantedState: grantedProjectStateToPb(project.ProjectGrantState), + } +} + +func projectStateToPb(state domain.ProjectState) project_pb.ProjectState { + switch state { + case domain.ProjectStateActive: + return project_pb.ProjectState_PROJECT_STATE_ACTIVE + case domain.ProjectStateInactive: + return project_pb.ProjectState_PROJECT_STATE_INACTIVE + case domain.ProjectStateUnspecified, domain.ProjectStateRemoved: + return project_pb.ProjectState_PROJECT_STATE_UNSPECIFIED + default: + return project_pb.ProjectState_PROJECT_STATE_UNSPECIFIED + } +} +func grantedProjectStateToPb(state domain.ProjectGrantState) project_pb.GrantedProjectState { + switch state { + case domain.ProjectGrantStateActive: + return project_pb.GrantedProjectState_GRANTED_PROJECT_STATE_ACTIVE + case domain.ProjectGrantStateInactive: + return project_pb.GrantedProjectState_GRANTED_PROJECT_STATE_INACTIVE + case domain.ProjectGrantStateUnspecified, domain.ProjectGrantStateRemoved: + return project_pb.GrantedProjectState_GRANTED_PROJECT_STATE_UNSPECIFIED + default: + return project_pb.GrantedProjectState_GRANTED_PROJECT_STATE_UNSPECIFIED + } +} + +func privateLabelingSettingToPb(setting domain.PrivateLabelingSetting) project_pb.PrivateLabelingSetting { + switch setting { + case domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy: + return project_pb.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_ALLOW_LOGIN_USER_RESOURCE_OWNER_POLICY + case domain.PrivateLabelingSettingEnforceProjectResourceOwnerPolicy: + return project_pb.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_ENFORCE_PROJECT_RESOURCE_OWNER_POLICY + case domain.PrivateLabelingSettingUnspecified: + return project_pb.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_UNSPECIFIED + default: + return project_pb.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_UNSPECIFIED + } +} + +func (s *Server) ListProjectGrants(ctx context.Context, req *connect.Request[project_pb.ListProjectGrantsRequest]) (*connect.Response[project_pb.ListProjectGrantsResponse], error) { + queries, err := s.listProjectGrantsRequestToModel(req.Msg) + if err != nil { + return nil, err + } + resp, err := s.query.SearchProjectGrants(ctx, queries, s.checkPermission) + if err != nil { + return nil, err + } + return connect.NewResponse(&project_pb.ListProjectGrantsResponse{ + ProjectGrants: projectGrantsToPb(resp.ProjectGrants), + Pagination: filter.QueryToPaginationPb(queries.SearchRequest, resp.SearchResponse), + }), nil +} + +func (s *Server) listProjectGrantsRequestToModel(req *project_pb.ListProjectGrantsRequest) (*query.ProjectGrantSearchQueries, error) { + offset, limit, asc, err := filter.PaginationPbToQuery(s.systemDefaults, req.Pagination) + if err != nil { + return nil, err + } + queries, err := projectGrantFiltersToModel(req.Filters) + if err != nil { + return nil, err + } + return &query.ProjectGrantSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + SortingColumn: projectGrantFieldNameToSortingColumn(req.SortingColumn), + }, + Queries: queries, + }, nil +} + +func projectGrantFieldNameToSortingColumn(field *project_pb.ProjectGrantFieldName) query.Column { + if field == nil { + return query.ProjectGrantColumnCreationDate + } + switch *field { + case project_pb.ProjectGrantFieldName_PROJECT_GRANT_FIELD_NAME_PROJECT_ID: + return query.ProjectGrantColumnProjectID + case project_pb.ProjectGrantFieldName_PROJECT_GRANT_FIELD_NAME_CREATION_DATE: + return query.ProjectGrantColumnCreationDate + case project_pb.ProjectGrantFieldName_PROJECT_GRANT_FIELD_NAME_CHANGE_DATE: + return query.ProjectGrantColumnChangeDate + case project_pb.ProjectGrantFieldName_PROJECT_GRANT_FIELD_NAME_UNSPECIFIED: + return query.ProjectGrantColumnCreationDate + default: + return query.ProjectGrantColumnCreationDate + } +} + +func projectGrantFiltersToModel(queries []*project_pb.ProjectGrantSearchFilter) (_ []query.SearchQuery, err error) { + q := make([]query.SearchQuery, len(queries)) + for i, qry := range queries { + q[i], err = projectGrantFilterToModel(qry) + if err != nil { + return nil, err + } + } + return q, nil +} + +func projectGrantFilterToModel(filter *project_pb.ProjectGrantSearchFilter) (query.SearchQuery, error) { + switch q := filter.Filter.(type) { + case *project_pb.ProjectGrantSearchFilter_ProjectNameFilter: + return projectNameFilterToQuery(q.ProjectNameFilter) + case *project_pb.ProjectGrantSearchFilter_RoleKeyFilter: + return query.NewProjectGrantRoleKeySearchQuery(q.RoleKeyFilter.Key) + case *project_pb.ProjectGrantSearchFilter_InProjectIdsFilter: + return query.NewProjectGrantProjectIDsSearchQuery(q.InProjectIdsFilter.Ids) + case *project_pb.ProjectGrantSearchFilter_ProjectResourceOwnerFilter: + return query.NewProjectGrantResourceOwnerSearchQuery(q.ProjectResourceOwnerFilter.Id) + case *project_pb.ProjectGrantSearchFilter_ProjectGrantResourceOwnerFilter: + return query.NewProjectGrantGrantedOrgIDSearchQuery(q.ProjectGrantResourceOwnerFilter.Id) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-M099f", "List.Query.Invalid") + } +} + +func projectGrantsToPb(projects []*query.ProjectGrant) []*project_pb.ProjectGrant { + p := make([]*project_pb.ProjectGrant, len(projects)) + for i, project := range projects { + p[i] = projectGrantToPb(project) + } + return p +} + +func projectGrantToPb(project *query.ProjectGrant) *project_pb.ProjectGrant { + return &project_pb.ProjectGrant{ + OrganizationId: project.ResourceOwner, + CreationDate: timestamppb.New(project.CreationDate), + ChangeDate: timestamppb.New(project.ChangeDate), + GrantedOrganizationId: project.GrantedOrgID, + GrantedOrganizationName: project.OrgName, + GrantedRoleKeys: project.GrantedRoleKeys, + ProjectId: project.ProjectID, + ProjectName: project.ProjectName, + State: projectGrantStateToPb(project.State), + } +} + +func projectGrantStateToPb(state domain.ProjectGrantState) project_pb.ProjectGrantState { + switch state { + case domain.ProjectGrantStateActive: + return project_pb.ProjectGrantState_PROJECT_GRANT_STATE_ACTIVE + case domain.ProjectGrantStateInactive: + return project_pb.ProjectGrantState_PROJECT_GRANT_STATE_INACTIVE + case domain.ProjectGrantStateUnspecified, domain.ProjectGrantStateRemoved: + return project_pb.ProjectGrantState_PROJECT_GRANT_STATE_UNSPECIFIED + default: + return project_pb.ProjectGrantState_PROJECT_GRANT_STATE_UNSPECIFIED + } +} + +func (s *Server) ListProjectRoles(ctx context.Context, req *connect.Request[project_pb.ListProjectRolesRequest]) (*connect.Response[project_pb.ListProjectRolesResponse], error) { + queries, err := s.listProjectRolesRequestToModel(req.Msg) + if err != nil { + return nil, err + } + err = queries.AppendProjectIDQuery(req.Msg.GetProjectId()) + if err != nil { + return nil, err + } + roles, err := s.query.SearchProjectRoles(ctx, true, queries, s.checkPermission) + if err != nil { + return nil, err + } + return connect.NewResponse(&project_pb.ListProjectRolesResponse{ + ProjectRoles: roleViewsToPb(roles.ProjectRoles), + Pagination: filter.QueryToPaginationPb(queries.SearchRequest, roles.SearchResponse), + }), nil +} + +func (s *Server) listProjectRolesRequestToModel(req *project_pb.ListProjectRolesRequest) (*query.ProjectRoleSearchQueries, error) { + offset, limit, asc, err := filter.PaginationPbToQuery(s.systemDefaults, req.Pagination) + if err != nil { + return nil, err + } + queries, err := roleQueriesToModel(req.Filters) + if err != nil { + return nil, err + } + return &query.ProjectRoleSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + SortingColumn: projectRoleFieldNameToSortingColumn(req.SortingColumn), + }, + Queries: queries, + }, nil +} + +func projectRoleFieldNameToSortingColumn(field *project_pb.ProjectRoleFieldName) query.Column { + if field == nil { + return query.ProjectRoleColumnCreationDate + } + switch *field { + case project_pb.ProjectRoleFieldName_PROJECT_ROLE_FIELD_NAME_KEY: + return query.ProjectRoleColumnKey + case project_pb.ProjectRoleFieldName_PROJECT_ROLE_FIELD_NAME_CREATION_DATE: + return query.ProjectRoleColumnCreationDate + case project_pb.ProjectRoleFieldName_PROJECT_ROLE_FIELD_NAME_CHANGE_DATE: + return query.ProjectRoleColumnChangeDate + case project_pb.ProjectRoleFieldName_PROJECT_ROLE_FIELD_NAME_UNSPECIFIED: + return query.ProjectRoleColumnCreationDate + default: + return query.ProjectRoleColumnCreationDate + } +} + +func roleQueriesToModel(queries []*project_pb.ProjectRoleSearchFilter) (_ []query.SearchQuery, err error) { + q := make([]query.SearchQuery, len(queries)) + for i, query := range queries { + q[i], err = roleQueryToModel(query) + if err != nil { + return nil, err + } + } + return q, nil +} + +func roleQueryToModel(apiQuery *project_pb.ProjectRoleSearchFilter) (query.SearchQuery, error) { + switch q := apiQuery.Filter.(type) { + case *project_pb.ProjectRoleSearchFilter_RoleKeyFilter: + return query.NewProjectRoleKeySearchQuery(filter.TextMethodPbToQuery(q.RoleKeyFilter.Method), q.RoleKeyFilter.Key) + case *project_pb.ProjectRoleSearchFilter_DisplayNameFilter: + return query.NewProjectRoleDisplayNameSearchQuery(filter.TextMethodPbToQuery(q.DisplayNameFilter.Method), q.DisplayNameFilter.DisplayName) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-fms0e", "List.Query.Invalid") + } +} + +func roleViewsToPb(roles []*query.ProjectRole) []*project_pb.ProjectRole { + o := make([]*project_pb.ProjectRole, len(roles)) + for i, org := range roles { + o[i] = roleViewToPb(org) + } + return o +} + +func roleViewToPb(role *query.ProjectRole) *project_pb.ProjectRole { + return &project_pb.ProjectRole{ + ProjectId: role.ProjectID, + Key: role.Key, + CreationDate: timestamppb.New(role.CreationDate), + ChangeDate: timestamppb.New(role.ChangeDate), + DisplayName: role.DisplayName, + Group: role.Group, + } +} diff --git a/internal/api/grpc/project/v2beta/server.go b/internal/api/grpc/project/v2beta/server.go new file mode 100644 index 0000000000..12c18ae4c6 --- /dev/null +++ b/internal/api/grpc/project/v2beta/server.go @@ -0,0 +1,62 @@ +package project + +import ( + "net/http" + + "connectrpc.com/connect" + "google.golang.org/protobuf/reflect/protoreflect" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/config/systemdefaults" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/query" + project "github.com/zitadel/zitadel/pkg/grpc/project/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/project/v2beta/projectconnect" +) + +var _ projectconnect.ProjectServiceHandler = (*Server)(nil) + +type Server struct { + systemDefaults systemdefaults.SystemDefaults + command *command.Commands + query *query.Queries + + checkPermission domain.PermissionCheck +} + +type Config struct{} + +func CreateServer( + systemDefaults systemdefaults.SystemDefaults, + command *command.Commands, + query *query.Queries, + checkPermission domain.PermissionCheck, +) *Server { + return &Server{ + systemDefaults: systemDefaults, + command: command, + query: query, + checkPermission: checkPermission, + } +} + +func (s *Server) RegisterConnectServer(interceptors ...connect.Interceptor) (string, http.Handler) { + return projectconnect.NewProjectServiceHandler(s, connect.WithInterceptors(interceptors...)) +} + +func (s *Server) FileDescriptor() protoreflect.FileDescriptor { + return project.File_zitadel_project_v2beta_project_service_proto +} + +func (s *Server) AppName() string { + return project.ProjectService_ServiceDesc.ServiceName +} + +func (s *Server) MethodPrefix() string { + return project.ProjectService_ServiceDesc.ServiceName +} + +func (s *Server) AuthMethods() authz.MethodMapping { + return project.ProjectService_AuthMethods +} diff --git a/internal/api/grpc/saml/v2/integration/saml_test.go b/internal/api/grpc/saml/v2/integration_test/saml_test.go similarity index 97% rename from internal/api/grpc/saml/v2/integration/saml_test.go rename to internal/api/grpc/saml/v2/integration_test/saml_test.go index fbfdae5aab..241c20715c 100644 --- a/internal/api/grpc/saml/v2/integration/saml_test.go +++ b/internal/api/grpc/saml/v2/integration_test/saml_test.go @@ -378,7 +378,7 @@ func TestServer_CreateResponse_Permission(t *testing.T) { projectID2, _, _ := createSAMLApplication(ctx, t, idpMetadata, saml.HTTPRedirectBinding, true, true) orgResp := Instance.CreateOrganization(ctx, "saml-permission-"+gofakeit.AppName(), gofakeit.Email()) - Instance.CreateProjectGrant(ctx, projectID2, orgResp.GetOrganizationId()) + Instance.CreateProjectGrant(ctx, t, projectID2, orgResp.GetOrganizationId()) user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone()) Instance.CreateProjectUserGrant(t, ctx, projectID, user.GetUserId()) @@ -392,7 +392,7 @@ func TestServer_CreateResponse_Permission(t *testing.T) { projectID, _, sp := createSAMLApplication(ctx, t, idpMetadata, saml.HTTPRedirectBinding, true, true) orgResp := Instance.CreateOrganization(ctx, "saml-permission-"+gofakeit.AppName(), gofakeit.Email()) - Instance.CreateProjectGrant(ctx, projectID, orgResp.GetOrganizationId()) + Instance.CreateProjectGrant(ctx, t, projectID, orgResp.GetOrganizationId()) user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone()) Instance.CreateProjectUserGrant(t, ctx, projectID, user.GetUserId()) @@ -414,9 +414,9 @@ func TestServer_CreateResponse_Permission(t *testing.T) { projectID, _, sp := createSAMLApplication(ctx, t, idpMetadata, saml.HTTPRedirectBinding, true, true) orgResp := Instance.CreateOrganization(ctx, "saml-permission-"+gofakeit.AppName(), gofakeit.Email()) - projectGrantResp := Instance.CreateProjectGrant(ctx, projectID, orgResp.GetOrganizationId()) + Instance.CreateProjectGrant(ctx, t, projectID, orgResp.GetOrganizationId()) user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone()) - Instance.CreateProjectGrantUserGrant(ctx, orgResp.GetOrganizationId(), projectID, projectGrantResp.GetGrantId(), user.GetUserId()) + Instance.CreateProjectGrantUserGrant(ctx, orgResp.GetOrganizationId(), projectID, orgResp.GetOrganizationId(), user.GetUserId()) return createSessionAndSmlRequestForCallback(ctx, t, sp, Instance.Users[integration.UserTypeLogin].ID, acsRedirect, user.GetUserId(), saml.HTTPRedirectBinding) }, @@ -548,9 +548,9 @@ func TestServer_CreateResponse_Permission(t *testing.T) { projectID, _, sp := createSAMLApplication(ctx, t, idpMetadata, saml.HTTPRedirectBinding, true, false) orgResp := Instance.CreateOrganization(ctx, "saml-permissison-"+gofakeit.AppName(), gofakeit.Email()) - projectGrantResp := Instance.CreateProjectGrant(ctx, projectID, orgResp.GetOrganizationId()) + Instance.CreateProjectGrant(ctx, t, projectID, orgResp.GetOrganizationId()) user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone()) - Instance.CreateProjectGrantUserGrant(ctx, orgResp.GetOrganizationId(), projectID, projectGrantResp.GetGrantId(), user.GetUserId()) + Instance.CreateProjectGrantUserGrant(ctx, orgResp.GetOrganizationId(), projectID, orgResp.GetOrganizationId(), user.GetUserId()) return createSessionAndSmlRequestForCallback(ctx, t, sp, Instance.Users[integration.UserTypeLogin].ID, acsRedirect, user.GetUserId(), saml.HTTPRedirectBinding) }, @@ -569,7 +569,7 @@ func TestServer_CreateResponse_Permission(t *testing.T) { projectID, _, sp := createSAMLApplication(ctx, t, idpMetadata, saml.HTTPRedirectBinding, true, false) orgResp := Instance.CreateOrganization(ctx, "saml-permissison-"+gofakeit.AppName(), gofakeit.Email()) - Instance.CreateProjectGrant(ctx, projectID, orgResp.GetOrganizationId()) + Instance.CreateProjectGrant(ctx, t, projectID, orgResp.GetOrganizationId()) user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone()) return createSessionAndSmlRequestForCallback(ctx, t, sp, Instance.Users[integration.UserTypeLogin].ID, acsRedirect, user.GetUserId(), saml.HTTPRedirectBinding) @@ -609,7 +609,7 @@ func TestServer_CreateResponse_Permission(t *testing.T) { dep: func(ctx context.Context, t *testing.T) *saml_pb.CreateResponseRequest { projectID, _, sp := createSAMLApplication(ctx, t, idpMetadata, saml.HTTPRedirectBinding, false, true) orgResp := Instance.CreateOrganization(ctx, "saml-permissison-"+gofakeit.AppName(), gofakeit.Email()) - Instance.CreateProjectGrant(ctx, projectID, orgResp.GetOrganizationId()) + Instance.CreateProjectGrant(ctx, t, projectID, orgResp.GetOrganizationId()) user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), gofakeit.Email(), gofakeit.Phone()) return createSessionAndSmlRequestForCallback(ctx, t, sp, Instance.Users[integration.UserTypeLogin].ID, acsRedirect, user.GetUserId(), saml.HTTPRedirectBinding) @@ -686,10 +686,9 @@ func createSAMLSP(t *testing.T, idpMetadata *saml.EntityDescriptor, binding stri } func createSAMLApplication(ctx context.Context, t *testing.T, idpMetadata *saml.EntityDescriptor, binding string, projectRoleCheck, hasProjectCheck bool) (string, string, *samlsp.Middleware) { - project, err := Instance.CreateProjectWithPermissionCheck(ctx, projectRoleCheck, hasProjectCheck) - require.NoError(t, err) + project := Instance.CreateProject(ctx, t, "", gofakeit.AppName(), projectRoleCheck, hasProjectCheck) rootURL, sp := createSAMLSP(t, idpMetadata, binding) - _, err = Instance.CreateSAMLClient(ctx, project.GetId(), sp) + _, err := Instance.CreateSAMLClient(ctx, project.GetId(), sp) require.NoError(t, err) return project.GetId(), rootURL, sp } diff --git a/internal/api/grpc/saml/v2/integration/server_test.go b/internal/api/grpc/saml/v2/integration_test/server_test.go similarity index 100% rename from internal/api/grpc/saml/v2/integration/server_test.go rename to internal/api/grpc/saml/v2/integration_test/server_test.go diff --git a/internal/api/grpc/saml/v2/saml.go b/internal/api/grpc/saml/v2/saml.go index 43eae5feb1..5491a5e04b 100644 --- a/internal/api/grpc/saml/v2/saml.go +++ b/internal/api/grpc/saml/v2/saml.go @@ -3,6 +3,7 @@ package saml import ( "context" + "connectrpc.com/connect" "github.com/zitadel/logging" "github.com/zitadel/saml/pkg/provider" "google.golang.org/protobuf/types/known/timestamppb" @@ -16,15 +17,15 @@ import ( saml_pb "github.com/zitadel/zitadel/pkg/grpc/saml/v2" ) -func (s *Server) GetSAMLRequest(ctx context.Context, req *saml_pb.GetSAMLRequestRequest) (*saml_pb.GetSAMLRequestResponse, error) { - authRequest, err := s.query.SamlRequestByID(ctx, true, req.GetSamlRequestId(), true) +func (s *Server) GetSAMLRequest(ctx context.Context, req *connect.Request[saml_pb.GetSAMLRequestRequest]) (*connect.Response[saml_pb.GetSAMLRequestResponse], error) { + authRequest, err := s.query.SamlRequestByID(ctx, true, req.Msg.GetSamlRequestId(), true) if err != nil { logging.WithError(err).Error("query samlRequest by ID") return nil, err } - return &saml_pb.GetSAMLRequestResponse{ + return connect.NewResponse(&saml_pb.GetSAMLRequestResponse{ SamlRequest: samlRequestToPb(authRequest), - }, nil + }), nil } func samlRequestToPb(a *query.SamlRequest) *saml_pb.SAMLRequest { @@ -34,18 +35,18 @@ func samlRequestToPb(a *query.SamlRequest) *saml_pb.SAMLRequest { } } -func (s *Server) CreateResponse(ctx context.Context, req *saml_pb.CreateResponseRequest) (*saml_pb.CreateResponseResponse, error) { - switch v := req.GetResponseKind().(type) { +func (s *Server) CreateResponse(ctx context.Context, req *connect.Request[saml_pb.CreateResponseRequest]) (*connect.Response[saml_pb.CreateResponseResponse], error) { + switch v := req.Msg.GetResponseKind().(type) { case *saml_pb.CreateResponseRequest_Error: - return s.failSAMLRequest(ctx, req.GetSamlRequestId(), v.Error) + return s.failSAMLRequest(ctx, req.Msg.GetSamlRequestId(), v.Error) case *saml_pb.CreateResponseRequest_Session: - return s.linkSessionToSAMLRequest(ctx, req.GetSamlRequestId(), v.Session) + return s.linkSessionToSAMLRequest(ctx, req.Msg.GetSamlRequestId(), v.Session) default: return nil, zerrors.ThrowUnimplementedf(nil, "SAMLv2-0Tfak3fBS0", "verification oneOf %T in method CreateResponse not implemented", v) } } -func (s *Server) failSAMLRequest(ctx context.Context, samlRequestID string, ae *saml_pb.AuthorizationError) (*saml_pb.CreateResponseResponse, error) { +func (s *Server) failSAMLRequest(ctx context.Context, samlRequestID string, ae *saml_pb.AuthorizationError) (*connect.Response[saml_pb.CreateResponseResponse], error) { details, aar, err := s.command.FailSAMLRequest(ctx, samlRequestID, errorReasonToDomain(ae.GetError())) if err != nil { return nil, err @@ -55,7 +56,7 @@ func (s *Server) failSAMLRequest(ctx context.Context, samlRequestID string, ae * if err != nil { return nil, err } - return createCallbackResponseFromBinding(details, url, body, authReq.RelayState), nil + return connect.NewResponse(createCallbackResponseFromBinding(details, url, body, authReq.RelayState)), nil } func (s *Server) checkPermission(ctx context.Context, issuer string, userID string) error { @@ -72,7 +73,7 @@ func (s *Server) checkPermission(ctx context.Context, issuer string, userID stri return nil } -func (s *Server) linkSessionToSAMLRequest(ctx context.Context, samlRequestID string, session *saml_pb.Session) (*saml_pb.CreateResponseResponse, error) { +func (s *Server) linkSessionToSAMLRequest(ctx context.Context, samlRequestID string, session *saml_pb.Session) (*connect.Response[saml_pb.CreateResponseResponse], error) { details, aar, err := s.command.LinkSessionToSAMLRequest(ctx, samlRequestID, session.GetSessionId(), session.GetSessionToken(), true, s.checkPermission) if err != nil { return nil, err @@ -87,7 +88,7 @@ func (s *Server) linkSessionToSAMLRequest(ctx context.Context, samlRequestID str if err != nil { return nil, err } - return createCallbackResponseFromBinding(details, url, body, authReq.RelayState), nil + return connect.NewResponse(createCallbackResponseFromBinding(details, url, body, authReq.RelayState)), nil } func createCallbackResponseFromBinding(details *domain.ObjectDetails, url string, body string, relayState string) *saml_pb.CreateResponseResponse { diff --git a/internal/api/grpc/saml/v2/server.go b/internal/api/grpc/saml/v2/server.go index 62299d88c5..312a7c356a 100644 --- a/internal/api/grpc/saml/v2/server.go +++ b/internal/api/grpc/saml/v2/server.go @@ -1,7 +1,10 @@ package saml import ( - "google.golang.org/grpc" + "net/http" + + "connectrpc.com/connect" + "google.golang.org/protobuf/reflect/protoreflect" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/grpc/server" @@ -9,9 +12,10 @@ import ( "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/query" saml_pb "github.com/zitadel/zitadel/pkg/grpc/saml/v2" + "github.com/zitadel/zitadel/pkg/grpc/saml/v2/samlconnect" ) -var _ saml_pb.SAMLServiceServer = (*Server)(nil) +var _ samlconnect.SAMLServiceHandler = (*Server)(nil) type Server struct { saml_pb.UnimplementedSAMLServiceServer @@ -38,8 +42,12 @@ func CreateServer( } } -func (s *Server) RegisterServer(grpcServer *grpc.Server) { - saml_pb.RegisterSAMLServiceServer(grpcServer, s) +func (s *Server) RegisterConnectServer(interceptors ...connect.Interceptor) (string, http.Handler) { + return samlconnect.NewSAMLServiceHandler(s, connect.WithInterceptors(interceptors...)) +} + +func (s *Server) FileDescriptor() protoreflect.FileDescriptor { + return saml_pb.File_zitadel_saml_v2_saml_service_proto } func (s *Server) AppName() string { diff --git a/internal/api/grpc/server/connect_middleware/access_interceptor.go b/internal/api/grpc/server/connect_middleware/access_interceptor.go new file mode 100644 index 0000000000..a08df59860 --- /dev/null +++ b/internal/api/grpc/server/connect_middleware/access_interceptor.go @@ -0,0 +1,57 @@ +package connect_middleware + +import ( + "context" + "net/http" + "time" + + "connectrpc.com/connect" + + "github.com/zitadel/zitadel/internal/api/authz" + http_util "github.com/zitadel/zitadel/internal/api/http" + "github.com/zitadel/zitadel/internal/logstore" + "github.com/zitadel/zitadel/internal/logstore/record" + "github.com/zitadel/zitadel/internal/telemetry/tracing" +) + +func AccessStorageInterceptor(svc *logstore.Service[*record.AccessLog]) connect.UnaryInterceptorFunc { + return func(handler connect.UnaryFunc) connect.UnaryFunc { + return func(ctx context.Context, req connect.AnyRequest) (_ connect.AnyResponse, err error) { + if !svc.Enabled() { + return handler(ctx, req) + } + resp, handlerErr := handler(ctx, req) + + interceptorCtx, span := tracing.NewServerInterceptorSpan(ctx) + defer func() { span.EndWithError(err) }() + + var respStatus uint32 + if code := connect.CodeOf(handlerErr); code != connect.CodeUnknown { + respStatus = uint32(code) + } + + respHeader := http.Header{} + if resp != nil { + respHeader = resp.Header() + } + instance := authz.GetInstance(ctx) + domainCtx := http_util.DomainContext(ctx) + + r := &record.AccessLog{ + LogDate: time.Now(), + Protocol: record.GRPC, + RequestURL: req.Spec().Procedure, + ResponseStatus: respStatus, + RequestHeaders: req.Header(), + ResponseHeaders: respHeader, + InstanceID: instance.InstanceID(), + ProjectID: instance.ProjectID(), + RequestedDomain: domainCtx.RequestedDomain(), + RequestedHost: domainCtx.RequestedHost(), + } + + svc.Handle(interceptorCtx, r) + return resp, handlerErr + } + } +} diff --git a/internal/api/grpc/server/connect_middleware/activity_interceptor.go b/internal/api/grpc/server/connect_middleware/activity_interceptor.go new file mode 100644 index 0000000000..4ba6044645 --- /dev/null +++ b/internal/api/grpc/server/connect_middleware/activity_interceptor.go @@ -0,0 +1,52 @@ +package connect_middleware + +import ( + "context" + "net/http" + "slices" + "strings" + + "connectrpc.com/connect" + + "github.com/zitadel/zitadel/internal/activity" + "github.com/zitadel/zitadel/internal/api/grpc/gerrors" + ainfo "github.com/zitadel/zitadel/internal/api/info" +) + +func ActivityInterceptor() connect.UnaryInterceptorFunc { + return func(handler connect.UnaryFunc) connect.UnaryFunc { + return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) { + ctx = activityInfoFromGateway(ctx, req.Header()).SetMethod(req.Spec().Procedure).IntoContext(ctx) + resp, err := handler(ctx, req) + if isResourceAPI(req.Spec().Procedure) { + code, _, _, _ := gerrors.ExtractZITADELError(err) + ctx = ainfo.ActivityInfoFromContext(ctx).SetGRPCStatus(code).IntoContext(ctx) + activity.TriggerGRPCWithContext(ctx, activity.ResourceAPI) + } + return resp, err + } + } +} + +var resourcePrefixes = []string{ + "/zitadel.management.v1.ManagementService/", + "/zitadel.admin.v1.AdminService/", + "/zitadel.user.v2.UserService/", + "/zitadel.settings.v2.SettingsService/", + "/zitadel.user.v2beta.UserService/", + "/zitadel.settings.v2beta.SettingsService/", + "/zitadel.auth.v1.AuthService/", +} + +func isResourceAPI(method string) bool { + return slices.ContainsFunc(resourcePrefixes, func(prefix string) bool { + return strings.HasPrefix(method, prefix) + }) +} + +func activityInfoFromGateway(ctx context.Context, headers http.Header) *ainfo.ActivityInfo { + info := ainfo.ActivityInfoFromContext(ctx) + path := headers.Get(activity.PathKey) + requestMethod := headers.Get(activity.RequestMethodKey) + return info.SetPath(path).SetRequestMethod(requestMethod) +} diff --git a/internal/api/grpc/server/connect_middleware/auth_interceptor.go b/internal/api/grpc/server/connect_middleware/auth_interceptor.go new file mode 100644 index 0000000000..9e500601d0 --- /dev/null +++ b/internal/api/grpc/server/connect_middleware/auth_interceptor.go @@ -0,0 +1,65 @@ +package connect_middleware + +import ( + "context" + "errors" + + "connectrpc.com/connect" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/api/http" + "github.com/zitadel/zitadel/internal/telemetry/tracing" +) + +func AuthorizationInterceptor(verifier authz.APITokenVerifier, systemUserPermissions authz.Config, authConfig authz.Config) connect.UnaryInterceptorFunc { + return func(handler connect.UnaryFunc) connect.UnaryFunc { + return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) { + return authorize(ctx, req, handler, verifier, systemUserPermissions, authConfig) + } + } +} + +func authorize(ctx context.Context, req connect.AnyRequest, handler connect.UnaryFunc, verifier authz.APITokenVerifier, systemUserPermissions authz.Config, authConfig authz.Config) (_ connect.AnyResponse, err error) { + authOpt, needsToken := verifier.CheckAuthMethod(req.Spec().Procedure) + if !needsToken { + return handler(ctx, req) + } + + authCtx, span := tracing.NewServerInterceptorSpan(ctx) + defer func() { span.EndWithError(err) }() + + authToken := req.Header().Get(http.Authorization) + if authToken == "" { + return nil, connect.NewError(connect.CodeUnauthenticated, errors.New("auth header missing")) + } + + orgID, orgDomain := orgIDAndDomainFromRequest(req) + ctxSetter, err := authz.CheckUserAuthorization(authCtx, req, authToken, orgID, orgDomain, verifier, systemUserPermissions.RolePermissionMappings, authConfig.RolePermissionMappings, authOpt, req.Spec().Procedure) + if err != nil { + return nil, err + } + span.End() + return handler(ctxSetter(ctx), req) +} + +func orgIDAndDomainFromRequest(req connect.AnyRequest) (id, domain string) { + orgID := req.Header().Get(http.ZitadelOrgID) + oz, ok := req.Any().(OrganizationFromRequest) + if ok { + id = oz.OrganizationFromRequestConnect().ID + domain = oz.OrganizationFromRequestConnect().Domain + if id != "" || domain != "" { + return id, domain + } + } + return orgID, domain +} + +type Organization struct { + ID string + Domain string +} + +type OrganizationFromRequest interface { + OrganizationFromRequestConnect() *Organization +} diff --git a/internal/api/grpc/server/connect_middleware/auth_interceptor_test.go b/internal/api/grpc/server/connect_middleware/auth_interceptor_test.go new file mode 100644 index 0000000000..06e716c140 --- /dev/null +++ b/internal/api/grpc/server/connect_middleware/auth_interceptor_test.go @@ -0,0 +1,318 @@ +package connect_middleware + +import ( + "context" + "errors" + "net/http" + "reflect" + "testing" + + "connectrpc.com/connect" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/zerrors" +) + +const anAPIRole = "AN_API_ROLE" + +type authzRepoMock struct{} + +func (v *authzRepoMock) VerifyAccessToken(ctx context.Context, token, clientID, projectID string) (string, string, string, string, string, error) { + return "", "", "", "", "", nil +} + +func (v *authzRepoMock) SearchMyMemberships(ctx context.Context, orgID string, _ bool) ([]*authz.Membership, error) { + return authz.Memberships{{ + MemberType: authz.MemberTypeOrganization, + AggregateID: orgID, + Roles: []string{anAPIRole}, + }}, nil +} + +func (v *authzRepoMock) ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (string, []string, error) { + return "", nil, nil +} + +func (v *authzRepoMock) ExistsOrg(ctx context.Context, orgID, domain string) (string, error) { + return orgID, nil +} + +func (v *authzRepoMock) VerifierClientID(ctx context.Context, appName string) (string, string, error) { + return "", "", nil +} + +var ( + accessTokenOK = authz.AccessTokenVerifierFunc(func(ctx context.Context, token string) (userID string, clientID string, agentID string, prefLan string, resourceOwner string, err error) { + return "user1", "", "", "", "org1", nil + }) + accessTokenNOK = authz.AccessTokenVerifierFunc(func(ctx context.Context, token string) (userID string, clientID string, agentID string, prefLan string, resourceOwner string, err error) { + return "", "", "", "", "", zerrors.ThrowUnauthenticated(nil, "TEST-fQHDI", "unauthenticaded") + }) + systemTokenNOK = authz.SystemTokenVerifierFunc(func(ctx context.Context, token string, orgID string) (memberships authz.Memberships, userID string, err error) { + return nil, "", errors.New("system token error") + }) +) + +type mockOrgFromRequest struct { + id string +} + +func (m *mockOrgFromRequest) OrganizationFromRequestConnect() *Organization { + return &Organization{ + ID: m.id, + Domain: "", + } +} + +func Test_authorize(t *testing.T) { + type args struct { + ctx context.Context + req connect.AnyRequest + handler func(t *testing.T) connect.UnaryFunc + verifier func() authz.APITokenVerifier + authConfig authz.Config + } + type res struct { + want interface{} + wantErr bool + } + tests := []struct { + name string + args args + res res + }{ + { + "no token needed ok", + args{ + ctx: context.Background(), + req: &mockReq[struct{}]{procedure: "/no/token/needed"}, + handler: emptyMockHandler(&connect.Response[struct{}]{}, authz.CtxData{}), + verifier: func() authz.APITokenVerifier { + verifier := authz.StartAPITokenVerifier(&authzRepoMock{}, accessTokenOK, systemTokenNOK) + verifier.RegisterServer("need", "need", authz.MethodMapping{}) + return verifier + }, + }, + res{ + &connect.Response[struct{}]{}, + false, + }, + }, + { + "auth header missing error", + args{ + ctx: context.Background(), + req: &mockReq[struct{}]{procedure: "/need/authentication"}, + handler: emptyMockHandler(&connect.Response[struct{}]{}, authz.CtxData{}), + verifier: func() authz.APITokenVerifier { + verifier := authz.StartAPITokenVerifier(&authzRepoMock{}, accessTokenOK, systemTokenNOK) + verifier.RegisterServer("need", "need", authz.MethodMapping{"/need/authentication": authz.Option{Permission: "authenticated"}}) + return verifier + }, + authConfig: authz.Config{}, + }, + res{ + nil, + true, + }, + }, + { + "unauthorized error", + args{ + ctx: context.Background(), + req: &mockReq[struct{}]{procedure: "/need/authentication", header: http.Header{"Authorization": []string{"wrong"}}}, + handler: emptyMockHandler(&connect.Response[struct{}]{}, authz.CtxData{}), + verifier: func() authz.APITokenVerifier { + verifier := authz.StartAPITokenVerifier(&authzRepoMock{}, accessTokenOK, systemTokenNOK) + verifier.RegisterServer("need", "need", authz.MethodMapping{"/need/authentication": authz.Option{Permission: "authenticated"}}) + return verifier + }, + authConfig: authz.Config{}, + }, + res{ + nil, + true, + }, + }, + { + "authorized ok", + args{ + ctx: context.Background(), + req: &mockReq[struct{}]{procedure: "/need/authentication", header: http.Header{"Authorization": []string{"Bearer token"}}}, + handler: emptyMockHandler(&connect.Response[struct{}]{}, authz.CtxData{ + UserID: "user1", + OrgID: "org1", + ResourceOwner: "org1", + }), + verifier: func() authz.APITokenVerifier { + verifier := authz.StartAPITokenVerifier(&authzRepoMock{}, accessTokenOK, systemTokenNOK) + verifier.RegisterServer("need", "need", authz.MethodMapping{"/need/authentication": authz.Option{Permission: "authenticated"}}) + return verifier + }, + authConfig: authz.Config{}, + }, + res{ + &connect.Response[struct{}]{}, + false, + }, + }, + { + "authorized ok, org by request", + args{ + ctx: context.Background(), + req: &mockReq[mockOrgFromRequest]{ + Request: connect.Request[mockOrgFromRequest]{Msg: &mockOrgFromRequest{"id"}}, + procedure: "/need/authentication", + header: http.Header{"Authorization": []string{"Bearer token"}}, + }, + handler: emptyMockHandler(&connect.Response[mockOrgFromRequest]{Msg: &mockOrgFromRequest{"id"}}, authz.CtxData{ + UserID: "user1", + OrgID: "id", + ResourceOwner: "org1", + }), + verifier: func() authz.APITokenVerifier { + verifier := authz.StartAPITokenVerifier(&authzRepoMock{}, accessTokenOK, systemTokenNOK) + verifier.RegisterServer("need", "need", authz.MethodMapping{"/need/authentication": authz.Option{Permission: "authenticated"}}) + return verifier + }, + authConfig: authz.Config{}, + }, + res{ + &connect.Response[mockOrgFromRequest]{Msg: &mockOrgFromRequest{"id"}}, + false, + }, + }, + { + "permission denied error", + args{ + ctx: context.Background(), + req: &mockReq[struct{}]{procedure: "/need/authentication", header: http.Header{"Authorization": []string{"Bearer token"}}}, + handler: emptyMockHandler(&connect.Response[struct{}]{}, authz.CtxData{ + UserID: "user1", + OrgID: "org1", + ResourceOwner: "org1", + }), + verifier: func() authz.APITokenVerifier { + verifier := authz.StartAPITokenVerifier(&authzRepoMock{}, accessTokenOK, systemTokenNOK) + verifier.RegisterServer("need", "need", authz.MethodMapping{"/need/authentication": authz.Option{Permission: "to.do.something"}}) + return verifier + }, + authConfig: authz.Config{ + RolePermissionMappings: []authz.RoleMapping{{ + Role: anAPIRole, + Permissions: []string{"to.do.something.else"}, + }}, + }, + }, + res{ + nil, + true, + }, + }, + { + "permission ok", + args{ + ctx: context.Background(), + req: &mockReq[struct{}]{procedure: "/need/authentication", header: http.Header{"Authorization": []string{"Bearer token"}}}, + handler: emptyMockHandler(&connect.Response[struct{}]{}, authz.CtxData{ + UserID: "user1", + OrgID: "org1", + ResourceOwner: "org1", + }), + verifier: func() authz.APITokenVerifier { + verifier := authz.StartAPITokenVerifier(&authzRepoMock{}, accessTokenOK, systemTokenNOK) + verifier.RegisterServer("need", "need", authz.MethodMapping{"/need/authentication": authz.Option{Permission: "to.do.something"}}) + return verifier + }, + authConfig: authz.Config{ + RolePermissionMappings: []authz.RoleMapping{{ + Role: anAPIRole, + Permissions: []string{"to.do.something"}, + }}, + }, + }, + res{ + &connect.Response[struct{}]{}, + false, + }, + }, + { + "system token permission denied error", + args{ + ctx: context.Background(), + req: &mockReq[struct{}]{procedure: "/need/authentication", header: http.Header{"Authorization": []string{"Bearer token"}}}, + handler: emptyMockHandler(&connect.Response[struct{}]{}, authz.CtxData{}), + verifier: func() authz.APITokenVerifier { + verifier := authz.StartAPITokenVerifier(&authzRepoMock{}, accessTokenNOK, authz.SystemTokenVerifierFunc(func(ctx context.Context, token string, orgID string) (memberships authz.Memberships, userID string, err error) { + return authz.Memberships{{ + MemberType: authz.MemberTypeSystem, + Roles: []string{"A_SYSTEM_ROLE"}, + }}, "systemuser", nil + })) + verifier.RegisterServer("need", "need", authz.MethodMapping{"/need/authentication": authz.Option{Permission: "to.do.something"}}) + return verifier + }, + authConfig: authz.Config{ + RolePermissionMappings: []authz.RoleMapping{{ + Role: "A_SYSTEM_ROLE", + Permissions: []string{"to.do.something.else"}, + }}, + }, + }, + res{ + nil, + true, + }, + }, + { + "system token permission denied error", + args{ + ctx: context.Background(), + req: &mockReq[struct{}]{procedure: "/need/authentication", header: http.Header{"Authorization": []string{"Bearer token"}}}, + handler: emptyMockHandler(&connect.Response[struct{}]{}, authz.CtxData{ + UserID: "systemuser", + SystemMemberships: authz.Memberships{{ + MemberType: authz.MemberTypeSystem, + Roles: []string{"A_SYSTEM_ROLE"}, + }}, + SystemUserPermissions: []authz.SystemUserPermissions{{ + MemberType: authz.MemberTypeSystem, + Permissions: []string{"to.do.something"}, + }}, + }), + verifier: func() authz.APITokenVerifier { + verifier := authz.StartAPITokenVerifier(&authzRepoMock{}, accessTokenNOK, authz.SystemTokenVerifierFunc(func(ctx context.Context, token string, orgID string) (memberships authz.Memberships, userID string, err error) { + return authz.Memberships{{ + MemberType: authz.MemberTypeSystem, + Roles: []string{"A_SYSTEM_ROLE"}, + }}, "systemuser", nil + })) + verifier.RegisterServer("need", "need", authz.MethodMapping{"/need/authentication": authz.Option{Permission: "to.do.something"}}) + return verifier + }, + authConfig: authz.Config{ + RolePermissionMappings: []authz.RoleMapping{{ + Role: "A_SYSTEM_ROLE", + Permissions: []string{"to.do.something"}, + }}, + }, + }, + res{ + &connect.Response[struct{}]{}, + false, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := authorize(tt.args.ctx, tt.args.req, tt.args.handler(t), tt.args.verifier(), tt.args.authConfig, tt.args.authConfig) + if (err != nil) != tt.res.wantErr { + t.Errorf("authorize() error = %v, wantErr %v", err, tt.res.wantErr) + return + } + if !reflect.DeepEqual(got, tt.res.want) { + t.Errorf("authorize() got = %v, want %v", got, tt.res.want) + } + }) + } +} diff --git a/internal/api/grpc/server/connect_middleware/cache_interceptor.go b/internal/api/grpc/server/connect_middleware/cache_interceptor.go new file mode 100644 index 0000000000..60ba0032f1 --- /dev/null +++ b/internal/api/grpc/server/connect_middleware/cache_interceptor.go @@ -0,0 +1,31 @@ +package connect_middleware + +import ( + "context" + "net/http" + "time" + + "connectrpc.com/connect" + + _ "github.com/zitadel/zitadel/internal/statik" +) + +func NoCacheInterceptor() connect.UnaryInterceptorFunc { + return func(handler connect.UnaryFunc) connect.UnaryFunc { + return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) { + headers := map[string]string{ + "cache-control": "no-store", + "expires": time.Now().UTC().Format(http.TimeFormat), + "pragma": "no-cache", + } + resp, err := handler(ctx, req) + if err != nil { + return nil, err + } + for key, value := range headers { + resp.Header().Set(key, value) + } + return resp, err + } + } +} diff --git a/internal/api/grpc/server/connect_middleware/call_interceptor.go b/internal/api/grpc/server/connect_middleware/call_interceptor.go new file mode 100644 index 0000000000..cc74e10f85 --- /dev/null +++ b/internal/api/grpc/server/connect_middleware/call_interceptor.go @@ -0,0 +1,18 @@ +package connect_middleware + +import ( + "context" + + "connectrpc.com/connect" + + "github.com/zitadel/zitadel/internal/api/call" +) + +func CallDurationHandler() connect.UnaryInterceptorFunc { + return func(handler connect.UnaryFunc) connect.UnaryFunc { + return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) { + ctx = call.WithTimestamp(ctx) + return handler(ctx, req) + } + } +} diff --git a/internal/api/grpc/server/connect_middleware/error_interceptor.go b/internal/api/grpc/server/connect_middleware/error_interceptor.go new file mode 100644 index 0000000000..9aef95bc6d --- /dev/null +++ b/internal/api/grpc/server/connect_middleware/error_interceptor.go @@ -0,0 +1,23 @@ +package connect_middleware + +import ( + "context" + + "connectrpc.com/connect" + + "github.com/zitadel/zitadel/internal/api/grpc/gerrors" + _ "github.com/zitadel/zitadel/internal/statik" +) + +func ErrorHandler() connect.UnaryInterceptorFunc { + return func(handler connect.UnaryFunc) connect.UnaryFunc { + return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) { + return toConnectError(ctx, req, handler) + } + } +} + +func toConnectError(ctx context.Context, req connect.AnyRequest, handler connect.UnaryFunc) (connect.AnyResponse, error) { + resp, err := handler(ctx, req) + return resp, gerrors.ZITADELToConnectError(err) // TODO ! +} diff --git a/internal/api/grpc/server/connect_middleware/error_interceptor_test.go b/internal/api/grpc/server/connect_middleware/error_interceptor_test.go new file mode 100644 index 0000000000..954f2fd58f --- /dev/null +++ b/internal/api/grpc/server/connect_middleware/error_interceptor_test.go @@ -0,0 +1,65 @@ +package connect_middleware + +import ( + "context" + "reflect" + "testing" + + "connectrpc.com/connect" + + "github.com/zitadel/zitadel/internal/api/authz" +) + +func Test_toGRPCError(t *testing.T) { + type args struct { + ctx context.Context + req connect.AnyRequest + handler func(t *testing.T) connect.UnaryFunc + } + type res struct { + want interface{} + wantErr bool + } + tests := []struct { + name string + args args + res res + }{ + { + "no error", + args{ + ctx: context.Background(), + req: &mockReq[struct{}]{}, + handler: emptyMockHandler(&connect.Response[struct{}]{}, authz.CtxData{}), + }, + res{ + &connect.Response[struct{}]{}, + false, + }, + }, + { + "error", + args{ + ctx: context.Background(), + req: &mockReq[struct{}]{}, + handler: errorMockHandler(), + }, + res{ + nil, + true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := toConnectError(tt.args.ctx, tt.args.req, tt.args.handler(t)) + if (err != nil) != tt.res.wantErr { + t.Errorf("toGRPCError() error = %v, wantErr %v", err, tt.res.wantErr) + return + } + if !reflect.DeepEqual(got, tt.res.want) { + t.Errorf("toGRPCError() got = %v, want %v", got, tt.res.want) + } + }) + } +} diff --git a/internal/api/grpc/server/connect_middleware/execution_interceptor.go b/internal/api/grpc/server/connect_middleware/execution_interceptor.go new file mode 100644 index 0000000000..879496a33f --- /dev/null +++ b/internal/api/grpc/server/connect_middleware/execution_interceptor.go @@ -0,0 +1,160 @@ +package connect_middleware + +import ( + "context" + "encoding/json" + + "connectrpc.com/connect" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/execution" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/telemetry/tracing" +) + +func ExecutionHandler(queries *query.Queries) connect.UnaryInterceptorFunc { + return func(handler connect.UnaryFunc) connect.UnaryFunc { + return func(ctx context.Context, req connect.AnyRequest) (_ connect.AnyResponse, err error) { + requestTargets, responseTargets := execution.QueryExecutionTargetsForRequestAndResponse(ctx, queries, req.Spec().Procedure) + + // call targets otherwise return req + handledReq, err := executeTargetsForRequest(ctx, requestTargets, req.Spec().Procedure, req) + if err != nil { + return nil, err + } + + response, err := handler(ctx, handledReq) + if err != nil { + return nil, err + } + + return executeTargetsForResponse(ctx, responseTargets, req.Spec().Procedure, handledReq, response) + } + } +} + +func executeTargetsForRequest(ctx context.Context, targets []execution.Target, fullMethod string, req connect.AnyRequest) (_ connect.AnyRequest, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + // if no targets are found, return without any calls + if len(targets) == 0 { + return req, nil + } + + ctxData := authz.GetCtxData(ctx) + info := &ContextInfoRequest{ + FullMethod: fullMethod, + InstanceID: authz.GetInstance(ctx).InstanceID(), + ProjectID: ctxData.ProjectID, + OrgID: ctxData.OrgID, + UserID: ctxData.UserID, + Request: Message{req.Any().(proto.Message)}, + } + + _, err = execution.CallTargets(ctx, targets, info) + if err != nil { + return nil, err + } + return req, nil +} + +func executeTargetsForResponse(ctx context.Context, targets []execution.Target, fullMethod string, req connect.AnyRequest, resp connect.AnyResponse) (_ connect.AnyResponse, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + // if no targets are found, return without any calls + if len(targets) == 0 { + return resp, nil + } + + ctxData := authz.GetCtxData(ctx) + info := &ContextInfoResponse{ + FullMethod: fullMethod, + InstanceID: authz.GetInstance(ctx).InstanceID(), + ProjectID: ctxData.ProjectID, + OrgID: ctxData.OrgID, + UserID: ctxData.UserID, + Request: Message{req.Any().(proto.Message)}, + Response: Message{resp.Any().(proto.Message)}, + } + + _, err = execution.CallTargets(ctx, targets, info) + if err != nil { + return nil, err + } + return resp, nil +} + +var _ execution.ContextInfo = &ContextInfoRequest{} + +type ContextInfoRequest struct { + FullMethod string `json:"fullMethod,omitempty"` + InstanceID string `json:"instanceID,omitempty"` + OrgID string `json:"orgID,omitempty"` + ProjectID string `json:"projectID,omitempty"` + UserID string `json:"userID,omitempty"` + Request Message `json:"request,omitempty"` +} + +type Message struct { + proto.Message +} + +func (r *Message) MarshalJSON() ([]byte, error) { + data, err := protojson.Marshal(r.Message) + if err != nil { + return nil, err + } + return data, nil +} + +func (r *Message) UnmarshalJSON(data []byte) error { + return protojson.Unmarshal(data, r.Message) +} + +func (c *ContextInfoRequest) GetHTTPRequestBody() []byte { + data, err := json.Marshal(c) + if err != nil { + return nil + } + return data +} + +func (c *ContextInfoRequest) SetHTTPResponseBody(resp []byte) error { + return json.Unmarshal(resp, &c.Request) +} + +func (c *ContextInfoRequest) GetContent() interface{} { + return c.Request.Message +} + +var _ execution.ContextInfo = &ContextInfoResponse{} + +type ContextInfoResponse struct { + FullMethod string `json:"fullMethod,omitempty"` + InstanceID string `json:"instanceID,omitempty"` + OrgID string `json:"orgID,omitempty"` + ProjectID string `json:"projectID,omitempty"` + UserID string `json:"userID,omitempty"` + Request Message `json:"request,omitempty"` + Response Message `json:"response,omitempty"` +} + +func (c *ContextInfoResponse) GetHTTPRequestBody() []byte { + data, err := json.Marshal(c) + if err != nil { + return nil + } + return data +} + +func (c *ContextInfoResponse) SetHTTPResponseBody(resp []byte) error { + return json.Unmarshal(resp, &c.Response) +} + +func (c *ContextInfoResponse) GetContent() interface{} { + return c.Response.Message +} diff --git a/internal/api/grpc/server/connect_middleware/execution_interceptor_test.go b/internal/api/grpc/server/connect_middleware/execution_interceptor_test.go new file mode 100644 index 0000000000..d910824f21 --- /dev/null +++ b/internal/api/grpc/server/connect_middleware/execution_interceptor_test.go @@ -0,0 +1,815 @@ +package connect_middleware + +import ( + "context" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "reflect" + "testing" + "time" + + "connectrpc.com/connect" + "github.com/stretchr/testify/assert" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/structpb" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/execution" +) + +var _ execution.Target = &mockExecutionTarget{} + +type mockExecutionTarget struct { + InstanceID string + ExecutionID string + TargetID string + TargetType domain.TargetType + Endpoint string + Timeout time.Duration + InterruptOnError bool + SigningKey string +} + +func (e *mockExecutionTarget) SetEndpoint(endpoint string) { + e.Endpoint = endpoint +} +func (e *mockExecutionTarget) IsInterruptOnError() bool { + return e.InterruptOnError +} +func (e *mockExecutionTarget) GetEndpoint() string { + return e.Endpoint +} +func (e *mockExecutionTarget) GetTargetType() domain.TargetType { + return e.TargetType +} +func (e *mockExecutionTarget) GetTimeout() time.Duration { + return e.Timeout +} +func (e *mockExecutionTarget) GetTargetID() string { + return e.TargetID +} +func (e *mockExecutionTarget) GetExecutionID() string { + return e.ExecutionID +} +func (e *mockExecutionTarget) GetSigningKey() string { + return e.SigningKey +} + +func newMockContentRequest(content string) *connect.Request[structpb.Struct] { + return connect.NewRequest(&structpb.Struct{ + Fields: map[string]*structpb.Value{ + "content": { + Kind: &structpb.Value_StringValue{StringValue: content}, + }, + }, + }) +} + +func newMockContentResponse(content string) *connect.Response[structpb.Struct] { + return connect.NewResponse(&structpb.Struct{ + Fields: map[string]*structpb.Value{ + "content": { + Kind: &structpb.Value_StringValue{StringValue: content}, + }, + }, + }) +} + +func newMockContextInfoRequest(fullMethod, request string) *ContextInfoRequest { + return &ContextInfoRequest{ + FullMethod: fullMethod, + Request: Message{Message: newMockContentRequest(request).Msg}, + } +} + +func newMockContextInfoResponse(fullMethod, request, response string) *ContextInfoResponse { + return &ContextInfoResponse{ + FullMethod: fullMethod, + Request: Message{Message: newMockContentRequest(request).Msg}, + Response: Message{Message: newMockContentResponse(response).Msg}, + } +} + +func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) { + type target struct { + reqBody execution.ContextInfo + sleep time.Duration + statusCode int + respBody connect.AnyResponse + } + type args struct { + ctx context.Context + + executionTargets []execution.Target + targets []target + fullMethod string + req connect.AnyRequest + } + type res struct { + want interface{} + wantErr bool + } + tests := []struct { + name string + args args + res res + }{ + { + "target, executionTargets nil", + args{ + ctx: context.Background(), + fullMethod: "/service/method", + executionTargets: nil, + req: newMockContentRequest("request"), + }, + res{ + want: newMockContentRequest("request"), + }, + }, + { + "target, executionTargets empty", + args{ + ctx: context.Background(), + fullMethod: "/service/method", + executionTargets: []execution.Target{}, + req: newMockContentRequest("request"), + }, + res{ + want: newMockContentRequest("request"), + }, + }, + { + "target, not reachable", + args{ + ctx: context.Background(), + fullMethod: "/service/method", + executionTargets: []execution.Target{ + &mockExecutionTarget{ + InstanceID: "instance", + ExecutionID: "request./zitadel.session.v2.SessionService/SetSession", + TargetID: "target", + TargetType: domain.TargetTypeCall, + Timeout: time.Minute, + InterruptOnError: true, + }, + }, + targets: []target{}, + req: newMockContentRequest("content"), + }, + res{ + wantErr: true, + }, + }, + { + "target, error without interrupt", + args{ + ctx: context.Background(), + fullMethod: "/service/method", + executionTargets: []execution.Target{ + &mockExecutionTarget{ + InstanceID: "instance", + ExecutionID: "request./zitadel.session.v2.SessionService/SetSession", + TargetID: "target", + TargetType: domain.TargetTypeCall, + Timeout: time.Minute, + SigningKey: "signingkey", + }, + }, + targets: []target{ + { + reqBody: newMockContextInfoRequest("/service/method", "content"), + respBody: newMockContentResponse("content1"), + sleep: 0, + statusCode: http.StatusBadRequest, + }, + }, + req: newMockContentRequest("content"), + }, + res{ + want: newMockContentRequest("content"), + }, + }, + { + "target, interruptOnError", + args{ + ctx: context.Background(), + fullMethod: "/service/method", + executionTargets: []execution.Target{ + &mockExecutionTarget{ + InstanceID: "instance", + ExecutionID: "request./zitadel.session.v2.SessionService/SetSession", + TargetID: "target", + TargetType: domain.TargetTypeCall, + Timeout: time.Minute, + InterruptOnError: true, + SigningKey: "signingkey", + }, + }, + + targets: []target{ + { + reqBody: newMockContextInfoRequest("/service/method", "content"), + respBody: newMockContentResponse("content1"), + sleep: 0, + statusCode: http.StatusBadRequest, + }, + }, + req: newMockContentRequest("content"), + }, + res{ + wantErr: true, + }, + }, + { + "target, timeout", + args{ + ctx: context.Background(), + fullMethod: "/service/method", + executionTargets: []execution.Target{ + &mockExecutionTarget{ + InstanceID: "instance", + ExecutionID: "request./zitadel.session.v2.SessionService/SetSession", + TargetID: "target", + TargetType: domain.TargetTypeCall, + Timeout: time.Second, + InterruptOnError: true, + SigningKey: "signingkey", + }, + }, + targets: []target{ + { + reqBody: newMockContextInfoRequest("/service/method", "content"), + respBody: newMockContentResponse("content1"), + sleep: 5 * time.Second, + statusCode: http.StatusOK, + }, + }, + req: newMockContentRequest("content"), + }, + res{ + wantErr: true, + }, + }, + { + "target, wrong request", + args{ + ctx: context.Background(), + fullMethod: "/service/method", + executionTargets: []execution.Target{ + &mockExecutionTarget{ + InstanceID: "instance", + ExecutionID: "request./zitadel.session.v2.SessionService/SetSession", + TargetID: "target", + TargetType: domain.TargetTypeCall, + Timeout: time.Second, + InterruptOnError: true, + SigningKey: "signingkey", + }, + }, + targets: []target{ + {reqBody: newMockContextInfoRequest("/service/method", "wrong")}, + }, + req: newMockContentRequest("content"), + }, + res{ + wantErr: true, + }, + }, + { + "target, ok", + args{ + ctx: context.Background(), + fullMethod: "/service/method", + executionTargets: []execution.Target{ + &mockExecutionTarget{ + InstanceID: "instance", + ExecutionID: "request./zitadel.session.v2.SessionService/SetSession", + TargetID: "target", + TargetType: domain.TargetTypeCall, + Timeout: time.Minute, + InterruptOnError: true, + SigningKey: "signingkey", + }, + }, + targets: []target{ + { + reqBody: newMockContextInfoRequest("/service/method", "content"), + respBody: newMockContentResponse("content1"), + sleep: 0, + statusCode: http.StatusOK, + }, + }, + req: newMockContentRequest("content"), + }, + res{ + want: newMockContentRequest("content1"), + }, + }, + { + "target async, timeout", + args{ + ctx: context.Background(), + fullMethod: "/service/method", + executionTargets: []execution.Target{ + &mockExecutionTarget{ + InstanceID: "instance", + ExecutionID: "request./zitadel.session.v2.SessionService/SetSession", + TargetID: "target", + TargetType: domain.TargetTypeAsync, + Timeout: time.Second, + SigningKey: "signingkey", + }, + }, + targets: []target{ + { + reqBody: newMockContextInfoRequest("/service/method", "content"), + respBody: newMockContentResponse("content1"), + sleep: 5 * time.Second, + statusCode: http.StatusOK, + }, + }, + req: newMockContentRequest("content"), + }, + res{ + want: newMockContentRequest("content"), + }, + }, + { + "target async, ok", + args{ + ctx: context.Background(), + fullMethod: "/service/method", + executionTargets: []execution.Target{ + &mockExecutionTarget{ + InstanceID: "instance", + ExecutionID: "request./zitadel.session.v2.SessionService/SetSession", + TargetID: "target", + TargetType: domain.TargetTypeAsync, + Timeout: time.Minute, + SigningKey: "signingkey", + }, + }, + targets: []target{ + { + reqBody: newMockContextInfoRequest("/service/method", "content"), + respBody: newMockContentResponse("content1"), + sleep: 0, + statusCode: http.StatusOK, + }, + }, + req: newMockContentRequest("content"), + }, + res{ + want: newMockContentRequest("content"), + }, + }, + { + "webhook, error", + args{ + ctx: context.Background(), + fullMethod: "/service/method", + executionTargets: []execution.Target{ + &mockExecutionTarget{ + InstanceID: "instance", + ExecutionID: "request./zitadel.session.v2.SessionService/SetSession", + TargetID: "target", + TargetType: domain.TargetTypeWebhook, + Timeout: time.Minute, + InterruptOnError: true, + SigningKey: "signingkey", + }, + }, + targets: []target{ + { + reqBody: newMockContextInfoRequest("/service/method", "content"), + sleep: 0, + statusCode: http.StatusInternalServerError, + }, + }, + req: newMockContentRequest("content"), + }, + res{ + wantErr: true, + }, + }, + { + "webhook, timeout", + args{ + ctx: context.Background(), + fullMethod: "/service/method", + executionTargets: []execution.Target{ + &mockExecutionTarget{ + InstanceID: "instance", + ExecutionID: "request./zitadel.session.v2.SessionService/SetSession", + TargetID: "target", + TargetType: domain.TargetTypeWebhook, + Timeout: time.Second, + InterruptOnError: true, + SigningKey: "signingkey", + }, + }, + targets: []target{ + { + reqBody: newMockContextInfoRequest("/service/method", "content"), + respBody: newMockContentResponse("content1"), + sleep: 5 * time.Second, + statusCode: http.StatusOK, + }, + }, + req: newMockContentRequest("content"), + }, + res{ + wantErr: true, + }, + }, + { + "webhook, ok", + args{ + ctx: context.Background(), + fullMethod: "/service/method", + executionTargets: []execution.Target{ + &mockExecutionTarget{ + InstanceID: "instance", + ExecutionID: "request./zitadel.session.v2.SessionService/SetSession", + TargetID: "target", + TargetType: domain.TargetTypeWebhook, + Timeout: time.Minute, + InterruptOnError: true, + SigningKey: "signingkey", + }, + }, + targets: []target{ + { + reqBody: newMockContextInfoRequest("/service/method", "content"), + respBody: newMockContentResponse("content1"), + sleep: 0, + statusCode: http.StatusOK, + }, + }, + req: newMockContentRequest("content"), + }, + res{ + want: newMockContentRequest("content"), + }, + }, + { + "with includes, interruptOnError", + args{ + ctx: context.Background(), + fullMethod: "/service/method", + executionTargets: []execution.Target{ + &mockExecutionTarget{ + InstanceID: "instance", + ExecutionID: "request./zitadel.session.v2.SessionService/SetSession", + TargetID: "target1", + TargetType: domain.TargetTypeCall, + Timeout: time.Minute, + InterruptOnError: true, + SigningKey: "signingkey", + }, + &mockExecutionTarget{ + InstanceID: "instance", + ExecutionID: "request./zitadel.session.v2.SessionService/SetSession", + TargetID: "target2", + TargetType: domain.TargetTypeCall, + Timeout: time.Minute, + InterruptOnError: true, + SigningKey: "signingkey", + }, + &mockExecutionTarget{ + InstanceID: "instance", + ExecutionID: "request./zitadel.session.v2.SessionService/SetSession", + TargetID: "target3", + TargetType: domain.TargetTypeCall, + Timeout: time.Minute, + InterruptOnError: true, + SigningKey: "signingkey", + }, + }, + + targets: []target{ + { + reqBody: newMockContextInfoRequest("/service/method", "content"), + respBody: newMockContentResponse("content1"), + sleep: 0, + statusCode: http.StatusOK, + }, + { + reqBody: newMockContextInfoRequest("/service/method", "content1"), + respBody: newMockContentResponse("content2"), + sleep: 0, + statusCode: http.StatusBadRequest, + }, + { + reqBody: newMockContextInfoRequest("/service/method", "content2"), + respBody: newMockContentResponse("content3"), + sleep: 0, + statusCode: http.StatusOK, + }, + }, + req: newMockContentRequest("content"), + }, + res{ + wantErr: true, + }, + }, + { + "with includes, timeout", + args{ + ctx: context.Background(), + fullMethod: "/service/method", + executionTargets: []execution.Target{ + &mockExecutionTarget{ + InstanceID: "instance", + ExecutionID: "request./zitadel.session.v2.SessionService/SetSession", + TargetID: "target1", + TargetType: domain.TargetTypeCall, + Timeout: time.Minute, + InterruptOnError: true, + SigningKey: "signingkey", + }, + &mockExecutionTarget{ + InstanceID: "instance", + ExecutionID: "request./zitadel.session.v2.SessionService/SetSession", + TargetID: "target2", + TargetType: domain.TargetTypeCall, + Timeout: time.Second, + InterruptOnError: true, + SigningKey: "signingkey", + }, + &mockExecutionTarget{ + InstanceID: "instance", + ExecutionID: "request./zitadel.session.v2.SessionService/SetSession", + TargetID: "target3", + TargetType: domain.TargetTypeCall, + Timeout: time.Second, + InterruptOnError: true, + SigningKey: "signingkey", + }, + }, + targets: []target{ + { + reqBody: newMockContextInfoRequest("/service/method", "content"), + respBody: newMockContentResponse("content1"), + sleep: 0, + statusCode: http.StatusOK, + }, + { + reqBody: newMockContextInfoRequest("/service/method", "content1"), + respBody: newMockContentResponse("content2"), + sleep: 5 * time.Second, + statusCode: http.StatusBadRequest, + }, + { + reqBody: newMockContextInfoRequest("/service/method", "content2"), + respBody: newMockContentResponse("content3"), + sleep: 5 * time.Second, + statusCode: http.StatusOK, + }, + }, + req: newMockContentRequest("content"), + }, + res{ + wantErr: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + closeFuncs := make([]func(), len(tt.args.targets)) + for i, target := range tt.args.targets { + url, closeF := testServerCall( + target.reqBody, + target.sleep, + target.statusCode, + target.respBody, + ) + + et := tt.args.executionTargets[i].(*mockExecutionTarget) + et.SetEndpoint(url) + closeFuncs[i] = closeF + } + + resp, err := executeTargetsForRequest( + tt.args.ctx, + tt.args.executionTargets, + tt.args.fullMethod, + tt.args.req, + ) + + if tt.res.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + assert.EqualExportedValues(t, tt.res.want, resp) + + for _, closeF := range closeFuncs { + closeF() + } + }) + } +} + +func testServerCall( + reqBody interface{}, + sleep time.Duration, + statusCode int, + respBody connect.AnyResponse, +) (string, func()) { + handler := func(w http.ResponseWriter, r *http.Request) { + data, err := json.Marshal(reqBody) + if err != nil { + http.Error(w, "error", http.StatusInternalServerError) + return + } + + sentBody, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, "error", http.StatusInternalServerError) + return + } + + if !reflect.DeepEqual(data, sentBody) { + http.Error(w, "error", http.StatusInternalServerError) + return + } + + if statusCode != http.StatusOK { + http.Error(w, "error", statusCode) + return + } + + time.Sleep(sleep) + + w.Header().Set("Content-Type", "application/json") + resp, err := protojson.Marshal(respBody.Any().(proto.Message)) + if err != nil { + http.Error(w, "error", http.StatusInternalServerError) + return + } + if _, err := w.Write(resp); err != nil { + http.Error(w, "error", http.StatusInternalServerError) + return + } + } + + server := httptest.NewServer(http.HandlerFunc(handler)) + + return server.URL, server.Close +} + +func Test_executeTargetsForGRPCFullMethod_response(t *testing.T) { + type target struct { + reqBody execution.ContextInfo + sleep time.Duration + statusCode int + respBody connect.AnyResponse + } + type args struct { + ctx context.Context + + executionTargets []execution.Target + targets []target + fullMethod string + req connect.AnyRequest + resp connect.AnyResponse + } + type res struct { + want interface{} + wantErr bool + } + tests := []struct { + name string + args args + res res + }{ + { + "target, executionTargets nil", + args{ + ctx: context.Background(), + fullMethod: "/service/method", + executionTargets: nil, + req: newMockContentRequest("request"), + resp: newMockContentResponse("response"), + }, + res{ + want: newMockContentResponse("response"), + }, + }, + { + "target, executionTargets empty", + args{ + ctx: context.Background(), + fullMethod: "/service/method", + executionTargets: []execution.Target{}, + req: newMockContentRequest("request"), + resp: newMockContentResponse("response"), + }, + res{ + want: newMockContentResponse("response"), + }, + }, + { + "target, empty response", + args{ + ctx: context.Background(), + fullMethod: "/service/method", + executionTargets: []execution.Target{ + &mockExecutionTarget{ + InstanceID: "instance", + ExecutionID: "request./zitadel.session.v2.SessionService/SetSession", + TargetID: "target", + TargetType: domain.TargetTypeCall, + Timeout: time.Minute, + InterruptOnError: true, + SigningKey: "signingkey", + }, + }, + targets: []target{ + { + reqBody: newMockContextInfoRequest("/service/method", "content"), + respBody: newMockContentResponse(""), + sleep: 0, + statusCode: http.StatusOK, + }, + }, + req: newMockContentRequest(""), + resp: newMockContentResponse(""), + }, + res{ + wantErr: true, + }, + }, + { + "target, ok", + args{ + ctx: context.Background(), + fullMethod: "/service/method", + executionTargets: []execution.Target{ + &mockExecutionTarget{ + InstanceID: "instance", + ExecutionID: "response./zitadel.session.v2.SessionService/SetSession", + TargetID: "target", + TargetType: domain.TargetTypeCall, + Timeout: time.Minute, + InterruptOnError: true, + SigningKey: "signingkey", + }, + }, + targets: []target{ + { + reqBody: newMockContextInfoResponse("/service/method", "request", "response"), + respBody: newMockContentResponse("response1"), + sleep: 0, + statusCode: http.StatusOK, + }, + }, + req: newMockContentRequest("request"), + resp: newMockContentResponse("response"), + }, + res{ + want: newMockContentResponse("response1"), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + closeFuncs := make([]func(), len(tt.args.targets)) + for i, target := range tt.args.targets { + url, closeF := testServerCall( + target.reqBody, + target.sleep, + target.statusCode, + target.respBody, + ) + + et := tt.args.executionTargets[i].(*mockExecutionTarget) + et.SetEndpoint(url) + closeFuncs[i] = closeF + } + + resp, err := executeTargetsForResponse( + tt.args.ctx, + tt.args.executionTargets, + tt.args.fullMethod, + tt.args.req, + tt.args.resp, + ) + + if tt.res.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + assert.EqualExportedValues(t, tt.res.want, resp) + + for _, closeF := range closeFuncs { + closeF() + } + }) + } +} diff --git a/internal/api/grpc/server/connect_middleware/instance_interceptor.go b/internal/api/grpc/server/connect_middleware/instance_interceptor.go new file mode 100644 index 0000000000..27f59313f8 --- /dev/null +++ b/internal/api/grpc/server/connect_middleware/instance_interceptor.go @@ -0,0 +1,107 @@ +package connect_middleware + +import ( + "context" + "errors" + "fmt" + "strings" + + "connectrpc.com/connect" + "github.com/zitadel/logging" + "golang.org/x/text/language" + + "github.com/zitadel/zitadel/internal/api/authz" + zitadel_http "github.com/zitadel/zitadel/internal/api/http" + "github.com/zitadel/zitadel/internal/i18n" + "github.com/zitadel/zitadel/internal/telemetry/tracing" + "github.com/zitadel/zitadel/internal/zerrors" + object_v3 "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha" +) + +func InstanceInterceptor(verifier authz.InstanceVerifier, externalDomain string, explicitInstanceIdServices ...string) connect.UnaryInterceptorFunc { + translator, err := i18n.NewZitadelTranslator(language.English) + logging.OnError(err).Panic("unable to get translator") + return func(handler connect.UnaryFunc) connect.UnaryFunc { + return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) { + return setInstance(ctx, req, handler, verifier, externalDomain, translator, explicitInstanceIdServices...) + } + } +} + +func setInstance(ctx context.Context, req connect.AnyRequest, handler connect.UnaryFunc, verifier authz.InstanceVerifier, externalDomain string, translator *i18n.Translator, idFromRequestsServices ...string) (_ connect.AnyResponse, err error) { + interceptorCtx, span := tracing.NewServerInterceptorSpan(ctx) + defer func() { span.EndWithError(err) }() + + for _, service := range idFromRequestsServices { + if !strings.HasPrefix(service, "/") { + service = "/" + service + } + if strings.HasPrefix(req.Spec().Procedure, service) { + withInstanceIDProperty, ok := req.Any().(interface { + GetInstanceId() string + }) + if !ok { + return handler(ctx, req) + } + return addInstanceByID(interceptorCtx, req, handler, verifier, translator, withInstanceIDProperty.GetInstanceId()) + } + } + explicitInstanceRequest, ok := req.Any().(interface { + GetInstance() *object_v3.Instance + }) + if ok { + instance := explicitInstanceRequest.GetInstance() + if id := instance.GetId(); id != "" { + return addInstanceByID(interceptorCtx, req, handler, verifier, translator, id) + } + if domain := instance.GetDomain(); domain != "" { + return addInstanceByDomain(interceptorCtx, req, handler, verifier, translator, domain) + } + } + return addInstanceByRequestedHost(interceptorCtx, req, handler, verifier, translator, externalDomain) +} + +func addInstanceByID(ctx context.Context, req connect.AnyRequest, handler connect.UnaryFunc, verifier authz.InstanceVerifier, translator *i18n.Translator, id string) (connect.AnyResponse, error) { + instance, err := verifier.InstanceByID(ctx, id) + if err != nil { + notFoundErr := new(zerrors.ZitadelError) + if errors.As(err, ¬FoundErr) { + notFoundErr.Message = translator.LocalizeFromCtx(ctx, notFoundErr.GetMessage(), nil) + } + return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("unable to set instance using id %s: %w", id, notFoundErr)) + } + return handler(authz.WithInstance(ctx, instance), req) +} + +func addInstanceByDomain(ctx context.Context, req connect.AnyRequest, handler connect.UnaryFunc, verifier authz.InstanceVerifier, translator *i18n.Translator, domain string) (connect.AnyResponse, error) { + instance, err := verifier.InstanceByHost(ctx, domain, "") + if err != nil { + notFoundErr := new(zerrors.NotFoundError) + if errors.As(err, ¬FoundErr) { + notFoundErr.Message = translator.LocalizeFromCtx(ctx, notFoundErr.GetMessage(), nil) + } + return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("unable to set instance using domain %s: %w", domain, notFoundErr)) + } + return handler(authz.WithInstance(ctx, instance), req) +} + +func addInstanceByRequestedHost(ctx context.Context, req connect.AnyRequest, handler connect.UnaryFunc, verifier authz.InstanceVerifier, translator *i18n.Translator, externalDomain string) (connect.AnyResponse, error) { + requestContext := zitadel_http.DomainContext(ctx) + if requestContext.InstanceHost == "" { + logging.WithFields("origin", requestContext.Origin(), "externalDomain", externalDomain).Error("unable to set instance") + return nil, connect.NewError(connect.CodeNotFound, errors.New("no instanceHost specified")) + } + instance, err := verifier.InstanceByHost(ctx, requestContext.InstanceHost, requestContext.PublicHost) + if err != nil { + origin := zitadel_http.DomainContext(ctx) + logging.WithFields("origin", requestContext.Origin(), "externalDomain", externalDomain).WithError(err).Error("unable to set instance") + zErr := new(zerrors.ZitadelError) + if errors.As(err, &zErr) { + zErr.SetMessage(translator.LocalizeFromCtx(ctx, zErr.GetMessage(), nil)) + zErr.Parent = err + return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("unable to set instance using origin %s (ExternalDomain is %s): %s", origin, externalDomain, zErr.Error())) + } + return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("unable to set instance using origin %s (ExternalDomain is %s)", origin, externalDomain)) + } + return handler(authz.WithInstance(ctx, instance), req) +} diff --git a/internal/api/grpc/server/connect_middleware/limits_interceptor.go b/internal/api/grpc/server/connect_middleware/limits_interceptor.go new file mode 100644 index 0000000000..abf7e5f0aa --- /dev/null +++ b/internal/api/grpc/server/connect_middleware/limits_interceptor.go @@ -0,0 +1,34 @@ +package connect_middleware + +import ( + "context" + "strings" + + "connectrpc.com/connect" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/zerrors" +) + +func LimitsInterceptor(ignoreService ...string) connect.UnaryInterceptorFunc { + for idx, service := range ignoreService { + if !strings.HasPrefix(service, "/") { + ignoreService[idx] = "/" + service + } + } + + return func(handler connect.UnaryFunc) connect.UnaryFunc { + return func(ctx context.Context, req connect.AnyRequest) (_ connect.AnyResponse, err error) { + for _, service := range ignoreService { + if strings.HasPrefix(req.Spec().Procedure, service) { + return handler(ctx, req) + } + } + instance := authz.GetInstance(ctx) + if block := instance.Block(); block != nil && *block { + return nil, zerrors.ThrowResourceExhausted(nil, "LIMITS-molsj", "Errors.Limits.Instance.Blocked") + } + return handler(ctx, req) + } + } +} diff --git a/internal/api/grpc/server/connect_middleware/metrics_interceptor.go b/internal/api/grpc/server/connect_middleware/metrics_interceptor.go new file mode 100644 index 0000000000..552fa5658d --- /dev/null +++ b/internal/api/grpc/server/connect_middleware/metrics_interceptor.go @@ -0,0 +1,96 @@ +package connect_middleware + +import ( + "context" + "strings" + + "connectrpc.com/connect" + "github.com/grpc-ecosystem/grpc-gateway/runtime" + "github.com/zitadel/logging" + "go.opentelemetry.io/otel/attribute" + "google.golang.org/grpc/codes" + + _ "github.com/zitadel/zitadel/internal/statik" + "github.com/zitadel/zitadel/internal/telemetry/metrics" +) + +const ( + GrpcMethod = "grpc_method" + ReturnCode = "return_code" + GrpcRequestCounter = "grpc.server.request_counter" + GrpcRequestCounterDescription = "Grpc request counter" + TotalGrpcRequestCounter = "grpc.server.total_request_counter" + TotalGrpcRequestCounterDescription = "Total grpc request counter" + GrpcStatusCodeCounter = "grpc.server.grpc_status_code" + GrpcStatusCodeCounterDescription = "Grpc status code counter" +) + +func MetricsHandler(metricTypes []metrics.MetricType, ignoredMethodSuffixes ...string) connect.UnaryInterceptorFunc { + return func(handler connect.UnaryFunc) connect.UnaryFunc { + return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) { + return RegisterMetrics(ctx, req, handler, metricTypes, ignoredMethodSuffixes...) + } + } +} + +func RegisterMetrics(ctx context.Context, req connect.AnyRequest, handler connect.UnaryFunc, metricTypes []metrics.MetricType, ignoredMethodSuffixes ...string) (_ connect.AnyResponse, err error) { + if len(metricTypes) == 0 { + return handler(ctx, req) + } + + for _, ignore := range ignoredMethodSuffixes { + if strings.HasSuffix(req.Spec().Procedure, ignore) { + return handler(ctx, req) + } + } + + resp, err := handler(ctx, req) + if containsMetricsMethod(metrics.MetricTypeRequestCount, metricTypes) { + RegisterGrpcRequestCounter(ctx, req.Spec().Procedure) + } + if containsMetricsMethod(metrics.MetricTypeTotalCount, metricTypes) { + RegisterGrpcTotalRequestCounter(ctx) + } + if containsMetricsMethod(metrics.MetricTypeStatusCode, metricTypes) { + RegisterGrpcRequestCodeCounter(ctx, req.Spec().Procedure, err) + } + return resp, err +} + +func RegisterGrpcRequestCounter(ctx context.Context, path string) { + var labels = map[string]attribute.Value{ + GrpcMethod: attribute.StringValue(path), + } + err := metrics.RegisterCounter(GrpcRequestCounter, GrpcRequestCounterDescription) + logging.OnError(err).Warn("failed to register grpc request counter") + err = metrics.AddCount(ctx, GrpcRequestCounter, 1, labels) + logging.OnError(err).Warn("failed to add grpc request count") +} + +func RegisterGrpcTotalRequestCounter(ctx context.Context) { + err := metrics.RegisterCounter(TotalGrpcRequestCounter, TotalGrpcRequestCounterDescription) + logging.OnError(err).Warn("failed to register total grpc request counter") + err = metrics.AddCount(ctx, TotalGrpcRequestCounter, 1, nil) + logging.OnError(err).Warn("failed to add total grpc request count") +} + +func RegisterGrpcRequestCodeCounter(ctx context.Context, path string, err error) { + statusCode := connect.CodeOf(err) + var labels = map[string]attribute.Value{ + GrpcMethod: attribute.StringValue(path), + ReturnCode: attribute.IntValue(runtime.HTTPStatusFromCode(codes.Code(statusCode))), + } + err = metrics.RegisterCounter(GrpcStatusCodeCounter, GrpcStatusCodeCounterDescription) + logging.OnError(err).Warn("failed to register grpc status code counter") + err = metrics.AddCount(ctx, GrpcStatusCodeCounter, 1, labels) + logging.OnError(err).Warn("failed to add grpc status code count") +} + +func containsMetricsMethod(metricType metrics.MetricType, metricTypes []metrics.MetricType) bool { + for _, m := range metricTypes { + if m == metricType { + return true + } + } + return false +} diff --git a/internal/api/grpc/server/connect_middleware/mock_test.go b/internal/api/grpc/server/connect_middleware/mock_test.go new file mode 100644 index 0000000000..abd996b01f --- /dev/null +++ b/internal/api/grpc/server/connect_middleware/mock_test.go @@ -0,0 +1,50 @@ +package connect_middleware + +import ( + "context" + "net/http" + "testing" + + "connectrpc.com/connect" + "github.com/stretchr/testify/assert" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/zerrors" +) + +func emptyMockHandler(resp connect.AnyResponse, expectedCtxData authz.CtxData) func(*testing.T) connect.UnaryFunc { + return func(t *testing.T) connect.UnaryFunc { + return func(ctx context.Context, _ connect.AnyRequest) (connect.AnyResponse, error) { + assert.Equal(t, expectedCtxData, authz.GetCtxData(ctx)) + return resp, nil + } + } +} + +func errorMockHandler() func(*testing.T) connect.UnaryFunc { + return func(t *testing.T) connect.UnaryFunc { + return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) { + return nil, zerrors.ThrowInternal(nil, "test", "error") + } + } +} + +type mockReq[t any] struct { + connect.Request[t] + + procedure string + header http.Header +} + +func (m *mockReq[T]) Spec() connect.Spec { + return connect.Spec{ + Procedure: m.procedure, + } +} + +func (m *mockReq[T]) Header() http.Header { + if m.header == nil { + m.header = make(http.Header) + } + return m.header +} diff --git a/internal/api/grpc/server/connect_middleware/quota_interceptor.go b/internal/api/grpc/server/connect_middleware/quota_interceptor.go new file mode 100644 index 0000000000..caa32511e4 --- /dev/null +++ b/internal/api/grpc/server/connect_middleware/quota_interceptor.go @@ -0,0 +1,53 @@ +package connect_middleware + +import ( + "context" + "strings" + + "connectrpc.com/connect" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/logstore" + "github.com/zitadel/zitadel/internal/logstore/record" + "github.com/zitadel/zitadel/internal/telemetry/tracing" + "github.com/zitadel/zitadel/internal/zerrors" +) + +func QuotaExhaustedInterceptor(svc *logstore.Service[*record.AccessLog], ignoreService ...string) connect.UnaryInterceptorFunc { + for idx, service := range ignoreService { + if !strings.HasPrefix(service, "/") { + ignoreService[idx] = "/" + service + } + } + return func(handler connect.UnaryFunc) connect.UnaryFunc { + return func(ctx context.Context, req connect.AnyRequest) (_ connect.AnyResponse, err error) { + if !svc.Enabled() { + return handler(ctx, req) + } + interceptorCtx, span := tracing.NewServerInterceptorSpan(ctx) + defer func() { span.EndWithError(err) }() + + // The auth interceptor will ensure that only authorized or public requests are allowed. + // So if there's no authorization context, we don't need to check for limitation + // Also, we don't limit calls with system user tokens + ctxData := authz.GetCtxData(ctx) + if ctxData.IsZero() || ctxData.SystemMemberships != nil { + return handler(ctx, req) + } + + for _, service := range ignoreService { + if strings.HasPrefix(req.Spec().Procedure, service) { + return handler(ctx, req) + } + } + + instance := authz.GetInstance(ctx) + remaining := svc.Limit(interceptorCtx, instance.InstanceID()) + if remaining != nil && *remaining == 0 { + return nil, zerrors.ThrowResourceExhausted(nil, "QUOTA-vjAy8", "Quota.Access.Exhausted") + } + span.End() + return handler(ctx, req) + } + } +} diff --git a/internal/api/grpc/server/connect_middleware/service_interceptor.go b/internal/api/grpc/server/connect_middleware/service_interceptor.go new file mode 100644 index 0000000000..c5cf798ce5 --- /dev/null +++ b/internal/api/grpc/server/connect_middleware/service_interceptor.go @@ -0,0 +1,45 @@ +package connect_middleware + +import ( + "context" + "strings" + + "connectrpc.com/connect" + + "github.com/zitadel/zitadel/internal/api/service" + _ "github.com/zitadel/zitadel/internal/statik" +) + +const ( + unknown = "UNKNOWN" +) + +func ServiceHandler() connect.UnaryInterceptorFunc { + return func(handler connect.UnaryFunc) connect.UnaryFunc { + return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) { + serviceName, _ := serviceAndMethod(req.Spec().Procedure) + if serviceName != unknown { + return handler(ctx, req) + } + ctx = service.WithService(ctx, serviceName) + return handler(ctx, req) + } + } +} + +// serviceAndMethod returns the service and method from a procedure. +func serviceAndMethod(procedure string) (string, string) { + procedure = strings.TrimPrefix(procedure, "/") + serviceName, method := unknown, unknown + if strings.Contains(procedure, "/") { + long := strings.Split(procedure, "/")[0] + if strings.Contains(long, ".") { + split := strings.Split(long, ".") + serviceName = split[len(split)-1] + } + } + if strings.Contains(procedure, "/") { + method = strings.Split(procedure, "/")[1] + } + return serviceName, method +} diff --git a/internal/api/grpc/server/connect_middleware/translation_interceptor.go b/internal/api/grpc/server/connect_middleware/translation_interceptor.go new file mode 100644 index 0000000000..f01b1c85ab --- /dev/null +++ b/internal/api/grpc/server/connect_middleware/translation_interceptor.go @@ -0,0 +1,48 @@ +package connect_middleware + +import ( + "context" + + "connectrpc.com/connect" + "github.com/zitadel/logging" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/i18n" + _ "github.com/zitadel/zitadel/internal/statik" + "github.com/zitadel/zitadel/internal/telemetry/tracing" +) + +func TranslationHandler() connect.UnaryInterceptorFunc { + + return func(handler connect.UnaryFunc) connect.UnaryFunc { + return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) { + resp, err := handler(ctx, req) + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + if err != nil { + translator, translatorError := getTranslator(ctx) + if translatorError != nil { + return resp, err + } + return resp, translateError(ctx, err, translator) + } + if loc, ok := resp.Any().(localizers); ok { + translator, translatorError := getTranslator(ctx) + if translatorError != nil { + return resp, err + } + translateFields(ctx, loc, translator) + } + return resp, nil + } + } +} + +func getTranslator(ctx context.Context) (*i18n.Translator, error) { + translator, err := i18n.NewZitadelTranslator(authz.GetInstance(ctx).DefaultLanguage()) + if err != nil { + logging.New().WithError(err).Error("could not load translator") + } + return translator, err +} diff --git a/internal/api/grpc/server/connect_middleware/translator.go b/internal/api/grpc/server/connect_middleware/translator.go new file mode 100644 index 0000000000..6d61b1d772 --- /dev/null +++ b/internal/api/grpc/server/connect_middleware/translator.go @@ -0,0 +1,37 @@ +package connect_middleware + +import ( + "context" + "errors" + + "github.com/zitadel/zitadel/internal/i18n" + "github.com/zitadel/zitadel/internal/zerrors" +) + +type localizers interface { + Localizers() []Localizer +} +type Localizer interface { + LocalizationKey() string + SetLocalizedMessage(string) +} + +func translateFields(ctx context.Context, object localizers, translator *i18n.Translator) { + if translator == nil || object == nil { + return + } + for _, field := range object.Localizers() { + field.SetLocalizedMessage(translator.LocalizeFromCtx(ctx, field.LocalizationKey(), nil)) + } +} + +func translateError(ctx context.Context, err error, translator *i18n.Translator) error { + if translator == nil || err == nil { + return err + } + caosErr := new(zerrors.ZitadelError) + if errors.As(err, &caosErr) { + caosErr.SetMessage(translator.LocalizeFromCtx(ctx, caosErr.GetMessage(), nil)) + } + return err +} diff --git a/internal/api/grpc/server/connect_middleware/validation_interceptor.go b/internal/api/grpc/server/connect_middleware/validation_interceptor.go new file mode 100644 index 0000000000..8441886114 --- /dev/null +++ b/internal/api/grpc/server/connect_middleware/validation_interceptor.go @@ -0,0 +1,36 @@ +package connect_middleware + +import ( + "context" + + "connectrpc.com/connect" + // import to make sure go.mod does not lose it + // because dependency is only needed for generated code + _ "github.com/envoyproxy/protoc-gen-validate/validate" +) + +func ValidationHandler() connect.UnaryInterceptorFunc { + return func(handler connect.UnaryFunc) connect.UnaryFunc { + return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) { + return validate(ctx, req, handler) + } + } +} + +// validator interface needed for github.com/envoyproxy/protoc-gen-validate +// (it does not expose an interface itself) +type validator interface { + Validate() error +} + +func validate(ctx context.Context, req connect.AnyRequest, handler connect.UnaryFunc) (connect.AnyResponse, error) { + validate, ok := req.Any().(validator) + if !ok { + return handler(ctx, req) + } + err := validate.Validate() + if err != nil { + return nil, connect.NewError(connect.CodeInvalidArgument, err) + } + return handler(ctx, req) +} diff --git a/internal/api/grpc/server/gateway.go b/internal/api/grpc/server/gateway.go index ca7579ee89..b20819b850 100644 --- a/internal/api/grpc/server/gateway.go +++ b/internal/api/grpc/server/gateway.go @@ -171,7 +171,7 @@ func CreateGateway( }, nil } -func RegisterGateway(ctx context.Context, gateway *Gateway, server Server) error { +func RegisterGateway(ctx context.Context, gateway *Gateway, server WithGateway) error { err := server.RegisterGateway()(ctx, gateway.mux, gateway.connection) if err != nil { return fmt.Errorf("failed to register grpc gateway: %w", err) diff --git a/internal/api/grpc/server/server.go b/internal/api/grpc/server/server.go index b686d3add9..0c02087c89 100644 --- a/internal/api/grpc/server/server.go +++ b/internal/api/grpc/server/server.go @@ -2,11 +2,14 @@ package server import ( "crypto/tls" + "net/http" + "connectrpc.com/connect" grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware" "google.golang.org/grpc" "google.golang.org/grpc/credentials" healthpb "google.golang.org/grpc/health/grpc_health_v1" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" "github.com/zitadel/zitadel/internal/api/authz" grpc_api "github.com/zitadel/zitadel/internal/api/grpc" @@ -19,21 +22,36 @@ import ( ) type Server interface { - RegisterServer(*grpc.Server) - RegisterGateway() RegisterGatewayFunc AppName() string MethodPrefix() string AuthMethods() authz.MethodMapping } +type GrpcServer interface { + Server + RegisterServer(*grpc.Server) +} + +type WithGateway interface { + Server + RegisterGateway() RegisterGatewayFunc +} + // WithGatewayPrefix extends the server interface with a prefix for the grpc gateway // // it's used for the System, Admin, Mgmt and Auth API type WithGatewayPrefix interface { - Server + GrpcServer + WithGateway GatewayPathPrefix() string } +type ConnectServer interface { + Server + RegisterConnectServer(interceptors ...connect.Interceptor) (string, http.Handler) + FileDescriptor() protoreflect.FileDescriptor +} + func CreateServer( verifier authz.APITokenVerifier, systemAuthz authz.Config, diff --git a/internal/api/grpc/session/v2/query.go b/internal/api/grpc/session/v2/query.go index 73303dd9e8..78d8623ee7 100644 --- a/internal/api/grpc/session/v2/query.go +++ b/internal/api/grpc/session/v2/query.go @@ -4,6 +4,7 @@ import ( "context" "time" + "connectrpc.com/connect" "github.com/muhlemmer/gu" "google.golang.org/protobuf/types/known/timestamppb" @@ -26,18 +27,18 @@ var ( } ) -func (s *Server) GetSession(ctx context.Context, req *session.GetSessionRequest) (*session.GetSessionResponse, error) { - res, err := s.query.SessionByID(ctx, true, req.GetSessionId(), req.GetSessionToken(), s.checkPermission) +func (s *Server) GetSession(ctx context.Context, req *connect.Request[session.GetSessionRequest]) (*connect.Response[session.GetSessionResponse], error) { + res, err := s.query.SessionByID(ctx, true, req.Msg.GetSessionId(), req.Msg.GetSessionToken(), s.checkPermission) if err != nil { return nil, err } - return &session.GetSessionResponse{ + return connect.NewResponse(&session.GetSessionResponse{ Session: sessionToPb(res), - }, nil + }), nil } -func (s *Server) ListSessions(ctx context.Context, req *session.ListSessionsRequest) (*session.ListSessionsResponse, error) { - queries, err := listSessionsRequestToQuery(ctx, req) +func (s *Server) ListSessions(ctx context.Context, req *connect.Request[session.ListSessionsRequest]) (*connect.Response[session.ListSessionsResponse], error) { + queries, err := listSessionsRequestToQuery(ctx, req.Msg) if err != nil { return nil, err } @@ -45,10 +46,10 @@ func (s *Server) ListSessions(ctx context.Context, req *session.ListSessionsRequ if err != nil { return nil, err } - return &session.ListSessionsResponse{ + return connect.NewResponse(&session.ListSessionsResponse{ Details: object.ToListDetails(sessions.SearchResponse), Sessions: sessionsToPb(sessions.Sessions), - }, nil + }), nil } func listSessionsRequestToQuery(ctx context.Context, req *session.ListSessionsRequest) (*query.SessionsSearchQueries, error) { diff --git a/internal/api/grpc/session/v2/server.go b/internal/api/grpc/session/v2/server.go index ee534cb26c..8f06cb3fb0 100644 --- a/internal/api/grpc/session/v2/server.go +++ b/internal/api/grpc/session/v2/server.go @@ -1,7 +1,10 @@ package session import ( - "google.golang.org/grpc" + "net/http" + + "connectrpc.com/connect" + "google.golang.org/protobuf/reflect/protoreflect" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/grpc/server" @@ -9,12 +12,12 @@ import ( "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/pkg/grpc/session/v2" + "github.com/zitadel/zitadel/pkg/grpc/session/v2/sessionconnect" ) -var _ session.SessionServiceServer = (*Server)(nil) +var _ sessionconnect.SessionServiceHandler = (*Server)(nil) type Server struct { - session.UnimplementedSessionServiceServer command *command.Commands query *query.Queries @@ -35,8 +38,12 @@ func CreateServer( } } -func (s *Server) RegisterServer(grpcServer *grpc.Server) { - session.RegisterSessionServiceServer(grpcServer, s) +func (s *Server) RegisterConnectServer(interceptors ...connect.Interceptor) (string, http.Handler) { + return sessionconnect.NewSessionServiceHandler(s, connect.WithInterceptors(interceptors...)) +} + +func (s *Server) FileDescriptor() protoreflect.FileDescriptor { + return session.File_zitadel_session_v2_session_service_proto } func (s *Server) AppName() string { diff --git a/internal/api/grpc/session/v2/session.go b/internal/api/grpc/session/v2/session.go index 6e3949d293..99e876d06e 100644 --- a/internal/api/grpc/session/v2/session.go +++ b/internal/api/grpc/session/v2/session.go @@ -6,6 +6,7 @@ import ( "net/http" "time" + "connectrpc.com/connect" "golang.org/x/text/language" "google.golang.org/protobuf/types/known/structpb" @@ -17,12 +18,12 @@ import ( "github.com/zitadel/zitadel/pkg/grpc/session/v2" ) -func (s *Server) CreateSession(ctx context.Context, req *session.CreateSessionRequest) (*session.CreateSessionResponse, error) { - checks, metadata, userAgent, lifetime, err := s.createSessionRequestToCommand(ctx, req) +func (s *Server) CreateSession(ctx context.Context, req *connect.Request[session.CreateSessionRequest]) (*connect.Response[session.CreateSessionResponse], error) { + checks, metadata, userAgent, lifetime, err := s.createSessionRequestToCommand(ctx, req.Msg) if err != nil { return nil, err } - challengeResponse, cmds, err := s.challengesToCommand(req.GetChallenges(), checks) + challengeResponse, cmds, err := s.challengesToCommand(req.Msg.GetChallenges(), checks) if err != nil { return nil, err } @@ -32,43 +33,43 @@ func (s *Server) CreateSession(ctx context.Context, req *session.CreateSessionRe return nil, err } - return &session.CreateSessionResponse{ + return connect.NewResponse(&session.CreateSessionResponse{ Details: object.DomainToDetailsPb(set.ObjectDetails), SessionId: set.ID, SessionToken: set.NewToken, Challenges: challengeResponse, - }, nil + }), nil } -func (s *Server) SetSession(ctx context.Context, req *session.SetSessionRequest) (*session.SetSessionResponse, error) { - checks, err := s.setSessionRequestToCommand(ctx, req) +func (s *Server) SetSession(ctx context.Context, req *connect.Request[session.SetSessionRequest]) (*connect.Response[session.SetSessionResponse], error) { + checks, err := s.setSessionRequestToCommand(ctx, req.Msg) if err != nil { return nil, err } - challengeResponse, cmds, err := s.challengesToCommand(req.GetChallenges(), checks) + challengeResponse, cmds, err := s.challengesToCommand(req.Msg.GetChallenges(), checks) if err != nil { return nil, err } - set, err := s.command.UpdateSession(ctx, req.GetSessionId(), req.GetSessionToken(), cmds, req.GetMetadata(), req.GetLifetime().AsDuration()) + set, err := s.command.UpdateSession(ctx, req.Msg.GetSessionId(), req.Msg.GetSessionToken(), cmds, req.Msg.GetMetadata(), req.Msg.GetLifetime().AsDuration()) if err != nil { return nil, err } - return &session.SetSessionResponse{ + return connect.NewResponse(&session.SetSessionResponse{ Details: object.DomainToDetailsPb(set.ObjectDetails), SessionToken: set.NewToken, Challenges: challengeResponse, - }, nil + }), nil } -func (s *Server) DeleteSession(ctx context.Context, req *session.DeleteSessionRequest) (*session.DeleteSessionResponse, error) { - details, err := s.command.TerminateSession(ctx, req.GetSessionId(), req.GetSessionToken()) +func (s *Server) DeleteSession(ctx context.Context, req *connect.Request[session.DeleteSessionRequest]) (*connect.Response[session.DeleteSessionResponse], error) { + details, err := s.command.TerminateSession(ctx, req.Msg.GetSessionId(), req.Msg.GetSessionToken()) if err != nil { return nil, err } - return &session.DeleteSessionResponse{ + return connect.NewResponse(&session.DeleteSessionResponse{ Details: object.DomainToDetailsPb(details), - }, nil + }), nil } func (s *Server) createSessionRequestToCommand(ctx context.Context, req *session.CreateSessionRequest) ([]command.SessionCommand, map[string][]byte, *domain.UserAgent, time.Duration, error) { diff --git a/internal/api/grpc/session/v2beta/server.go b/internal/api/grpc/session/v2beta/server.go index cf0d0c27f0..e659b406eb 100644 --- a/internal/api/grpc/session/v2beta/server.go +++ b/internal/api/grpc/session/v2beta/server.go @@ -1,7 +1,10 @@ package session import ( - "google.golang.org/grpc" + "net/http" + + "connectrpc.com/connect" + "google.golang.org/protobuf/reflect/protoreflect" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/grpc/server" @@ -9,12 +12,12 @@ import ( "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query" session "github.com/zitadel/zitadel/pkg/grpc/session/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/session/v2beta/sessionconnect" ) -var _ session.SessionServiceServer = (*Server)(nil) +var _ sessionconnect.SessionServiceHandler = (*Server)(nil) type Server struct { - session.UnimplementedSessionServiceServer command *command.Commands query *query.Queries @@ -35,8 +38,12 @@ func CreateServer( } } -func (s *Server) RegisterServer(grpcServer *grpc.Server) { - session.RegisterSessionServiceServer(grpcServer, s) +func (s *Server) RegisterConnectServer(interceptors ...connect.Interceptor) (string, http.Handler) { + return sessionconnect.NewSessionServiceHandler(s, connect.WithInterceptors(interceptors...)) +} + +func (s *Server) FileDescriptor() protoreflect.FileDescriptor { + return session.File_zitadel_session_v2beta_session_service_proto } func (s *Server) AppName() string { diff --git a/internal/api/grpc/session/v2beta/session.go b/internal/api/grpc/session/v2beta/session.go index fb32ba7b4d..c5c129fb11 100644 --- a/internal/api/grpc/session/v2beta/session.go +++ b/internal/api/grpc/session/v2beta/session.go @@ -6,6 +6,7 @@ import ( "net/http" "time" + "connectrpc.com/connect" "github.com/muhlemmer/gu" "golang.org/x/text/language" "google.golang.org/protobuf/types/known/structpb" @@ -30,18 +31,18 @@ var ( } ) -func (s *Server) GetSession(ctx context.Context, req *session.GetSessionRequest) (*session.GetSessionResponse, error) { - res, err := s.query.SessionByID(ctx, true, req.GetSessionId(), req.GetSessionToken(), s.checkPermission) +func (s *Server) GetSession(ctx context.Context, req *connect.Request[session.GetSessionRequest]) (*connect.Response[session.GetSessionResponse], error) { + res, err := s.query.SessionByID(ctx, true, req.Msg.GetSessionId(), req.Msg.GetSessionToken(), s.checkPermission) if err != nil { return nil, err } - return &session.GetSessionResponse{ + return connect.NewResponse(&session.GetSessionResponse{ Session: sessionToPb(res), - }, nil + }), nil } -func (s *Server) ListSessions(ctx context.Context, req *session.ListSessionsRequest) (*session.ListSessionsResponse, error) { - queries, err := listSessionsRequestToQuery(ctx, req) +func (s *Server) ListSessions(ctx context.Context, req *connect.Request[session.ListSessionsRequest]) (*connect.Response[session.ListSessionsResponse], error) { + queries, err := listSessionsRequestToQuery(ctx, req.Msg) if err != nil { return nil, err } @@ -49,18 +50,18 @@ func (s *Server) ListSessions(ctx context.Context, req *session.ListSessionsRequ if err != nil { return nil, err } - return &session.ListSessionsResponse{ + return connect.NewResponse(&session.ListSessionsResponse{ Details: object.ToListDetails(sessions.SearchResponse), Sessions: sessionsToPb(sessions.Sessions), - }, nil + }), nil } -func (s *Server) CreateSession(ctx context.Context, req *session.CreateSessionRequest) (*session.CreateSessionResponse, error) { - checks, metadata, userAgent, lifetime, err := s.createSessionRequestToCommand(ctx, req) +func (s *Server) CreateSession(ctx context.Context, req *connect.Request[session.CreateSessionRequest]) (*connect.Response[session.CreateSessionResponse], error) { + checks, metadata, userAgent, lifetime, err := s.createSessionRequestToCommand(ctx, req.Msg) if err != nil { return nil, err } - challengeResponse, cmds, err := s.challengesToCommand(req.GetChallenges(), checks) + challengeResponse, cmds, err := s.challengesToCommand(req.Msg.GetChallenges(), checks) if err != nil { return nil, err } @@ -70,43 +71,43 @@ func (s *Server) CreateSession(ctx context.Context, req *session.CreateSessionRe return nil, err } - return &session.CreateSessionResponse{ + return connect.NewResponse(&session.CreateSessionResponse{ Details: object.DomainToDetailsPb(set.ObjectDetails), SessionId: set.ID, SessionToken: set.NewToken, Challenges: challengeResponse, - }, nil + }), nil } -func (s *Server) SetSession(ctx context.Context, req *session.SetSessionRequest) (*session.SetSessionResponse, error) { - checks, err := s.setSessionRequestToCommand(ctx, req) +func (s *Server) SetSession(ctx context.Context, req *connect.Request[session.SetSessionRequest]) (*connect.Response[session.SetSessionResponse], error) { + checks, err := s.setSessionRequestToCommand(ctx, req.Msg) if err != nil { return nil, err } - challengeResponse, cmds, err := s.challengesToCommand(req.GetChallenges(), checks) + challengeResponse, cmds, err := s.challengesToCommand(req.Msg.GetChallenges(), checks) if err != nil { return nil, err } - set, err := s.command.UpdateSession(ctx, req.GetSessionId(), req.GetSessionToken(), cmds, req.GetMetadata(), req.GetLifetime().AsDuration()) + set, err := s.command.UpdateSession(ctx, req.Msg.GetSessionId(), req.Msg.GetSessionToken(), cmds, req.Msg.GetMetadata(), req.Msg.GetLifetime().AsDuration()) if err != nil { return nil, err } - return &session.SetSessionResponse{ + return connect.NewResponse(&session.SetSessionResponse{ Details: object.DomainToDetailsPb(set.ObjectDetails), SessionToken: set.NewToken, Challenges: challengeResponse, - }, nil + }), nil } -func (s *Server) DeleteSession(ctx context.Context, req *session.DeleteSessionRequest) (*session.DeleteSessionResponse, error) { - details, err := s.command.TerminateSession(ctx, req.GetSessionId(), req.GetSessionToken()) +func (s *Server) DeleteSession(ctx context.Context, req *connect.Request[session.DeleteSessionRequest]) (*connect.Response[session.DeleteSessionResponse], error) { + details, err := s.command.TerminateSession(ctx, req.Msg.GetSessionId(), req.Msg.GetSessionToken()) if err != nil { return nil, err } - return &session.DeleteSessionResponse{ + return connect.NewResponse(&session.DeleteSessionResponse{ Details: object.DomainToDetailsPb(details), - }, nil + }), nil } func sessionsToPb(sessions []*query.Session) []*session.Session { diff --git a/internal/api/grpc/session/v2beta/session_test.go b/internal/api/grpc/session/v2beta/session_test.go index de043ed0e2..16de30a9b1 100644 --- a/internal/api/grpc/session/v2beta/session_test.go +++ b/internal/api/grpc/session/v2beta/session_test.go @@ -328,22 +328,7 @@ func Test_listSessionsRequestToQuery(t *testing.T) { wantErr error }{ { - name: "default request", - args: args{ - ctx: authz.NewMockContext("123", "456", "789"), - req: &session.ListSessionsRequest{}, - }, - want: &query.SessionsSearchQueries{ - SearchRequest: query.SearchRequest{ - Offset: 0, - Limit: 0, - Asc: false, - }, - Queries: []query.SearchQuery{}, - }, - }, - { - name: "default request with sorting column", + name: "sorting column", args: args{ ctx: authz.NewMockContext("123", "456", "789"), req: &session.ListSessionsRequest{ @@ -452,13 +437,6 @@ func Test_sessionQueriesToQuery(t *testing.T) { want []query.SearchQuery wantErr error }{ - { - name: "creator only", - args: args{ - ctx: authz.NewMockContext("123", "456", "789"), - }, - want: []query.SearchQuery{}, - }, { name: "invalid argument", args: args{ @@ -470,7 +448,7 @@ func Test_sessionQueriesToQuery(t *testing.T) { wantErr: zerrors.ThrowInvalidArgument(nil, "GRPC-Sfefs", "List.Query.Invalid"), }, { - name: "creator and sessions", + name: "sessions", args: args{ ctx: authz.NewMockContext("123", "456", "789"), queries: []*session.SearchQuery{ diff --git a/internal/api/grpc/settings/v2/integration_test/query_test.go b/internal/api/grpc/settings/v2/integration_test/query_test.go new file mode 100644 index 0000000000..c3bf54e992 --- /dev/null +++ b/internal/api/grpc/settings/v2/integration_test/query_test.go @@ -0,0 +1,432 @@ +//go:build integration + +package settings_test + +import ( + "context" + "testing" + "time" + + "github.com/brianvoe/gofakeit/v6" + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/structpb" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/integration" + "github.com/zitadel/zitadel/pkg/grpc/idp" + idp_pb "github.com/zitadel/zitadel/pkg/grpc/idp/v2" + object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2" + "github.com/zitadel/zitadel/pkg/grpc/settings/v2" +) + +func TestServer_GetSecuritySettings(t *testing.T) { + _, err := Client.SetSecuritySettings(AdminCTX, &settings.SetSecuritySettingsRequest{ + EmbeddedIframe: &settings.EmbeddedIframeSettings{ + Enabled: true, + AllowedOrigins: []string{"foo", "bar"}, + }, + EnableImpersonation: true, + }) + require.NoError(t, err) + + tests := []struct { + name string + ctx context.Context + want *settings.GetSecuritySettingsResponse + wantErr bool + }{ + { + name: "permission error", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + wantErr: true, + }, + { + name: "success", + ctx: AdminCTX, + want: &settings.GetSecuritySettingsResponse{ + Settings: &settings.SecuritySettings{ + EmbeddedIframe: &settings.EmbeddedIframeSettings{ + Enabled: true, + AllowedOrigins: []string{"foo", "bar"}, + }, + EnableImpersonation: true, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tt.ctx, time.Minute) + assert.EventuallyWithT(t, func(ct *assert.CollectT) { + resp, err := Client.GetSecuritySettings(tt.ctx, &settings.GetSecuritySettingsRequest{}) + if tt.wantErr { + assert.Error(ct, err) + return + } + if !assert.NoError(ct, err) { + return + } + got, want := resp.GetSettings(), tt.want.GetSettings() + assert.Equal(ct, want.GetEmbeddedIframe().GetEnabled(), got.GetEmbeddedIframe().GetEnabled(), "enable iframe embedding") + assert.Equal(ct, want.GetEmbeddedIframe().GetAllowedOrigins(), got.GetEmbeddedIframe().GetAllowedOrigins(), "allowed origins") + assert.Equal(ct, want.GetEnableImpersonation(), got.GetEnableImpersonation(), "enable impersonation") + }, retryDuration, tick) + }) + } +} + +func idpResponse(id, name string, linking, creation, autoCreation, autoUpdate bool, autoLinking idp_pb.AutoLinkingOption) *settings.IdentityProvider { + return &settings.IdentityProvider{ + Id: id, + Name: name, + Type: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_OAUTH, + Options: &idp_pb.Options{ + IsLinkingAllowed: linking, + IsCreationAllowed: creation, + IsAutoCreation: autoCreation, + IsAutoUpdate: autoUpdate, + AutoLinking: autoLinking, + }, + } +} + +func TestServer_GetActiveIdentityProviders(t *testing.T) { + instance := integration.NewInstance(CTX) + isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + + instance.AddGenericOAuthProvider(isolatedIAMOwnerCTX, gofakeit.AppName()) // inactive + idpActiveName := gofakeit.AppName() + idpActiveResp := instance.AddGenericOAuthProvider(isolatedIAMOwnerCTX, idpActiveName) + instance.AddProviderToDefaultLoginPolicy(isolatedIAMOwnerCTX, idpActiveResp.GetId()) + idpActiveResponse := idpResponse(idpActiveResp.GetId(), idpActiveName, true, true, true, true, idp_pb.AutoLinkingOption_AUTO_LINKING_OPTION_USERNAME) + idpLinkingDisallowedName := gofakeit.AppName() + idpLinkingDisallowedResp := instance.AddGenericOAuthProviderWithOptions(isolatedIAMOwnerCTX, idpLinkingDisallowedName, false, true, true, idp.AutoLinkingOption_AUTO_LINKING_OPTION_USERNAME) + instance.AddProviderToDefaultLoginPolicy(isolatedIAMOwnerCTX, idpLinkingDisallowedResp.GetId()) + idpLinkingDisallowedResponse := idpResponse(idpLinkingDisallowedResp.GetId(), idpLinkingDisallowedName, false, true, true, true, idp_pb.AutoLinkingOption_AUTO_LINKING_OPTION_USERNAME) + idpCreationDisallowedName := gofakeit.AppName() + idpCreationDisallowedResp := instance.AddGenericOAuthProviderWithOptions(isolatedIAMOwnerCTX, idpCreationDisallowedName, true, false, true, idp.AutoLinkingOption_AUTO_LINKING_OPTION_USERNAME) + instance.AddProviderToDefaultLoginPolicy(isolatedIAMOwnerCTX, idpCreationDisallowedResp.GetId()) + idpCreationDisallowedResponse := idpResponse(idpCreationDisallowedResp.GetId(), idpCreationDisallowedName, true, false, true, true, idp_pb.AutoLinkingOption_AUTO_LINKING_OPTION_USERNAME) + idpNoAutoCreationName := gofakeit.AppName() + idpNoAutoCreationResp := instance.AddGenericOAuthProviderWithOptions(isolatedIAMOwnerCTX, idpNoAutoCreationName, true, true, false, idp.AutoLinkingOption_AUTO_LINKING_OPTION_USERNAME) + instance.AddProviderToDefaultLoginPolicy(isolatedIAMOwnerCTX, idpNoAutoCreationResp.GetId()) + idpNoAutoCreationResponse := idpResponse(idpNoAutoCreationResp.GetId(), idpNoAutoCreationName, true, true, false, true, idp_pb.AutoLinkingOption_AUTO_LINKING_OPTION_USERNAME) + idpNoAutoLinkingName := gofakeit.AppName() + idpNoAutoLinkingResp := instance.AddGenericOAuthProviderWithOptions(isolatedIAMOwnerCTX, idpNoAutoLinkingName, true, true, true, idp.AutoLinkingOption_AUTO_LINKING_OPTION_UNSPECIFIED) + instance.AddProviderToDefaultLoginPolicy(isolatedIAMOwnerCTX, idpNoAutoLinkingResp.GetId()) + idpNoAutoLinkingResponse := idpResponse(idpNoAutoLinkingResp.GetId(), idpNoAutoLinkingName, true, true, true, true, idp_pb.AutoLinkingOption_AUTO_LINKING_OPTION_UNSPECIFIED) + + type args struct { + ctx context.Context + req *settings.GetActiveIdentityProvidersRequest + } + tests := []struct { + name string + args args + want *settings.GetActiveIdentityProvidersResponse + wantErr bool + }{ + { + name: "permission error", + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + req: &settings.GetActiveIdentityProvidersRequest{}, + }, + wantErr: true, + }, + { + name: "success, all", + args: args{ + ctx: isolatedIAMOwnerCTX, + req: &settings.GetActiveIdentityProvidersRequest{}, + }, + want: &settings.GetActiveIdentityProvidersResponse{ + Details: &object_pb.ListDetails{ + TotalResult: 5, + Timestamp: timestamppb.Now(), + }, + IdentityProviders: []*settings.IdentityProvider{ + idpActiveResponse, + idpLinkingDisallowedResponse, + idpCreationDisallowedResponse, + idpNoAutoCreationResponse, + idpNoAutoLinkingResponse, + }, + }, + }, + { + name: "success, exclude linking disallowed", + args: args{ + ctx: isolatedIAMOwnerCTX, + req: &settings.GetActiveIdentityProvidersRequest{ + LinkingAllowed: gu.Ptr(true), + }, + }, + want: &settings.GetActiveIdentityProvidersResponse{ + Details: &object_pb.ListDetails{ + TotalResult: 4, + Timestamp: timestamppb.Now(), + }, + IdentityProviders: []*settings.IdentityProvider{ + idpActiveResponse, + idpCreationDisallowedResponse, + idpNoAutoCreationResponse, + idpNoAutoLinkingResponse, + }, + }, + }, + { + name: "success, only linking disallowed", + args: args{ + ctx: isolatedIAMOwnerCTX, + req: &settings.GetActiveIdentityProvidersRequest{ + LinkingAllowed: gu.Ptr(false), + }, + }, + want: &settings.GetActiveIdentityProvidersResponse{ + Details: &object_pb.ListDetails{ + TotalResult: 1, + Timestamp: timestamppb.Now(), + }, + IdentityProviders: []*settings.IdentityProvider{ + idpLinkingDisallowedResponse, + }, + }, + }, + { + name: "success, exclude creation disallowed", + args: args{ + ctx: isolatedIAMOwnerCTX, + req: &settings.GetActiveIdentityProvidersRequest{ + CreationAllowed: gu.Ptr(true), + }, + }, + want: &settings.GetActiveIdentityProvidersResponse{ + Details: &object_pb.ListDetails{ + TotalResult: 4, + Timestamp: timestamppb.Now(), + }, + IdentityProviders: []*settings.IdentityProvider{ + idpActiveResponse, + idpLinkingDisallowedResponse, + idpNoAutoCreationResponse, + idpNoAutoLinkingResponse, + }, + }, + }, + { + name: "success, only creation disallowed", + args: args{ + ctx: isolatedIAMOwnerCTX, + req: &settings.GetActiveIdentityProvidersRequest{ + CreationAllowed: gu.Ptr(false), + }, + }, + want: &settings.GetActiveIdentityProvidersResponse{ + Details: &object_pb.ListDetails{ + TotalResult: 1, + Timestamp: timestamppb.Now(), + }, + IdentityProviders: []*settings.IdentityProvider{ + idpCreationDisallowedResponse, + }, + }, + }, + { + name: "success, auto creation", + args: args{ + ctx: isolatedIAMOwnerCTX, + req: &settings.GetActiveIdentityProvidersRequest{ + AutoCreation: gu.Ptr(true), + }, + }, + want: &settings.GetActiveIdentityProvidersResponse{ + Details: &object_pb.ListDetails{ + TotalResult: 4, + Timestamp: timestamppb.Now(), + }, + IdentityProviders: []*settings.IdentityProvider{ + idpActiveResponse, + idpLinkingDisallowedResponse, + idpCreationDisallowedResponse, + idpNoAutoLinkingResponse, + }, + }, + }, + { + name: "success, no auto creation", + args: args{ + ctx: isolatedIAMOwnerCTX, + req: &settings.GetActiveIdentityProvidersRequest{ + AutoCreation: gu.Ptr(false), + }, + }, + want: &settings.GetActiveIdentityProvidersResponse{ + Details: &object_pb.ListDetails{ + TotalResult: 1, + Timestamp: timestamppb.Now(), + }, + IdentityProviders: []*settings.IdentityProvider{ + idpNoAutoCreationResponse, + }, + }, + }, + { + name: "success, auto linking", + args: args{ + ctx: isolatedIAMOwnerCTX, + req: &settings.GetActiveIdentityProvidersRequest{ + AutoLinking: gu.Ptr(true), + }, + }, + want: &settings.GetActiveIdentityProvidersResponse{ + Details: &object_pb.ListDetails{ + TotalResult: 4, + Timestamp: timestamppb.Now(), + }, + IdentityProviders: []*settings.IdentityProvider{ + idpActiveResponse, + idpLinkingDisallowedResponse, + idpCreationDisallowedResponse, + idpNoAutoCreationResponse, + }, + }, + }, + { + name: "success, no auto linking", + args: args{ + ctx: isolatedIAMOwnerCTX, + req: &settings.GetActiveIdentityProvidersRequest{ + AutoLinking: gu.Ptr(false), + }, + }, + want: &settings.GetActiveIdentityProvidersResponse{ + Details: &object_pb.ListDetails{ + TotalResult: 1, + Timestamp: timestamppb.Now(), + }, + IdentityProviders: []*settings.IdentityProvider{ + idpNoAutoLinkingResponse, + }, + }, + }, + { + name: "success, exclude all", + args: args{ + ctx: isolatedIAMOwnerCTX, + req: &settings.GetActiveIdentityProvidersRequest{ + LinkingAllowed: gu.Ptr(true), + CreationAllowed: gu.Ptr(true), + AutoCreation: gu.Ptr(true), + AutoLinking: gu.Ptr(true), + }, + }, + want: &settings.GetActiveIdentityProvidersResponse{ + Details: &object_pb.ListDetails{ + TotalResult: 1, + Timestamp: timestamppb.Now(), + }, + IdentityProviders: []*settings.IdentityProvider{ + idpActiveResponse, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tt.args.ctx, time.Minute) + assert.EventuallyWithT(t, func(ct *assert.CollectT) { + got, err := instance.Client.SettingsV2.GetActiveIdentityProviders(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(ct, err) + return + } + if !assert.NoError(ct, err) { + return + } + for i, result := range tt.want.GetIdentityProviders() { + assert.EqualExportedValues(ct, result, got.GetIdentityProviders()[i]) + } + integration.AssertListDetails(ct, tt.want, got) + }, retryDuration, tick) + }) + } +} + +func TestServer_GetHostedLoginTranslation(t *testing.T) { + // Given + translations := map[string]any{"loginTitle": gofakeit.Slogan()} + + protoTranslations, err := structpb.NewStruct(translations) + require.NoError(t, err) + + setupRequest := &settings.SetHostedLoginTranslationRequest{ + Level: &settings.SetHostedLoginTranslationRequest_OrganizationId{ + OrganizationId: Instance.DefaultOrg.GetId(), + }, + Translations: protoTranslations, + Locale: gofakeit.LanguageBCP(), + } + savedTranslation, err := Client.SetHostedLoginTranslation(AdminCTX, setupRequest) + require.NoError(t, err) + + tt := []struct { + testName string + inputCtx context.Context + inputRequest *settings.GetHostedLoginTranslationRequest + + expectedErrorCode codes.Code + expectedErrorMsg string + expectedResponse *settings.GetHostedLoginTranslationResponse + }{ + { + testName: "when unauthN context should return unauthN error", + inputCtx: CTX, + inputRequest: &settings.GetHostedLoginTranslationRequest{Locale: "en-US"}, + expectedErrorCode: codes.Unauthenticated, + expectedErrorMsg: "auth header missing", + }, + { + testName: "when unauthZ context should return unauthZ error", + inputCtx: OrgOwnerCtx, + inputRequest: &settings.GetHostedLoginTranslationRequest{Locale: "en-US"}, + expectedErrorCode: codes.PermissionDenied, + expectedErrorMsg: "No matching permissions found (AUTH-5mWD2)", + }, + { + testName: "when authZ request should save to db and return etag", + inputCtx: AdminCTX, + inputRequest: &settings.GetHostedLoginTranslationRequest{ + Level: &settings.GetHostedLoginTranslationRequest_OrganizationId{ + OrganizationId: Instance.DefaultOrg.GetId(), + }, + Locale: setupRequest.GetLocale(), + }, + expectedResponse: &settings.GetHostedLoginTranslationResponse{ + Etag: savedTranslation.GetEtag(), + Translations: protoTranslations, + }, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + // When + res, err := Client.GetHostedLoginTranslation(tc.inputCtx, tc.inputRequest) + + // Then + assert.Equal(t, tc.expectedErrorCode, status.Code(err)) + assert.Equal(t, tc.expectedErrorMsg, status.Convert(err).Message()) + + if tc.expectedErrorMsg == "" { + require.NoError(t, err) + assert.NotEmpty(t, res.GetEtag()) + assert.NotEmpty(t, res.GetTranslations().GetFields()) + } + }) + } +} diff --git a/internal/api/grpc/settings/v2/integration_test/server_test.go b/internal/api/grpc/settings/v2/integration_test/server_test.go index d57e2a7694..c5c851c310 100644 --- a/internal/api/grpc/settings/v2/integration_test/server_test.go +++ b/internal/api/grpc/settings/v2/integration_test/server_test.go @@ -13,9 +13,9 @@ import ( ) var ( - CTX, AdminCTX context.Context - Instance *integration.Instance - Client settings.SettingsServiceClient + CTX, AdminCTX, UserTypeLoginCtx, OrgOwnerCtx context.Context + Instance *integration.Instance + Client settings.SettingsServiceClient ) func TestMain(m *testing.M) { @@ -27,6 +27,9 @@ func TestMain(m *testing.M) { CTX = ctx AdminCTX = Instance.WithAuthorization(ctx, integration.UserTypeIAMOwner) + UserTypeLoginCtx = Instance.WithAuthorization(ctx, integration.UserTypeLogin) + OrgOwnerCtx = Instance.WithAuthorization(ctx, integration.UserTypeOrgOwner) + Client = Instance.Client.SettingsV2 return m.Run() }()) diff --git a/internal/api/grpc/settings/v2/integration_test/settings_test.go b/internal/api/grpc/settings/v2/integration_test/settings_test.go index 3430eae5f8..7d1e4b0239 100644 --- a/internal/api/grpc/settings/v2/integration_test/settings_test.go +++ b/internal/api/grpc/settings/v2/integration_test/settings_test.go @@ -4,78 +4,23 @@ package settings_test import ( "context" + "crypto/md5" + "encoding/hex" + "fmt" "testing" - "time" - "github.com/brianvoe/gofakeit/v6" - "github.com/muhlemmer/gu" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" "github.com/zitadel/zitadel/internal/integration" - "github.com/zitadel/zitadel/pkg/grpc/idp" - idp_pb "github.com/zitadel/zitadel/pkg/grpc/idp/v2" object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2" "github.com/zitadel/zitadel/pkg/grpc/settings/v2" ) -func TestServer_GetSecuritySettings(t *testing.T) { - _, err := Client.SetSecuritySettings(AdminCTX, &settings.SetSecuritySettingsRequest{ - EmbeddedIframe: &settings.EmbeddedIframeSettings{ - Enabled: true, - AllowedOrigins: []string{"foo", "bar"}, - }, - EnableImpersonation: true, - }) - require.NoError(t, err) - - tests := []struct { - name string - ctx context.Context - want *settings.GetSecuritySettingsResponse - wantErr bool - }{ - { - name: "permission error", - ctx: Instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), - wantErr: true, - }, - { - name: "success", - ctx: AdminCTX, - want: &settings.GetSecuritySettingsResponse{ - Settings: &settings.SecuritySettings{ - EmbeddedIframe: &settings.EmbeddedIframeSettings{ - Enabled: true, - AllowedOrigins: []string{"foo", "bar"}, - }, - EnableImpersonation: true, - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tt.ctx, time.Minute) - assert.EventuallyWithT(t, func(ct *assert.CollectT) { - resp, err := Client.GetSecuritySettings(tt.ctx, &settings.GetSecuritySettingsRequest{}) - if tt.wantErr { - assert.Error(ct, err) - return - } - if !assert.NoError(ct, err) { - return - } - got, want := resp.GetSettings(), tt.want.GetSettings() - assert.Equal(ct, want.GetEmbeddedIframe().GetEnabled(), got.GetEmbeddedIframe().GetEnabled(), "enable iframe embedding") - assert.Equal(ct, want.GetEmbeddedIframe().GetAllowedOrigins(), got.GetEmbeddedIframe().GetAllowedOrigins(), "allowed origins") - assert.Equal(ct, want.GetEnableImpersonation(), got.GetEnableImpersonation(), "enable impersonation") - }, retryDuration, tick) - }) - } -} - func TestServer_SetSecuritySettings(t *testing.T) { type args struct { ctx context.Context @@ -183,280 +128,64 @@ func TestServer_SetSecuritySettings(t *testing.T) { } } -func idpResponse(id, name string, linking, creation, autoCreation, autoUpdate bool, autoLinking idp_pb.AutoLinkingOption) *settings.IdentityProvider { - return &settings.IdentityProvider{ - Id: id, - Name: name, - Type: settings.IdentityProviderType_IDENTITY_PROVIDER_TYPE_OAUTH, - Options: &idp_pb.Options{ - IsLinkingAllowed: linking, - IsCreationAllowed: creation, - IsAutoCreation: autoCreation, - IsAutoUpdate: autoUpdate, - AutoLinking: autoLinking, - }, - } -} +func TestSetHostedLoginTranslation(t *testing.T) { + translations := map[string]any{"loginTitle": "Welcome to our service"} -func TestServer_GetActiveIdentityProviders(t *testing.T) { - instance := integration.NewInstance(CTX) - isolatedIAMOwnerCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + protoTranslations, err := structpb.NewStruct(translations) + require.Nil(t, err) - instance.AddGenericOAuthProvider(isolatedIAMOwnerCTX, gofakeit.AppName()) // inactive - idpActiveName := gofakeit.AppName() - idpActiveResp := instance.AddGenericOAuthProvider(isolatedIAMOwnerCTX, idpActiveName) - instance.AddProviderToDefaultLoginPolicy(isolatedIAMOwnerCTX, idpActiveResp.GetId()) - idpActiveResponse := idpResponse(idpActiveResp.GetId(), idpActiveName, true, true, true, true, idp_pb.AutoLinkingOption_AUTO_LINKING_OPTION_USERNAME) - idpLinkingDisallowedName := gofakeit.AppName() - idpLinkingDisallowedResp := instance.AddGenericOAuthProviderWithOptions(isolatedIAMOwnerCTX, idpLinkingDisallowedName, false, true, true, idp.AutoLinkingOption_AUTO_LINKING_OPTION_USERNAME) - instance.AddProviderToDefaultLoginPolicy(isolatedIAMOwnerCTX, idpLinkingDisallowedResp.GetId()) - idpLinkingDisallowedResponse := idpResponse(idpLinkingDisallowedResp.GetId(), idpLinkingDisallowedName, false, true, true, true, idp_pb.AutoLinkingOption_AUTO_LINKING_OPTION_USERNAME) - idpCreationDisallowedName := gofakeit.AppName() - idpCreationDisallowedResp := instance.AddGenericOAuthProviderWithOptions(isolatedIAMOwnerCTX, idpCreationDisallowedName, true, false, true, idp.AutoLinkingOption_AUTO_LINKING_OPTION_USERNAME) - instance.AddProviderToDefaultLoginPolicy(isolatedIAMOwnerCTX, idpCreationDisallowedResp.GetId()) - idpCreationDisallowedResponse := idpResponse(idpCreationDisallowedResp.GetId(), idpCreationDisallowedName, true, false, true, true, idp_pb.AutoLinkingOption_AUTO_LINKING_OPTION_USERNAME) - idpNoAutoCreationName := gofakeit.AppName() - idpNoAutoCreationResp := instance.AddGenericOAuthProviderWithOptions(isolatedIAMOwnerCTX, idpNoAutoCreationName, true, true, false, idp.AutoLinkingOption_AUTO_LINKING_OPTION_USERNAME) - instance.AddProviderToDefaultLoginPolicy(isolatedIAMOwnerCTX, idpNoAutoCreationResp.GetId()) - idpNoAutoCreationResponse := idpResponse(idpNoAutoCreationResp.GetId(), idpNoAutoCreationName, true, true, false, true, idp_pb.AutoLinkingOption_AUTO_LINKING_OPTION_USERNAME) - idpNoAutoLinkingName := gofakeit.AppName() - idpNoAutoLinkingResp := instance.AddGenericOAuthProviderWithOptions(isolatedIAMOwnerCTX, idpNoAutoLinkingName, true, true, true, idp.AutoLinkingOption_AUTO_LINKING_OPTION_UNSPECIFIED) - instance.AddProviderToDefaultLoginPolicy(isolatedIAMOwnerCTX, idpNoAutoLinkingResp.GetId()) - idpNoAutoLinkingResponse := idpResponse(idpNoAutoLinkingResp.GetId(), idpNoAutoLinkingName, true, true, true, true, idp_pb.AutoLinkingOption_AUTO_LINKING_OPTION_UNSPECIFIED) + hash := md5.Sum(fmt.Append(nil, translations)) - type args struct { - ctx context.Context - req *settings.GetActiveIdentityProvidersRequest - } - tests := []struct { - name string - args args - want *settings.GetActiveIdentityProvidersResponse - wantErr bool + tt := []struct { + testName string + inputCtx context.Context + inputRequest *settings.SetHostedLoginTranslationRequest + + expectedErrorCode codes.Code + expectedErrorMsg string + expectedResponse *settings.SetHostedLoginTranslationResponse }{ { - name: "permission error", - args: args{ - ctx: instance.WithAuthorization(CTX, integration.UserTypeNoPermission), - req: &settings.GetActiveIdentityProvidersRequest{}, - }, - wantErr: true, + testName: "when unauthN context should return unauthN error", + inputCtx: CTX, + expectedErrorCode: codes.Unauthenticated, + expectedErrorMsg: "auth header missing", }, { - name: "success, all", - args: args{ - ctx: isolatedIAMOwnerCTX, - req: &settings.GetActiveIdentityProvidersRequest{}, - }, - want: &settings.GetActiveIdentityProvidersResponse{ - Details: &object_pb.ListDetails{ - TotalResult: 5, - Timestamp: timestamppb.Now(), - }, - IdentityProviders: []*settings.IdentityProvider{ - idpActiveResponse, - idpLinkingDisallowedResponse, - idpCreationDisallowedResponse, - idpNoAutoCreationResponse, - idpNoAutoLinkingResponse, - }, - }, + testName: "when unauthZ context should return unauthZ error", + inputCtx: UserTypeLoginCtx, + expectedErrorCode: codes.PermissionDenied, + expectedErrorMsg: "No matching permissions found (AUTH-5mWD2)", }, { - name: "success, exclude linking disallowed", - args: args{ - ctx: isolatedIAMOwnerCTX, - req: &settings.GetActiveIdentityProvidersRequest{ - LinkingAllowed: gu.Ptr(true), + testName: "when authZ request should save to db and return etag", + inputCtx: AdminCTX, + inputRequest: &settings.SetHostedLoginTranslationRequest{ + Level: &settings.SetHostedLoginTranslationRequest_OrganizationId{ + OrganizationId: Instance.DefaultOrg.GetId(), }, + Translations: protoTranslations, + Locale: "en-US", }, - want: &settings.GetActiveIdentityProvidersResponse{ - Details: &object_pb.ListDetails{ - TotalResult: 4, - Timestamp: timestamppb.Now(), - }, - IdentityProviders: []*settings.IdentityProvider{ - idpActiveResponse, - idpCreationDisallowedResponse, - idpNoAutoCreationResponse, - idpNoAutoLinkingResponse, - }, - }, - }, - { - name: "success, only linking disallowed", - args: args{ - ctx: isolatedIAMOwnerCTX, - req: &settings.GetActiveIdentityProvidersRequest{ - LinkingAllowed: gu.Ptr(false), - }, - }, - want: &settings.GetActiveIdentityProvidersResponse{ - Details: &object_pb.ListDetails{ - TotalResult: 1, - Timestamp: timestamppb.Now(), - }, - IdentityProviders: []*settings.IdentityProvider{ - idpLinkingDisallowedResponse, - }, - }, - }, - { - name: "success, exclude creation disallowed", - args: args{ - ctx: isolatedIAMOwnerCTX, - req: &settings.GetActiveIdentityProvidersRequest{ - CreationAllowed: gu.Ptr(true), - }, - }, - want: &settings.GetActiveIdentityProvidersResponse{ - Details: &object_pb.ListDetails{ - TotalResult: 4, - Timestamp: timestamppb.Now(), - }, - IdentityProviders: []*settings.IdentityProvider{ - idpActiveResponse, - idpLinkingDisallowedResponse, - idpNoAutoCreationResponse, - idpNoAutoLinkingResponse, - }, - }, - }, - { - name: "success, only creation disallowed", - args: args{ - ctx: isolatedIAMOwnerCTX, - req: &settings.GetActiveIdentityProvidersRequest{ - CreationAllowed: gu.Ptr(false), - }, - }, - want: &settings.GetActiveIdentityProvidersResponse{ - Details: &object_pb.ListDetails{ - TotalResult: 1, - Timestamp: timestamppb.Now(), - }, - IdentityProviders: []*settings.IdentityProvider{ - idpCreationDisallowedResponse, - }, - }, - }, - { - name: "success, auto creation", - args: args{ - ctx: isolatedIAMOwnerCTX, - req: &settings.GetActiveIdentityProvidersRequest{ - AutoCreation: gu.Ptr(true), - }, - }, - want: &settings.GetActiveIdentityProvidersResponse{ - Details: &object_pb.ListDetails{ - TotalResult: 4, - Timestamp: timestamppb.Now(), - }, - IdentityProviders: []*settings.IdentityProvider{ - idpActiveResponse, - idpLinkingDisallowedResponse, - idpCreationDisallowedResponse, - idpNoAutoLinkingResponse, - }, - }, - }, - { - name: "success, no auto creation", - args: args{ - ctx: isolatedIAMOwnerCTX, - req: &settings.GetActiveIdentityProvidersRequest{ - AutoCreation: gu.Ptr(false), - }, - }, - want: &settings.GetActiveIdentityProvidersResponse{ - Details: &object_pb.ListDetails{ - TotalResult: 1, - Timestamp: timestamppb.Now(), - }, - IdentityProviders: []*settings.IdentityProvider{ - idpNoAutoCreationResponse, - }, - }, - }, - { - name: "success, auto linking", - args: args{ - ctx: isolatedIAMOwnerCTX, - req: &settings.GetActiveIdentityProvidersRequest{ - AutoLinking: gu.Ptr(true), - }, - }, - want: &settings.GetActiveIdentityProvidersResponse{ - Details: &object_pb.ListDetails{ - TotalResult: 4, - Timestamp: timestamppb.Now(), - }, - IdentityProviders: []*settings.IdentityProvider{ - idpActiveResponse, - idpLinkingDisallowedResponse, - idpCreationDisallowedResponse, - idpNoAutoCreationResponse, - }, - }, - }, - { - name: "success, no auto linking", - args: args{ - ctx: isolatedIAMOwnerCTX, - req: &settings.GetActiveIdentityProvidersRequest{ - AutoLinking: gu.Ptr(false), - }, - }, - want: &settings.GetActiveIdentityProvidersResponse{ - Details: &object_pb.ListDetails{ - TotalResult: 1, - Timestamp: timestamppb.Now(), - }, - IdentityProviders: []*settings.IdentityProvider{ - idpNoAutoLinkingResponse, - }, - }, - }, - { - name: "success, exclude all", - args: args{ - ctx: isolatedIAMOwnerCTX, - req: &settings.GetActiveIdentityProvidersRequest{ - LinkingAllowed: gu.Ptr(true), - CreationAllowed: gu.Ptr(true), - AutoCreation: gu.Ptr(true), - AutoLinking: gu.Ptr(true), - }, - }, - want: &settings.GetActiveIdentityProvidersResponse{ - Details: &object_pb.ListDetails{ - TotalResult: 1, - Timestamp: timestamppb.Now(), - }, - IdentityProviders: []*settings.IdentityProvider{ - idpActiveResponse, - }, + expectedResponse: &settings.SetHostedLoginTranslationResponse{ + Etag: hex.EncodeToString(hash[:]), }, }, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tt.args.ctx, time.Minute) - assert.EventuallyWithT(t, func(ct *assert.CollectT) { - got, err := instance.Client.SettingsV2.GetActiveIdentityProviders(tt.args.ctx, tt.args.req) - if tt.wantErr { - assert.Error(ct, err) - return - } - if !assert.NoError(ct, err) { - return - } - for i, result := range tt.want.GetIdentityProviders() { - assert.EqualExportedValues(ct, result, got.GetIdentityProviders()[i]) - } - integration.AssertListDetails(ct, tt.want, got) - }, retryDuration, tick) + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + // When + res, err := Client.SetHostedLoginTranslation(tc.inputCtx, tc.inputRequest) + + // Then + assert.Equal(t, tc.expectedErrorCode, status.Code(err)) + assert.Equal(t, tc.expectedErrorMsg, status.Convert(err).Message()) + + if tc.expectedErrorMsg == "" { + require.NoError(t, err) + assert.Equal(t, tc.expectedResponse.GetEtag(), res.GetEtag()) + } }) } } diff --git a/internal/api/grpc/settings/v2/query.go b/internal/api/grpc/settings/v2/query.go new file mode 100644 index 0000000000..d522424040 --- /dev/null +++ b/internal/api/grpc/settings/v2/query.go @@ -0,0 +1,210 @@ +package settings + +import ( + "context" + + "connectrpc.com/connect" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/api/grpc/object/v2" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/i18n" + "github.com/zitadel/zitadel/internal/query" + object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2" + "github.com/zitadel/zitadel/pkg/grpc/settings/v2" +) + +func (s *Server) GetLoginSettings(ctx context.Context, req *connect.Request[settings.GetLoginSettingsRequest]) (*connect.Response[settings.GetLoginSettingsResponse], error) { + current, err := s.query.LoginPolicyByID(ctx, true, object.ResourceOwnerFromReq(ctx, req.Msg.GetCtx()), false) + if err != nil { + return nil, err + } + return connect.NewResponse(&settings.GetLoginSettingsResponse{ + Settings: loginSettingsToPb(current), + Details: &object_pb.Details{ + Sequence: current.Sequence, + CreationDate: timestamppb.New(current.CreationDate), + ChangeDate: timestamppb.New(current.ChangeDate), + ResourceOwner: current.OrgID, + }, + }), nil +} + +func (s *Server) GetPasswordComplexitySettings(ctx context.Context, req *connect.Request[settings.GetPasswordComplexitySettingsRequest]) (*connect.Response[settings.GetPasswordComplexitySettingsResponse], error) { + current, err := s.query.PasswordComplexityPolicyByOrg(ctx, true, object.ResourceOwnerFromReq(ctx, req.Msg.GetCtx()), false) + if err != nil { + return nil, err + } + return connect.NewResponse(&settings.GetPasswordComplexitySettingsResponse{ + Settings: passwordComplexitySettingsToPb(current), + Details: &object_pb.Details{ + Sequence: current.Sequence, + CreationDate: timestamppb.New(current.CreationDate), + ChangeDate: timestamppb.New(current.ChangeDate), + ResourceOwner: current.ResourceOwner, + }, + }), nil +} + +func (s *Server) GetPasswordExpirySettings(ctx context.Context, req *connect.Request[settings.GetPasswordExpirySettingsRequest]) (*connect.Response[settings.GetPasswordExpirySettingsResponse], error) { + current, err := s.query.PasswordAgePolicyByOrg(ctx, true, object.ResourceOwnerFromReq(ctx, req.Msg.GetCtx()), false) + if err != nil { + return nil, err + } + return connect.NewResponse(&settings.GetPasswordExpirySettingsResponse{ + Settings: passwordExpirySettingsToPb(current), + Details: &object_pb.Details{ + Sequence: current.Sequence, + CreationDate: timestamppb.New(current.CreationDate), + ChangeDate: timestamppb.New(current.ChangeDate), + ResourceOwner: current.ResourceOwner, + }, + }), nil +} + +func (s *Server) GetBrandingSettings(ctx context.Context, req *connect.Request[settings.GetBrandingSettingsRequest]) (*connect.Response[settings.GetBrandingSettingsResponse], error) { + current, err := s.query.ActiveLabelPolicyByOrg(ctx, object.ResourceOwnerFromReq(ctx, req.Msg.GetCtx()), false) + if err != nil { + return nil, err + } + return connect.NewResponse(&settings.GetBrandingSettingsResponse{ + Settings: brandingSettingsToPb(current, s.assetsAPIDomain(ctx)), + Details: &object_pb.Details{ + Sequence: current.Sequence, + CreationDate: timestamppb.New(current.CreationDate), + ChangeDate: timestamppb.New(current.ChangeDate), + ResourceOwner: current.ResourceOwner, + }, + }), nil +} + +func (s *Server) GetDomainSettings(ctx context.Context, req *connect.Request[settings.GetDomainSettingsRequest]) (*connect.Response[settings.GetDomainSettingsResponse], error) { + current, err := s.query.DomainPolicyByOrg(ctx, true, object.ResourceOwnerFromReq(ctx, req.Msg.GetCtx()), false) + if err != nil { + return nil, err + } + return connect.NewResponse(&settings.GetDomainSettingsResponse{ + Settings: domainSettingsToPb(current), + Details: &object_pb.Details{ + Sequence: current.Sequence, + CreationDate: timestamppb.New(current.CreationDate), + ChangeDate: timestamppb.New(current.ChangeDate), + ResourceOwner: current.ResourceOwner, + }, + }), nil +} + +func (s *Server) GetLegalAndSupportSettings(ctx context.Context, req *connect.Request[settings.GetLegalAndSupportSettingsRequest]) (*connect.Response[settings.GetLegalAndSupportSettingsResponse], error) { + current, err := s.query.PrivacyPolicyByOrg(ctx, true, object.ResourceOwnerFromReq(ctx, req.Msg.GetCtx()), false) + if err != nil { + return nil, err + } + return connect.NewResponse(&settings.GetLegalAndSupportSettingsResponse{ + Settings: legalAndSupportSettingsToPb(current), + Details: &object_pb.Details{ + Sequence: current.Sequence, + CreationDate: timestamppb.New(current.CreationDate), + ChangeDate: timestamppb.New(current.ChangeDate), + ResourceOwner: current.ResourceOwner, + }, + }), nil +} + +func (s *Server) GetLockoutSettings(ctx context.Context, req *connect.Request[settings.GetLockoutSettingsRequest]) (*connect.Response[settings.GetLockoutSettingsResponse], error) { + current, err := s.query.LockoutPolicyByOrg(ctx, true, object.ResourceOwnerFromReq(ctx, req.Msg.GetCtx())) + if err != nil { + return nil, err + } + return connect.NewResponse(&settings.GetLockoutSettingsResponse{ + Settings: lockoutSettingsToPb(current), + Details: &object_pb.Details{ + Sequence: current.Sequence, + CreationDate: timestamppb.New(current.CreationDate), + ChangeDate: timestamppb.New(current.ChangeDate), + ResourceOwner: current.ResourceOwner, + }, + }), nil +} + +func (s *Server) GetActiveIdentityProviders(ctx context.Context, req *connect.Request[settings.GetActiveIdentityProvidersRequest]) (*connect.Response[settings.GetActiveIdentityProvidersResponse], error) { + queries, err := activeIdentityProvidersToQuery(req.Msg) + if err != nil { + return nil, err + } + + links, err := s.query.IDPLoginPolicyLinks(ctx, object.ResourceOwnerFromReq(ctx, req.Msg.GetCtx()), &query.IDPLoginPolicyLinksSearchQuery{Queries: queries}, false) + if err != nil { + return nil, err + } + + return connect.NewResponse(&settings.GetActiveIdentityProvidersResponse{ + Details: object.ToListDetails(links.SearchResponse), + IdentityProviders: identityProvidersToPb(links.Links), + }), nil +} + +func activeIdentityProvidersToQuery(req *settings.GetActiveIdentityProvidersRequest) (_ []query.SearchQuery, err error) { + q := make([]query.SearchQuery, 0, 4) + if req.CreationAllowed != nil { + creationQuery, err := query.NewIDPTemplateIsCreationAllowedSearchQuery(*req.CreationAllowed) + if err != nil { + return nil, err + } + q = append(q, creationQuery) + } + if req.LinkingAllowed != nil { + creationQuery, err := query.NewIDPTemplateIsLinkingAllowedSearchQuery(*req.LinkingAllowed) + if err != nil { + return nil, err + } + q = append(q, creationQuery) + } + if req.AutoCreation != nil { + creationQuery, err := query.NewIDPTemplateIsAutoCreationSearchQuery(*req.AutoCreation) + if err != nil { + return nil, err + } + q = append(q, creationQuery) + } + if req.AutoLinking != nil { + compare := query.NumberEquals + if *req.AutoLinking { + compare = query.NumberNotEquals + } + creationQuery, err := query.NewIDPTemplateAutoLinkingSearchQuery(0, compare) + if err != nil { + return nil, err + } + q = append(q, creationQuery) + } + return q, nil +} + +func (s *Server) GetGeneralSettings(ctx context.Context, _ *connect.Request[settings.GetGeneralSettingsRequest]) (*connect.Response[settings.GetGeneralSettingsResponse], error) { + instance := authz.GetInstance(ctx) + return connect.NewResponse(&settings.GetGeneralSettingsResponse{ + SupportedLanguages: domain.LanguagesToStrings(i18n.SupportedLanguages()), + DefaultOrgId: instance.DefaultOrganisationID(), + DefaultLanguage: instance.DefaultLanguage().String(), + }), nil +} + +func (s *Server) GetSecuritySettings(ctx context.Context, req *connect.Request[settings.GetSecuritySettingsRequest]) (*connect.Response[settings.GetSecuritySettingsResponse], error) { + policy, err := s.query.SecurityPolicy(ctx) + if err != nil { + return nil, err + } + return connect.NewResponse(&settings.GetSecuritySettingsResponse{ + Settings: securityPolicyToSettingsPb(policy), + }), nil +} + +func (s *Server) GetHostedLoginTranslation(ctx context.Context, req *connect.Request[settings.GetHostedLoginTranslationRequest]) (*connect.Response[settings.GetHostedLoginTranslationResponse], error) { + translation, err := s.query.GetHostedLoginTranslation(ctx, req.Msg) + if err != nil { + return nil, err + } + + return connect.NewResponse(translation), nil +} diff --git a/internal/api/grpc/settings/v2/server.go b/internal/api/grpc/settings/v2/server.go index 9cae50824f..bfaec17fc2 100644 --- a/internal/api/grpc/settings/v2/server.go +++ b/internal/api/grpc/settings/v2/server.go @@ -2,8 +2,10 @@ package settings import ( "context" + "net/http" - "google.golang.org/grpc" + "connectrpc.com/connect" + "google.golang.org/protobuf/reflect/protoreflect" "github.com/zitadel/zitadel/internal/api/assets" "github.com/zitadel/zitadel/internal/api/authz" @@ -11,12 +13,12 @@ import ( "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/pkg/grpc/settings/v2" + "github.com/zitadel/zitadel/pkg/grpc/settings/v2/settingsconnect" ) -var _ settings.SettingsServiceServer = (*Server)(nil) +var _ settingsconnect.SettingsServiceHandler = (*Server)(nil) type Server struct { - settings.UnimplementedSettingsServiceServer command *command.Commands query *query.Queries assetsAPIDomain func(context.Context) string @@ -35,8 +37,12 @@ func CreateServer( } } -func (s *Server) RegisterServer(grpcServer *grpc.Server) { - settings.RegisterSettingsServiceServer(grpcServer, s) +func (s *Server) RegisterConnectServer(interceptors ...connect.Interceptor) (string, http.Handler) { + return settingsconnect.NewSettingsServiceHandler(s, connect.WithInterceptors(interceptors...)) +} + +func (s *Server) FileDescriptor() protoreflect.FileDescriptor { + return settings.File_zitadel_settings_v2_settings_service_proto } func (s *Server) AppName() string { diff --git a/internal/api/grpc/settings/v2/settings.go b/internal/api/grpc/settings/v2/settings.go index 77874bf970..c7db200211 100644 --- a/internal/api/grpc/settings/v2/settings.go +++ b/internal/api/grpc/settings/v2/settings.go @@ -3,208 +3,27 @@ package settings import ( "context" - "google.golang.org/protobuf/types/known/timestamppb" + "connectrpc.com/connect" - "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/grpc/object/v2" - "github.com/zitadel/zitadel/internal/domain" - "github.com/zitadel/zitadel/internal/i18n" - "github.com/zitadel/zitadel/internal/query" - object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2" "github.com/zitadel/zitadel/pkg/grpc/settings/v2" ) -func (s *Server) GetLoginSettings(ctx context.Context, req *settings.GetLoginSettingsRequest) (*settings.GetLoginSettingsResponse, error) { - current, err := s.query.LoginPolicyByID(ctx, true, object.ResourceOwnerFromReq(ctx, req.GetCtx()), false) +func (s *Server) SetSecuritySettings(ctx context.Context, req *connect.Request[settings.SetSecuritySettingsRequest]) (*connect.Response[settings.SetSecuritySettingsResponse], error) { + details, err := s.command.SetSecurityPolicy(ctx, securitySettingsToCommand(req.Msg)) if err != nil { return nil, err } - return &settings.GetLoginSettingsResponse{ - Settings: loginSettingsToPb(current), - Details: &object_pb.Details{ - Sequence: current.Sequence, - CreationDate: timestamppb.New(current.CreationDate), - ChangeDate: timestamppb.New(current.ChangeDate), - ResourceOwner: current.OrgID, - }, - }, nil -} - -func (s *Server) GetPasswordComplexitySettings(ctx context.Context, req *settings.GetPasswordComplexitySettingsRequest) (*settings.GetPasswordComplexitySettingsResponse, error) { - current, err := s.query.PasswordComplexityPolicyByOrg(ctx, true, object.ResourceOwnerFromReq(ctx, req.GetCtx()), false) - if err != nil { - return nil, err - } - return &settings.GetPasswordComplexitySettingsResponse{ - Settings: passwordComplexitySettingsToPb(current), - Details: &object_pb.Details{ - Sequence: current.Sequence, - CreationDate: timestamppb.New(current.CreationDate), - ChangeDate: timestamppb.New(current.ChangeDate), - ResourceOwner: current.ResourceOwner, - }, - }, nil -} - -func (s *Server) GetPasswordExpirySettings(ctx context.Context, req *settings.GetPasswordExpirySettingsRequest) (*settings.GetPasswordExpirySettingsResponse, error) { - current, err := s.query.PasswordAgePolicyByOrg(ctx, true, object.ResourceOwnerFromReq(ctx, req.GetCtx()), false) - if err != nil { - return nil, err - } - return &settings.GetPasswordExpirySettingsResponse{ - Settings: passwordExpirySettingsToPb(current), - Details: &object_pb.Details{ - Sequence: current.Sequence, - CreationDate: timestamppb.New(current.CreationDate), - ChangeDate: timestamppb.New(current.ChangeDate), - ResourceOwner: current.ResourceOwner, - }, - }, nil -} - -func (s *Server) GetBrandingSettings(ctx context.Context, req *settings.GetBrandingSettingsRequest) (*settings.GetBrandingSettingsResponse, error) { - current, err := s.query.ActiveLabelPolicyByOrg(ctx, object.ResourceOwnerFromReq(ctx, req.GetCtx()), false) - if err != nil { - return nil, err - } - return &settings.GetBrandingSettingsResponse{ - Settings: brandingSettingsToPb(current, s.assetsAPIDomain(ctx)), - Details: &object_pb.Details{ - Sequence: current.Sequence, - CreationDate: timestamppb.New(current.CreationDate), - ChangeDate: timestamppb.New(current.ChangeDate), - ResourceOwner: current.ResourceOwner, - }, - }, nil -} - -func (s *Server) GetDomainSettings(ctx context.Context, req *settings.GetDomainSettingsRequest) (*settings.GetDomainSettingsResponse, error) { - current, err := s.query.DomainPolicyByOrg(ctx, true, object.ResourceOwnerFromReq(ctx, req.GetCtx()), false) - if err != nil { - return nil, err - } - return &settings.GetDomainSettingsResponse{ - Settings: domainSettingsToPb(current), - Details: &object_pb.Details{ - Sequence: current.Sequence, - CreationDate: timestamppb.New(current.CreationDate), - ChangeDate: timestamppb.New(current.ChangeDate), - ResourceOwner: current.ResourceOwner, - }, - }, nil -} - -func (s *Server) GetLegalAndSupportSettings(ctx context.Context, req *settings.GetLegalAndSupportSettingsRequest) (*settings.GetLegalAndSupportSettingsResponse, error) { - current, err := s.query.PrivacyPolicyByOrg(ctx, true, object.ResourceOwnerFromReq(ctx, req.GetCtx()), false) - if err != nil { - return nil, err - } - return &settings.GetLegalAndSupportSettingsResponse{ - Settings: legalAndSupportSettingsToPb(current), - Details: &object_pb.Details{ - Sequence: current.Sequence, - CreationDate: timestamppb.New(current.CreationDate), - ChangeDate: timestamppb.New(current.ChangeDate), - ResourceOwner: current.ResourceOwner, - }, - }, nil -} - -func (s *Server) GetLockoutSettings(ctx context.Context, req *settings.GetLockoutSettingsRequest) (*settings.GetLockoutSettingsResponse, error) { - current, err := s.query.LockoutPolicyByOrg(ctx, true, object.ResourceOwnerFromReq(ctx, req.GetCtx())) - if err != nil { - return nil, err - } - return &settings.GetLockoutSettingsResponse{ - Settings: lockoutSettingsToPb(current), - Details: &object_pb.Details{ - Sequence: current.Sequence, - CreationDate: timestamppb.New(current.CreationDate), - ChangeDate: timestamppb.New(current.ChangeDate), - ResourceOwner: current.ResourceOwner, - }, - }, nil -} - -func (s *Server) GetActiveIdentityProviders(ctx context.Context, req *settings.GetActiveIdentityProvidersRequest) (*settings.GetActiveIdentityProvidersResponse, error) { - queries, err := activeIdentityProvidersToQuery(req) - if err != nil { - return nil, err - } - - links, err := s.query.IDPLoginPolicyLinks(ctx, object.ResourceOwnerFromReq(ctx, req.GetCtx()), &query.IDPLoginPolicyLinksSearchQuery{Queries: queries}, false) - if err != nil { - return nil, err - } - - return &settings.GetActiveIdentityProvidersResponse{ - Details: object.ToListDetails(links.SearchResponse), - IdentityProviders: identityProvidersToPb(links.Links), - }, nil -} - -func activeIdentityProvidersToQuery(req *settings.GetActiveIdentityProvidersRequest) (_ []query.SearchQuery, err error) { - q := make([]query.SearchQuery, 0, 4) - if req.CreationAllowed != nil { - creationQuery, err := query.NewIDPTemplateIsCreationAllowedSearchQuery(*req.CreationAllowed) - if err != nil { - return nil, err - } - q = append(q, creationQuery) - } - if req.LinkingAllowed != nil { - creationQuery, err := query.NewIDPTemplateIsLinkingAllowedSearchQuery(*req.LinkingAllowed) - if err != nil { - return nil, err - } - q = append(q, creationQuery) - } - if req.AutoCreation != nil { - creationQuery, err := query.NewIDPTemplateIsAutoCreationSearchQuery(*req.AutoCreation) - if err != nil { - return nil, err - } - q = append(q, creationQuery) - } - if req.AutoLinking != nil { - compare := query.NumberEquals - if *req.AutoLinking { - compare = query.NumberNotEquals - } - creationQuery, err := query.NewIDPTemplateAutoLinkingSearchQuery(0, compare) - if err != nil { - return nil, err - } - q = append(q, creationQuery) - } - return q, nil -} - -func (s *Server) GetGeneralSettings(ctx context.Context, _ *settings.GetGeneralSettingsRequest) (*settings.GetGeneralSettingsResponse, error) { - instance := authz.GetInstance(ctx) - return &settings.GetGeneralSettingsResponse{ - SupportedLanguages: domain.LanguagesToStrings(i18n.SupportedLanguages()), - DefaultOrgId: instance.DefaultOrganisationID(), - DefaultLanguage: instance.DefaultLanguage().String(), - }, nil -} - -func (s *Server) GetSecuritySettings(ctx context.Context, req *settings.GetSecuritySettingsRequest) (*settings.GetSecuritySettingsResponse, error) { - policy, err := s.query.SecurityPolicy(ctx) - if err != nil { - return nil, err - } - return &settings.GetSecuritySettingsResponse{ - Settings: securityPolicyToSettingsPb(policy), - }, nil -} - -func (s *Server) SetSecuritySettings(ctx context.Context, req *settings.SetSecuritySettingsRequest) (*settings.SetSecuritySettingsResponse, error) { - details, err := s.command.SetSecurityPolicy(ctx, securitySettingsToCommand(req)) - if err != nil { - return nil, err - } - return &settings.SetSecuritySettingsResponse{ + return connect.NewResponse(&settings.SetSecuritySettingsResponse{ Details: object.DomainToDetailsPb(details), - }, nil + }), nil +} + +func (s *Server) SetHostedLoginTranslation(ctx context.Context, req *connect.Request[settings.SetHostedLoginTranslationRequest]) (*connect.Response[settings.SetHostedLoginTranslationResponse], error) { + res, err := s.command.SetHostedLoginTranslation(ctx, req.Msg) + if err != nil { + return nil, err + } + + return connect.NewResponse(res), nil } diff --git a/internal/api/grpc/settings/v2beta/server.go b/internal/api/grpc/settings/v2beta/server.go index 24c8f7774a..a8200a7216 100644 --- a/internal/api/grpc/settings/v2beta/server.go +++ b/internal/api/grpc/settings/v2beta/server.go @@ -2,8 +2,10 @@ package settings import ( "context" + "net/http" - "google.golang.org/grpc" + "connectrpc.com/connect" + "google.golang.org/protobuf/reflect/protoreflect" "github.com/zitadel/zitadel/internal/api/assets" "github.com/zitadel/zitadel/internal/api/authz" @@ -11,12 +13,12 @@ import ( "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/query" settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta/settingsconnect" ) -var _ settings.SettingsServiceServer = (*Server)(nil) +var _ settingsconnect.SettingsServiceHandler = (*Server)(nil) type Server struct { - settings.UnimplementedSettingsServiceServer command *command.Commands query *query.Queries assetsAPIDomain func(context.Context) string @@ -35,8 +37,12 @@ func CreateServer( } } -func (s *Server) RegisterServer(grpcServer *grpc.Server) { - settings.RegisterSettingsServiceServer(grpcServer, s) +func (s *Server) RegisterConnectServer(interceptors ...connect.Interceptor) (string, http.Handler) { + return settingsconnect.NewSettingsServiceHandler(s, connect.WithInterceptors(interceptors...)) +} + +func (s *Server) FileDescriptor() protoreflect.FileDescriptor { + return settings.File_zitadel_settings_v2beta_settings_service_proto } func (s *Server) AppName() string { diff --git a/internal/api/grpc/settings/v2beta/settings.go b/internal/api/grpc/settings/v2beta/settings.go index 6193f129ba..53d2c37c32 100644 --- a/internal/api/grpc/settings/v2beta/settings.go +++ b/internal/api/grpc/settings/v2beta/settings.go @@ -3,6 +3,7 @@ package settings import ( "context" + "connectrpc.com/connect" "google.golang.org/protobuf/types/known/timestamppb" "github.com/zitadel/zitadel/internal/api/authz" @@ -14,12 +15,12 @@ import ( settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta" ) -func (s *Server) GetLoginSettings(ctx context.Context, req *settings.GetLoginSettingsRequest) (*settings.GetLoginSettingsResponse, error) { - current, err := s.query.LoginPolicyByID(ctx, true, object.ResourceOwnerFromReq(ctx, req.GetCtx()), false) +func (s *Server) GetLoginSettings(ctx context.Context, req *connect.Request[settings.GetLoginSettingsRequest]) (*connect.Response[settings.GetLoginSettingsResponse], error) { + current, err := s.query.LoginPolicyByID(ctx, true, object.ResourceOwnerFromReq(ctx, req.Msg.GetCtx()), false) if err != nil { return nil, err } - return &settings.GetLoginSettingsResponse{ + return connect.NewResponse(&settings.GetLoginSettingsResponse{ Settings: loginSettingsToPb(current), Details: &object_pb.Details{ Sequence: current.Sequence, @@ -27,15 +28,15 @@ func (s *Server) GetLoginSettings(ctx context.Context, req *settings.GetLoginSet ChangeDate: timestamppb.New(current.ChangeDate), ResourceOwner: current.OrgID, }, - }, nil + }), nil } -func (s *Server) GetPasswordComplexitySettings(ctx context.Context, req *settings.GetPasswordComplexitySettingsRequest) (*settings.GetPasswordComplexitySettingsResponse, error) { - current, err := s.query.PasswordComplexityPolicyByOrg(ctx, true, object.ResourceOwnerFromReq(ctx, req.GetCtx()), false) +func (s *Server) GetPasswordComplexitySettings(ctx context.Context, req *connect.Request[settings.GetPasswordComplexitySettingsRequest]) (*connect.Response[settings.GetPasswordComplexitySettingsResponse], error) { + current, err := s.query.PasswordComplexityPolicyByOrg(ctx, true, object.ResourceOwnerFromReq(ctx, req.Msg.GetCtx()), false) if err != nil { return nil, err } - return &settings.GetPasswordComplexitySettingsResponse{ + return connect.NewResponse(&settings.GetPasswordComplexitySettingsResponse{ Settings: passwordComplexitySettingsToPb(current), Details: &object_pb.Details{ Sequence: current.Sequence, @@ -43,15 +44,15 @@ func (s *Server) GetPasswordComplexitySettings(ctx context.Context, req *setting ChangeDate: timestamppb.New(current.ChangeDate), ResourceOwner: current.ResourceOwner, }, - }, nil + }), nil } -func (s *Server) GetPasswordExpirySettings(ctx context.Context, req *settings.GetPasswordExpirySettingsRequest) (*settings.GetPasswordExpirySettingsResponse, error) { - current, err := s.query.PasswordAgePolicyByOrg(ctx, true, object.ResourceOwnerFromReq(ctx, req.GetCtx()), false) +func (s *Server) GetPasswordExpirySettings(ctx context.Context, req *connect.Request[settings.GetPasswordExpirySettingsRequest]) (*connect.Response[settings.GetPasswordExpirySettingsResponse], error) { + current, err := s.query.PasswordAgePolicyByOrg(ctx, true, object.ResourceOwnerFromReq(ctx, req.Msg.GetCtx()), false) if err != nil { return nil, err } - return &settings.GetPasswordExpirySettingsResponse{ + return connect.NewResponse(&settings.GetPasswordExpirySettingsResponse{ Settings: passwordExpirySettingsToPb(current), Details: &object_pb.Details{ Sequence: current.Sequence, @@ -59,15 +60,15 @@ func (s *Server) GetPasswordExpirySettings(ctx context.Context, req *settings.Ge ChangeDate: timestamppb.New(current.ChangeDate), ResourceOwner: current.ResourceOwner, }, - }, nil + }), nil } -func (s *Server) GetBrandingSettings(ctx context.Context, req *settings.GetBrandingSettingsRequest) (*settings.GetBrandingSettingsResponse, error) { - current, err := s.query.ActiveLabelPolicyByOrg(ctx, object.ResourceOwnerFromReq(ctx, req.GetCtx()), false) +func (s *Server) GetBrandingSettings(ctx context.Context, req *connect.Request[settings.GetBrandingSettingsRequest]) (*connect.Response[settings.GetBrandingSettingsResponse], error) { + current, err := s.query.ActiveLabelPolicyByOrg(ctx, object.ResourceOwnerFromReq(ctx, req.Msg.GetCtx()), false) if err != nil { return nil, err } - return &settings.GetBrandingSettingsResponse{ + return connect.NewResponse(&settings.GetBrandingSettingsResponse{ Settings: brandingSettingsToPb(current, s.assetsAPIDomain(ctx)), Details: &object_pb.Details{ Sequence: current.Sequence, @@ -75,15 +76,15 @@ func (s *Server) GetBrandingSettings(ctx context.Context, req *settings.GetBrand ChangeDate: timestamppb.New(current.ChangeDate), ResourceOwner: current.ResourceOwner, }, - }, nil + }), nil } -func (s *Server) GetDomainSettings(ctx context.Context, req *settings.GetDomainSettingsRequest) (*settings.GetDomainSettingsResponse, error) { - current, err := s.query.DomainPolicyByOrg(ctx, true, object.ResourceOwnerFromReq(ctx, req.GetCtx()), false) +func (s *Server) GetDomainSettings(ctx context.Context, req *connect.Request[settings.GetDomainSettingsRequest]) (*connect.Response[settings.GetDomainSettingsResponse], error) { + current, err := s.query.DomainPolicyByOrg(ctx, true, object.ResourceOwnerFromReq(ctx, req.Msg.GetCtx()), false) if err != nil { return nil, err } - return &settings.GetDomainSettingsResponse{ + return connect.NewResponse(&settings.GetDomainSettingsResponse{ Settings: domainSettingsToPb(current), Details: &object_pb.Details{ Sequence: current.Sequence, @@ -91,15 +92,15 @@ func (s *Server) GetDomainSettings(ctx context.Context, req *settings.GetDomainS ChangeDate: timestamppb.New(current.ChangeDate), ResourceOwner: current.ResourceOwner, }, - }, nil + }), nil } -func (s *Server) GetLegalAndSupportSettings(ctx context.Context, req *settings.GetLegalAndSupportSettingsRequest) (*settings.GetLegalAndSupportSettingsResponse, error) { - current, err := s.query.PrivacyPolicyByOrg(ctx, true, object.ResourceOwnerFromReq(ctx, req.GetCtx()), false) +func (s *Server) GetLegalAndSupportSettings(ctx context.Context, req *connect.Request[settings.GetLegalAndSupportSettingsRequest]) (*connect.Response[settings.GetLegalAndSupportSettingsResponse], error) { + current, err := s.query.PrivacyPolicyByOrg(ctx, true, object.ResourceOwnerFromReq(ctx, req.Msg.GetCtx()), false) if err != nil { return nil, err } - return &settings.GetLegalAndSupportSettingsResponse{ + return connect.NewResponse(&settings.GetLegalAndSupportSettingsResponse{ Settings: legalAndSupportSettingsToPb(current), Details: &object_pb.Details{ Sequence: current.Sequence, @@ -107,15 +108,15 @@ func (s *Server) GetLegalAndSupportSettings(ctx context.Context, req *settings.G ChangeDate: timestamppb.New(current.ChangeDate), ResourceOwner: current.ResourceOwner, }, - }, nil + }), nil } -func (s *Server) GetLockoutSettings(ctx context.Context, req *settings.GetLockoutSettingsRequest) (*settings.GetLockoutSettingsResponse, error) { - current, err := s.query.LockoutPolicyByOrg(ctx, true, object.ResourceOwnerFromReq(ctx, req.GetCtx())) +func (s *Server) GetLockoutSettings(ctx context.Context, req *connect.Request[settings.GetLockoutSettingsRequest]) (*connect.Response[settings.GetLockoutSettingsResponse], error) { + current, err := s.query.LockoutPolicyByOrg(ctx, true, object.ResourceOwnerFromReq(ctx, req.Msg.GetCtx())) if err != nil { return nil, err } - return &settings.GetLockoutSettingsResponse{ + return connect.NewResponse(&settings.GetLockoutSettingsResponse{ Settings: lockoutSettingsToPb(current), Details: &object_pb.Details{ Sequence: current.Sequence, @@ -123,46 +124,46 @@ func (s *Server) GetLockoutSettings(ctx context.Context, req *settings.GetLockou ChangeDate: timestamppb.New(current.ChangeDate), ResourceOwner: current.ResourceOwner, }, - }, nil + }), nil } -func (s *Server) GetActiveIdentityProviders(ctx context.Context, req *settings.GetActiveIdentityProvidersRequest) (*settings.GetActiveIdentityProvidersResponse, error) { - links, err := s.query.IDPLoginPolicyLinks(ctx, object.ResourceOwnerFromReq(ctx, req.GetCtx()), &query.IDPLoginPolicyLinksSearchQuery{}, false) +func (s *Server) GetActiveIdentityProviders(ctx context.Context, req *connect.Request[settings.GetActiveIdentityProvidersRequest]) (*connect.Response[settings.GetActiveIdentityProvidersResponse], error) { + links, err := s.query.IDPLoginPolicyLinks(ctx, object.ResourceOwnerFromReq(ctx, req.Msg.GetCtx()), &query.IDPLoginPolicyLinksSearchQuery{}, false) if err != nil { return nil, err } - return &settings.GetActiveIdentityProvidersResponse{ + return connect.NewResponse(&settings.GetActiveIdentityProvidersResponse{ Details: object.ToListDetails(links.SearchResponse), IdentityProviders: identityProvidersToPb(links.Links), - }, nil + }), nil } -func (s *Server) GetGeneralSettings(ctx context.Context, _ *settings.GetGeneralSettingsRequest) (*settings.GetGeneralSettingsResponse, error) { +func (s *Server) GetGeneralSettings(ctx context.Context, _ *connect.Request[settings.GetGeneralSettingsRequest]) (*connect.Response[settings.GetGeneralSettingsResponse], error) { instance := authz.GetInstance(ctx) - return &settings.GetGeneralSettingsResponse{ + return connect.NewResponse(&settings.GetGeneralSettingsResponse{ SupportedLanguages: domain.LanguagesToStrings(i18n.SupportedLanguages()), DefaultOrgId: instance.DefaultOrganisationID(), DefaultLanguage: instance.DefaultLanguage().String(), - }, nil + }), nil } -func (s *Server) GetSecuritySettings(ctx context.Context, req *settings.GetSecuritySettingsRequest) (*settings.GetSecuritySettingsResponse, error) { +func (s *Server) GetSecuritySettings(ctx context.Context, req *connect.Request[settings.GetSecuritySettingsRequest]) (*connect.Response[settings.GetSecuritySettingsResponse], error) { policy, err := s.query.SecurityPolicy(ctx) if err != nil { return nil, err } - return &settings.GetSecuritySettingsResponse{ + return connect.NewResponse(&settings.GetSecuritySettingsResponse{ Settings: securityPolicyToSettingsPb(policy), - }, nil + }), nil } -func (s *Server) SetSecuritySettings(ctx context.Context, req *settings.SetSecuritySettingsRequest) (*settings.SetSecuritySettingsResponse, error) { - details, err := s.command.SetSecurityPolicy(ctx, securitySettingsToCommand(req)) +func (s *Server) SetSecuritySettings(ctx context.Context, req *connect.Request[settings.SetSecuritySettingsRequest]) (*connect.Response[settings.SetSecuritySettingsResponse], error) { + details, err := s.command.SetSecurityPolicy(ctx, securitySettingsToCommand(req.Msg)) if err != nil { return nil, err } - return &settings.SetSecuritySettingsResponse{ + return connect.NewResponse(&settings.SetSecuritySettingsResponse{ Details: object.DomainToDetailsPb(details), - }, nil + }), nil } diff --git a/internal/api/grpc/system/instance.go b/internal/api/grpc/system/instance.go index a5dd7b81bc..ccfcfecbf3 100644 --- a/internal/api/grpc/system/instance.go +++ b/internal/api/grpc/system/instance.go @@ -40,7 +40,7 @@ func (s *Server) GetInstance(ctx context.Context, req *system_pb.GetInstanceRequ } func (s *Server) AddInstance(ctx context.Context, req *system_pb.AddInstanceRequest) (*system_pb.AddInstanceResponse, error) { - id, _, _, details, err := s.command.SetUpInstance(ctx, AddInstancePbToSetupInstance(req, s.defaultInstance, s.externalDomain)) + id, _, _, _, details, err := s.command.SetUpInstance(ctx, AddInstancePbToSetupInstance(req, s.defaultInstance, s.externalDomain)) if err != nil { return nil, err } @@ -61,7 +61,7 @@ func (s *Server) UpdateInstance(ctx context.Context, req *system_pb.UpdateInstan } func (s *Server) CreateInstance(ctx context.Context, req *system_pb.CreateInstanceRequest) (*system_pb.CreateInstanceResponse, error) { - id, pat, key, details, err := s.command.SetUpInstance(ctx, CreateInstancePbToSetupInstance(req, s.defaultInstance, s.externalDomain)) + id, pat, key, _, details, err := s.command.SetUpInstance(ctx, CreateInstancePbToSetupInstance(req, s.defaultInstance, s.externalDomain)) if err != nil { return nil, err } diff --git a/internal/api/grpc/user/v2/email.go b/internal/api/grpc/user/v2/email.go index 4b247ef10f..df68e58c7d 100644 --- a/internal/api/grpc/user/v2/email.go +++ b/internal/api/grpc/user/v2/email.go @@ -3,6 +3,7 @@ package user import ( "context" + "connectrpc.com/connect" "google.golang.org/protobuf/types/known/timestamppb" "github.com/zitadel/zitadel/internal/domain" @@ -11,18 +12,18 @@ import ( "github.com/zitadel/zitadel/pkg/grpc/user/v2" ) -func (s *Server) SetEmail(ctx context.Context, req *user.SetEmailRequest) (resp *user.SetEmailResponse, err error) { +func (s *Server) SetEmail(ctx context.Context, req *connect.Request[user.SetEmailRequest]) (resp *connect.Response[user.SetEmailResponse], err error) { var email *domain.Email - switch v := req.GetVerification().(type) { + switch v := req.Msg.GetVerification().(type) { case *user.SetEmailRequest_SendCode: - email, err = s.command.ChangeUserEmailURLTemplate(ctx, req.GetUserId(), req.GetEmail(), s.userCodeAlg, v.SendCode.GetUrlTemplate()) + email, err = s.command.ChangeUserEmailURLTemplate(ctx, req.Msg.GetUserId(), req.Msg.GetEmail(), s.userCodeAlg, v.SendCode.GetUrlTemplate()) case *user.SetEmailRequest_ReturnCode: - email, err = s.command.ChangeUserEmailReturnCode(ctx, req.GetUserId(), req.GetEmail(), s.userCodeAlg) + email, err = s.command.ChangeUserEmailReturnCode(ctx, req.Msg.GetUserId(), req.Msg.GetEmail(), s.userCodeAlg) case *user.SetEmailRequest_IsVerified: - email, err = s.command.ChangeUserEmailVerified(ctx, req.GetUserId(), req.GetEmail()) + email, err = s.command.ChangeUserEmailVerified(ctx, req.Msg.GetUserId(), req.Msg.GetEmail()) case nil: - email, err = s.command.ChangeUserEmail(ctx, req.GetUserId(), req.GetEmail(), s.userCodeAlg) + email, err = s.command.ChangeUserEmail(ctx, req.Msg.GetUserId(), req.Msg.GetEmail(), s.userCodeAlg) default: err = zerrors.ThrowUnimplementedf(nil, "USERv2-Ahng0", "verification oneOf %T in method SetEmail not implemented", v) } @@ -30,26 +31,26 @@ func (s *Server) SetEmail(ctx context.Context, req *user.SetEmailRequest) (resp return nil, err } - return &user.SetEmailResponse{ + return connect.NewResponse(&user.SetEmailResponse{ Details: &object.Details{ Sequence: email.Sequence, ChangeDate: timestamppb.New(email.ChangeDate), ResourceOwner: email.ResourceOwner, }, VerificationCode: email.PlainCode, - }, nil + }), nil } -func (s *Server) ResendEmailCode(ctx context.Context, req *user.ResendEmailCodeRequest) (resp *user.ResendEmailCodeResponse, err error) { +func (s *Server) ResendEmailCode(ctx context.Context, req *connect.Request[user.ResendEmailCodeRequest]) (resp *connect.Response[user.ResendEmailCodeResponse], err error) { var email *domain.Email - switch v := req.GetVerification().(type) { + switch v := req.Msg.GetVerification().(type) { case *user.ResendEmailCodeRequest_SendCode: - email, err = s.command.ResendUserEmailCodeURLTemplate(ctx, req.GetUserId(), s.userCodeAlg, v.SendCode.GetUrlTemplate()) + email, err = s.command.ResendUserEmailCodeURLTemplate(ctx, req.Msg.GetUserId(), s.userCodeAlg, v.SendCode.GetUrlTemplate()) case *user.ResendEmailCodeRequest_ReturnCode: - email, err = s.command.ResendUserEmailReturnCode(ctx, req.GetUserId(), s.userCodeAlg) + email, err = s.command.ResendUserEmailReturnCode(ctx, req.Msg.GetUserId(), s.userCodeAlg) case nil: - email, err = s.command.ResendUserEmailCode(ctx, req.GetUserId(), s.userCodeAlg) + email, err = s.command.ResendUserEmailCode(ctx, req.Msg.GetUserId(), s.userCodeAlg) default: err = zerrors.ThrowUnimplementedf(nil, "USERv2-faj0l0nj5x", "verification oneOf %T in method ResendEmailCode not implemented", v) } @@ -57,26 +58,26 @@ func (s *Server) ResendEmailCode(ctx context.Context, req *user.ResendEmailCodeR return nil, err } - return &user.ResendEmailCodeResponse{ + return connect.NewResponse(&user.ResendEmailCodeResponse{ Details: &object.Details{ Sequence: email.Sequence, ChangeDate: timestamppb.New(email.ChangeDate), ResourceOwner: email.ResourceOwner, }, VerificationCode: email.PlainCode, - }, nil + }), nil } -func (s *Server) SendEmailCode(ctx context.Context, req *user.SendEmailCodeRequest) (resp *user.SendEmailCodeResponse, err error) { +func (s *Server) SendEmailCode(ctx context.Context, req *connect.Request[user.SendEmailCodeRequest]) (resp *connect.Response[user.SendEmailCodeResponse], err error) { var email *domain.Email - switch v := req.GetVerification().(type) { + switch v := req.Msg.GetVerification().(type) { case *user.SendEmailCodeRequest_SendCode: - email, err = s.command.SendUserEmailCodeURLTemplate(ctx, req.GetUserId(), s.userCodeAlg, v.SendCode.GetUrlTemplate()) + email, err = s.command.SendUserEmailCodeURLTemplate(ctx, req.Msg.GetUserId(), s.userCodeAlg, v.SendCode.GetUrlTemplate()) case *user.SendEmailCodeRequest_ReturnCode: - email, err = s.command.SendUserEmailReturnCode(ctx, req.GetUserId(), s.userCodeAlg) + email, err = s.command.SendUserEmailReturnCode(ctx, req.Msg.GetUserId(), s.userCodeAlg) case nil: - email, err = s.command.SendUserEmailCode(ctx, req.GetUserId(), s.userCodeAlg) + email, err = s.command.SendUserEmailCode(ctx, req.Msg.GetUserId(), s.userCodeAlg) default: err = zerrors.ThrowUnimplementedf(nil, "USERv2-faj0l0nj5x", "verification oneOf %T in method SendEmailCode not implemented", v) } @@ -84,30 +85,30 @@ func (s *Server) SendEmailCode(ctx context.Context, req *user.SendEmailCodeReque return nil, err } - return &user.SendEmailCodeResponse{ + return connect.NewResponse(&user.SendEmailCodeResponse{ Details: &object.Details{ Sequence: email.Sequence, ChangeDate: timestamppb.New(email.ChangeDate), ResourceOwner: email.ResourceOwner, }, VerificationCode: email.PlainCode, - }, nil + }), nil } -func (s *Server) VerifyEmail(ctx context.Context, req *user.VerifyEmailRequest) (*user.VerifyEmailResponse, error) { +func (s *Server) VerifyEmail(ctx context.Context, req *connect.Request[user.VerifyEmailRequest]) (*connect.Response[user.VerifyEmailResponse], error) { details, err := s.command.VerifyUserEmail(ctx, - req.GetUserId(), - req.GetVerificationCode(), + req.Msg.GetUserId(), + req.Msg.GetVerificationCode(), s.userCodeAlg, ) if err != nil { return nil, err } - return &user.VerifyEmailResponse{ + return connect.NewResponse(&user.VerifyEmailResponse{ Details: &object.Details{ Sequence: details.Sequence, ChangeDate: timestamppb.New(details.EventDate), ResourceOwner: details.ResourceOwner, }, - }, nil + }), nil } diff --git a/internal/api/grpc/user/v2/human.go b/internal/api/grpc/user/v2/human.go new file mode 100644 index 0000000000..03af0f75be --- /dev/null +++ b/internal/api/grpc/user/v2/human.go @@ -0,0 +1,196 @@ +package user + +import ( + "context" + "io" + + "connectrpc.com/connect" + "golang.org/x/text/language" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/zerrors" + legacyobject "github.com/zitadel/zitadel/pkg/grpc/object/v2" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +func (s *Server) createUserTypeHuman(ctx context.Context, humanPb *user.CreateUserRequest_Human, orgId string, userName, userId *string) (*connect.Response[user.CreateUserResponse], error) { + metadataEntries := make([]*user.SetMetadataEntry, len(humanPb.Metadata)) + for i, metadataEntry := range humanPb.Metadata { + metadataEntries[i] = &user.SetMetadataEntry{ + Key: metadataEntry.GetKey(), + Value: metadataEntry.GetValue(), + } + } + addHumanPb := &user.AddHumanUserRequest{ + Username: userName, + UserId: userId, + Organization: &legacyobject.Organization{ + Org: &legacyobject.Organization_OrgId{OrgId: orgId}, + }, + Profile: humanPb.Profile, + Email: humanPb.Email, + Phone: humanPb.Phone, + IdpLinks: humanPb.IdpLinks, + TotpSecret: humanPb.TotpSecret, + Metadata: metadataEntries, + } + switch pwType := humanPb.GetPasswordType().(type) { + case *user.CreateUserRequest_Human_HashedPassword: + addHumanPb.PasswordType = &user.AddHumanUserRequest_HashedPassword{ + HashedPassword: pwType.HashedPassword, + } + case *user.CreateUserRequest_Human_Password: + addHumanPb.PasswordType = &user.AddHumanUserRequest_Password{ + Password: pwType.Password, + } + default: + // optional password is not set + } + newHuman, err := AddUserRequestToAddHuman(addHumanPb) + if err != nil { + return nil, err + } + if err = s.command.AddUserHuman( + ctx, + orgId, + newHuman, + false, + s.userCodeAlg, + ); err != nil { + return nil, err + } + return connect.NewResponse(&user.CreateUserResponse{ + Id: newHuman.ID, + CreationDate: timestamppb.New(newHuman.Details.EventDate), + EmailCode: newHuman.EmailCode, + PhoneCode: newHuman.PhoneCode, + }), nil +} + +func (s *Server) updateUserTypeHuman(ctx context.Context, humanPb *user.UpdateUserRequest_Human, userId string, userName *string) (*connect.Response[user.UpdateUserResponse], error) { + cmd, err := updateHumanUserToCommand(userId, userName, humanPb) + if err != nil { + return nil, err + } + if err = s.command.ChangeUserHuman(ctx, cmd, s.userCodeAlg); err != nil { + return nil, err + } + return connect.NewResponse(&user.UpdateUserResponse{ + ChangeDate: timestamppb.New(cmd.Details.EventDate), + EmailCode: cmd.EmailCode, + PhoneCode: cmd.PhoneCode, + }), nil +} + +func updateHumanUserToCommand(userId string, userName *string, human *user.UpdateUserRequest_Human) (*command.ChangeHuman, error) { + phone := human.GetPhone() + if phone != nil && phone.Phone == "" && phone.GetVerification() != nil { + return nil, zerrors.ThrowInvalidArgument(nil, "USERv2-4f3d6", "Errors.User.Phone.VerifyingRemovalIsNotSupported") + } + email, err := setHumanEmailToEmail(human.Email, userId) + if err != nil { + return nil, err + } + return &command.ChangeHuman{ + ID: userId, + Username: userName, + Profile: SetHumanProfileToProfile(human.Profile), + Email: email, + Phone: setHumanPhoneToPhone(human.Phone, true), + Password: setHumanPasswordToPassword(human.Password), + }, nil +} + +func updateHumanUserRequestToChangeHuman(req *user.UpdateHumanUserRequest) (*command.ChangeHuman, error) { + email, err := setHumanEmailToEmail(req.Email, req.GetUserId()) + if err != nil { + return nil, err + } + changeHuman := &command.ChangeHuman{ + ID: req.GetUserId(), + Username: req.Username, + Email: email, + Phone: setHumanPhoneToPhone(req.Phone, false), + Password: setHumanPasswordToPassword(req.Password), + } + if profile := req.GetProfile(); profile != nil { + var firstName *string + if profile.GivenName != "" { + firstName = &profile.GivenName + } + var lastName *string + if profile.FamilyName != "" { + lastName = &profile.FamilyName + } + changeHuman.Profile = SetHumanProfileToProfile(&user.UpdateUserRequest_Human_Profile{ + GivenName: firstName, + FamilyName: lastName, + NickName: profile.NickName, + DisplayName: profile.DisplayName, + PreferredLanguage: profile.PreferredLanguage, + Gender: profile.Gender, + }) + } + return changeHuman, nil +} + +func SetHumanProfileToProfile(profile *user.UpdateUserRequest_Human_Profile) *command.Profile { + if profile == nil { + return nil + } + return &command.Profile{ + FirstName: profile.GivenName, + LastName: profile.FamilyName, + NickName: profile.NickName, + DisplayName: profile.DisplayName, + PreferredLanguage: ifNotNilPtr(profile.PreferredLanguage, language.Make), + Gender: ifNotNilPtr(profile.Gender, genderToDomain), + } +} + +func setHumanEmailToEmail(email *user.SetHumanEmail, userID string) (*command.Email, error) { + if email == nil { + return nil, nil + } + var urlTemplate string + if email.GetSendCode() != nil && email.GetSendCode().UrlTemplate != nil { + urlTemplate = *email.GetSendCode().UrlTemplate + if err := domain.RenderConfirmURLTemplate(io.Discard, urlTemplate, userID, "code", "orgID"); err != nil { + return nil, err + } + } + return &command.Email{ + Address: domain.EmailAddress(email.Email), + Verified: email.GetIsVerified(), + ReturnCode: email.GetReturnCode() != nil, + URLTemplate: urlTemplate, + }, nil +} + +func setHumanPhoneToPhone(phone *user.SetHumanPhone, withRemove bool) *command.Phone { + if phone == nil { + return nil + } + number := phone.GetPhone() + return &command.Phone{ + Number: domain.PhoneNumber(number), + Verified: phone.GetIsVerified(), + ReturnCode: phone.GetReturnCode() != nil, + Remove: withRemove && number == "", + } +} + +func setHumanPasswordToPassword(password *user.SetPassword) *command.Password { + if password == nil { + return nil + } + return &command.Password{ + PasswordCode: password.GetVerificationCode(), + OldPassword: password.GetCurrentPassword(), + Password: password.GetPassword().GetPassword(), + EncodedPasswordHash: password.GetHashedPassword().GetHash(), + ChangeRequired: password.GetPassword().GetChangeRequired() || password.GetHashedPassword().GetChangeRequired(), + } +} diff --git a/internal/api/grpc/user/v2/human_test.go b/internal/api/grpc/user/v2/human_test.go new file mode 100644 index 0000000000..52e5371dcc --- /dev/null +++ b/internal/api/grpc/user/v2/human_test.go @@ -0,0 +1,254 @@ +package user + +import ( + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "golang.org/x/text/language" + + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +func Test_patchHumanUserToCommand(t *testing.T) { + type args struct { + userId string + userName *string + human *user.UpdateUserRequest_Human + } + tests := []struct { + name string + args args + want *command.ChangeHuman + wantErr assert.ErrorAssertionFunc + }{{ + name: "single property", + args: args{ + userId: "userId", + human: &user.UpdateUserRequest_Human{ + Profile: &user.UpdateUserRequest_Human_Profile{ + GivenName: gu.Ptr("givenName"), + }, + }, + }, + want: &command.ChangeHuman{ + ID: "userId", + Profile: &command.Profile{ + FirstName: gu.Ptr("givenName"), + }, + }, + wantErr: assert.NoError, + }, { + name: "all properties", + args: args{ + userId: "userId", + userName: gu.Ptr("userName"), + human: &user.UpdateUserRequest_Human{ + Profile: &user.UpdateUserRequest_Human_Profile{ + GivenName: gu.Ptr("givenName"), + FamilyName: gu.Ptr("familyName"), + NickName: gu.Ptr("nickName"), + DisplayName: gu.Ptr("displayName"), + PreferredLanguage: gu.Ptr("en-US"), + Gender: gu.Ptr(user.Gender_GENDER_FEMALE), + }, + Email: &user.SetHumanEmail{ + Email: "email@example.com", + Verification: &user.SetHumanEmail_IsVerified{ + IsVerified: true, + }, + }, + Phone: &user.SetHumanPhone{ + Phone: "+123456789", + Verification: &user.SetHumanPhone_IsVerified{ + IsVerified: true, + }, + }, + Password: &user.SetPassword{ + Verification: &user.SetPassword_CurrentPassword{ + CurrentPassword: "currentPassword", + }, + PasswordType: &user.SetPassword_Password{ + Password: &user.Password{ + Password: "newPassword", + ChangeRequired: true, + }, + }, + }, + }, + }, + want: &command.ChangeHuman{ + ID: "userId", + Username: gu.Ptr("userName"), + Profile: &command.Profile{ + FirstName: gu.Ptr("givenName"), + LastName: gu.Ptr("familyName"), + NickName: gu.Ptr("nickName"), + DisplayName: gu.Ptr("displayName"), + PreferredLanguage: &language.AmericanEnglish, + Gender: gu.Ptr(domain.GenderFemale), + }, + Email: &command.Email{ + Address: "email@example.com", + Verified: true, + }, + Phone: &command.Phone{ + Number: "+123456789", + Verified: true, + }, + Password: &command.Password{ + OldPassword: "currentPassword", + Password: "newPassword", + ChangeRequired: true, + }, + }, + wantErr: assert.NoError, + }, { + name: "set email and request code", + args: args{ + userId: "userId", + human: &user.UpdateUserRequest_Human{ + Email: &user.SetHumanEmail{ + Email: "email@example.com", + Verification: &user.SetHumanEmail_ReturnCode{ + ReturnCode: &user.ReturnEmailVerificationCode{}, + }, + }, + }, + }, + want: &command.ChangeHuman{ + ID: "userId", + Email: &command.Email{ + Address: "email@example.com", + ReturnCode: true, + }, + }, + wantErr: assert.NoError, + }, { + name: "set email and send code", + args: args{ + userId: "userId", + human: &user.UpdateUserRequest_Human{ + Email: &user.SetHumanEmail{ + Email: "email@example.com", + Verification: &user.SetHumanEmail_SendCode{ + SendCode: &user.SendEmailVerificationCode{}, + }, + }, + }, + }, + want: &command.ChangeHuman{ + ID: "userId", + Email: &command.Email{ + Address: "email@example.com", + }, + }, + wantErr: assert.NoError, + }, { + name: "set email and send code with template", + args: args{ + userId: "userId", + human: &user.UpdateUserRequest_Human{ + Email: &user.SetHumanEmail{ + Email: "email@example.com", + Verification: &user.SetHumanEmail_SendCode{ + SendCode: &user.SendEmailVerificationCode{ + UrlTemplate: gu.Ptr("Code: {{.Code}}"), + }, + }, + }, + }, + }, + want: &command.ChangeHuman{ + ID: "userId", + Email: &command.Email{ + Address: "email@example.com", + URLTemplate: "Code: {{.Code}}", + }, + }, + wantErr: assert.NoError, + }, { + name: "set phone and request code", + args: args{ + userId: "userId", + human: &user.UpdateUserRequest_Human{ + Phone: &user.SetHumanPhone{ + Phone: "+123456789", + Verification: &user.SetHumanPhone_ReturnCode{ + ReturnCode: &user.ReturnPhoneVerificationCode{}, + }, + }, + }, + }, + want: &command.ChangeHuman{ + ID: "userId", + Phone: &command.Phone{ + Number: "+123456789", + ReturnCode: true, + }, + }, + wantErr: assert.NoError, + }, { + name: "set phone and send code", + args: args{ + userId: "userId", + human: &user.UpdateUserRequest_Human{ + Phone: &user.SetHumanPhone{ + Phone: "+123456789", + Verification: &user.SetHumanPhone_SendCode{ + SendCode: &user.SendPhoneVerificationCode{}, + }, + }, + }, + }, + want: &command.ChangeHuman{ + ID: "userId", + Phone: &command.Phone{ + Number: "+123456789", + }, + }, + wantErr: assert.NoError, + }, { + name: "remove phone, ok", + args: args{ + userId: "userId", + human: &user.UpdateUserRequest_Human{ + Phone: &user.SetHumanPhone{}, + }, + }, + want: &command.ChangeHuman{ + ID: "userId", + Phone: &command.Phone{ + Remove: true, + }, + }, + wantErr: assert.NoError, + }, { + name: "remove phone with verification, error", + args: args{ + userId: "userId", + human: &user.UpdateUserRequest_Human{ + Phone: &user.SetHumanPhone{ + Verification: &user.SetHumanPhone_ReturnCode{}, + }, + }, + }, + wantErr: assert.Error, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := updateHumanUserToCommand(tt.args.userId, tt.args.userName, tt.args.human) + if !tt.wantErr(t, err, fmt.Sprintf("patchHumanUserToCommand(%v, %v, %v)", tt.args.userId, tt.args.userName, tt.args.human)) { + return + } + if diff := cmp.Diff(tt.want, got, cmpopts.EquateComparable(language.Tag{})); diff != "" { + t.Errorf("patchHumanUserToCommand() mismatch (-want +got):\n%s", diff) + } + }) + } +} diff --git a/internal/api/grpc/user/v2/idp_link.go b/internal/api/grpc/user/v2/idp_link.go index bef40617cf..0b1e7ab998 100644 --- a/internal/api/grpc/user/v2/idp_link.go +++ b/internal/api/grpc/user/v2/idp_link.go @@ -3,6 +3,8 @@ package user import ( "context" + "connectrpc.com/connect" + "github.com/zitadel/zitadel/internal/api/grpc/object/v2" "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/domain" @@ -11,22 +13,22 @@ import ( "github.com/zitadel/zitadel/pkg/grpc/user/v2" ) -func (s *Server) AddIDPLink(ctx context.Context, req *user.AddIDPLinkRequest) (_ *user.AddIDPLinkResponse, err error) { - details, err := s.command.AddUserIDPLink(ctx, req.UserId, "", &command.AddLink{ - IDPID: req.GetIdpLink().GetIdpId(), - DisplayName: req.GetIdpLink().GetUserName(), - IDPExternalID: req.GetIdpLink().GetUserId(), +func (s *Server) AddIDPLink(ctx context.Context, req *connect.Request[user.AddIDPLinkRequest]) (_ *connect.Response[user.AddIDPLinkResponse], err error) { + details, err := s.command.AddUserIDPLink(ctx, req.Msg.GetUserId(), "", &command.AddLink{ + IDPID: req.Msg.GetIdpLink().GetIdpId(), + DisplayName: req.Msg.GetIdpLink().GetUserName(), + IDPExternalID: req.Msg.GetIdpLink().GetUserId(), }) if err != nil { return nil, err } - return &user.AddIDPLinkResponse{ + return connect.NewResponse(&user.AddIDPLinkResponse{ Details: object.DomainToDetailsPb(details), - }, nil + }), nil } -func (s *Server) ListIDPLinks(ctx context.Context, req *user.ListIDPLinksRequest) (_ *user.ListIDPLinksResponse, err error) { - queries, err := ListLinkedIDPsRequestToQuery(req) +func (s *Server) ListIDPLinks(ctx context.Context, req *connect.Request[user.ListIDPLinksRequest]) (_ *connect.Response[user.ListIDPLinksResponse], err error) { + queries, err := ListLinkedIDPsRequestToQuery(req.Msg) if err != nil { return nil, err } @@ -34,10 +36,10 @@ func (s *Server) ListIDPLinks(ctx context.Context, req *user.ListIDPLinksRequest if err != nil { return nil, err } - return &user.ListIDPLinksResponse{ + return connect.NewResponse(&user.ListIDPLinksResponse{ Result: IDPLinksToPb(res.Links), Details: object.ToListDetails(res.SearchResponse), - }, nil + }), nil } func ListLinkedIDPsRequestToQuery(req *user.ListIDPLinksRequest) (*query.IDPUserLinksSearchQuery, error) { @@ -72,14 +74,14 @@ func IDPLinkToPb(link *query.IDPUserLink) *user.IDPLink { } } -func (s *Server) RemoveIDPLink(ctx context.Context, req *user.RemoveIDPLinkRequest) (*user.RemoveIDPLinkResponse, error) { - objectDetails, err := s.command.RemoveUserIDPLink(ctx, RemoveIDPLinkRequestToDomain(ctx, req)) +func (s *Server) RemoveIDPLink(ctx context.Context, req *connect.Request[user.RemoveIDPLinkRequest]) (*connect.Response[user.RemoveIDPLinkResponse], error) { + objectDetails, err := s.command.RemoveUserIDPLink(ctx, RemoveIDPLinkRequestToDomain(ctx, req.Msg)) if err != nil { return nil, err } - return &user.RemoveIDPLinkResponse{ + return connect.NewResponse(&user.RemoveIDPLinkResponse{ Details: object.DomainToDetailsPb(objectDetails), - }, nil + }), nil } func RemoveIDPLinkRequestToDomain(ctx context.Context, req *user.RemoveIDPLinkRequest) *domain.UserIDPLink { diff --git a/internal/api/grpc/user/v2/integration_test/email_test.go b/internal/api/grpc/user/v2/integration_test/email_test.go index ad63c2ce5e..87f8a04dd8 100644 --- a/internal/api/grpc/user/v2/integration_test/email_test.go +++ b/internal/api/grpc/user/v2/integration_test/email_test.go @@ -16,7 +16,7 @@ import ( "github.com/zitadel/zitadel/pkg/grpc/user/v2" ) -func TestServer_SetEmail(t *testing.T) { +func TestServer_Deprecated_SetEmail(t *testing.T) { userID := Instance.CreateHumanUser(CTX).GetUserId() tests := []struct { diff --git a/internal/api/grpc/user/v2/integration_test/key_test.go b/internal/api/grpc/user/v2/integration_test/key_test.go new file mode 100644 index 0000000000..bb4f8657fa --- /dev/null +++ b/internal/api/grpc/user/v2/integration_test/key_test.go @@ -0,0 +1,659 @@ +//go:build integration + +package user_test + +import ( + "context" + "fmt" + "slices" + "testing" + "time" + + "github.com/brianvoe/gofakeit/v6" + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/testing/protocmp" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/integration" + "github.com/zitadel/zitadel/pkg/grpc/filter/v2" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +func TestServer_AddKey(t *testing.T) { + resp := Instance.CreateUserTypeMachine(IamCTX, Instance.DefaultOrg.Id) + userId := resp.GetId() + expirationDate := timestamppb.New(time.Now().Add(time.Hour * 24)) + type args struct { + req *user.AddKeyRequest + prepare func(request *user.AddKeyRequest) error + } + tests := []struct { + name string + args args + wantErr bool + wantEmtpyKey bool + }{ + { + name: "add key, user not existing", + args: args{ + &user.AddKeyRequest{ + UserId: "notexisting", + ExpirationDate: expirationDate, + }, + func(request *user.AddKeyRequest) error { return nil }, + }, + wantErr: true, + }, + { + name: "generate key pair, ok", + args: args{ + &user.AddKeyRequest{ + ExpirationDate: expirationDate, + }, + func(request *user.AddKeyRequest) error { + request.UserId = userId + return nil + }, + }, + }, + { + name: "add valid public key, ok", + args: args{ + &user.AddKeyRequest{ + ExpirationDate: expirationDate, + // This is the public key of the tester system user. This must be valid. + PublicKey: []byte(` +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzi+FFSJL7f5yw4KTwzgM +P34ePGycm/M+kT0M7V4Cgx5V3EaDIvTQKTLfBaEB45zb9LtjIXzDw0rXRoS2hO6t +h+CYQCz3KCvh09C0IzxZiB2IS3H/aT+5Bx9EFY+vnAkZjccbyG5YNRvmtOlnvIeI +H7qZ0tEwkPfF5GEZNPJPtmy3UGV7iofdVQS1xRj73+aMw5rvH4D8IdyiAC3VekIb +pt0Vj0SUX3DwKtog337BzTiPk3aXRF0sbFhQoqdJRI8NqgZjCwjq9yfI5tyxYswn ++JGzHGdHvW3idODlmwEt5K2pasiRIWK2OGfq+w0EcltQHabuqEPgZlmhCkRdNfix +BwIDAQAB +-----END PUBLIC KEY----- +`), + }, + func(request *user.AddKeyRequest) error { + request.UserId = userId + return nil + }, + }, + wantEmtpyKey: true, + }, + { + name: "add invalid public key, error", + args: args{ + &user.AddKeyRequest{ + ExpirationDate: expirationDate, + PublicKey: []byte(` +-----BEGIN PUBLIC KEY----- +abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 +-----END PUBLIC KEY----- +`), + }, + func(request *user.AddKeyRequest) error { + request.UserId = userId + return nil + }, + }, + wantErr: true, + }, + { + name: "add key human, error", + args: args{ + &user.AddKeyRequest{ + ExpirationDate: expirationDate, + }, + func(request *user.AddKeyRequest) error { + resp := Instance.CreateUserTypeHuman(IamCTX, gofakeit.Email()) + request.UserId = resp.Id + return nil + }, + }, + wantErr: true, + }, + { + name: "add another key, ok", + args: args{ + &user.AddKeyRequest{ + ExpirationDate: expirationDate, + }, + func(request *user.AddKeyRequest) error { + request.UserId = userId + _, err := Client.AddKey(IamCTX, &user.AddKeyRequest{ + ExpirationDate: expirationDate, + UserId: userId, + }) + return err + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + err := tt.args.prepare(tt.args.req) + require.NoError(t, err) + got, err := Client.AddKey(CTX, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.NotEmpty(t, got.KeyId, "key id is empty") + if tt.wantEmtpyKey { + assert.Empty(t, got.KeyContent, "key content is not empty") + } else { + assert.NotEmpty(t, got.KeyContent, "key content is empty") + } + creationDate := got.CreationDate.AsTime() + assert.Greater(t, creationDate, now, "creation date is before the test started") + assert.Less(t, creationDate, time.Now(), "creation date is in the future") + }) + } +} + +func TestServer_AddKey_Permission(t *testing.T) { + OrgCTX := CTX + otherOrg := Instance.CreateOrganization(IamCTX, fmt.Sprintf("AddKey-%s", gofakeit.AppName()), gofakeit.Email()) + otherOrgUser, err := Client.CreateUser(IamCTX, &user.CreateUserRequest{ + OrganizationId: otherOrg.OrganizationId, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: gofakeit.Name(), + }, + }, + }) + require.NoError(t, err) + request := &user.AddKeyRequest{ + ExpirationDate: timestamppb.New(time.Now().Add(time.Hour * 24)), + UserId: otherOrgUser.GetId(), + } + type args struct { + ctx context.Context + req *user.AddKeyRequest + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "system, ok", + args: args{SystemCTX, request}, + }, + { + name: "instance, ok", + args: args{IamCTX, request}, + }, + { + name: "org, error", + args: args{OrgCTX, request}, + wantErr: true, + }, + { + name: "user, error", + args: args{UserCTX, request}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + require.NoError(t, err) + got, err := Client.AddKey(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.NotEmpty(t, got.KeyId, "key id is empty") + assert.NotEmpty(t, got.KeyContent, "key content is empty") + creationDate := got.CreationDate.AsTime() + assert.Greater(t, creationDate, now, "creation date is before the test started") + assert.Less(t, creationDate, time.Now(), "creation date is in the future") + }) + } +} + +func TestServer_RemoveKey(t *testing.T) { + resp := Instance.CreateUserTypeMachine(IamCTX, Instance.DefaultOrg.Id) + userId := resp.GetId() + expirationDate := timestamppb.New(time.Now().Add(time.Hour * 24)) + type args struct { + req *user.RemoveKeyRequest + prepare func(request *user.RemoveKeyRequest) error + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "remove key, user not existing", + args: args{ + &user.RemoveKeyRequest{ + UserId: "notexisting", + }, + func(request *user.RemoveKeyRequest) error { + key, err := Client.AddKey(IamCTX, &user.AddKeyRequest{ + ExpirationDate: expirationDate, + UserId: userId, + }) + request.KeyId = key.GetKeyId() + return err + }, + }, + wantErr: true, + }, + { + name: "remove key, not existing", + args: args{ + &user.RemoveKeyRequest{ + KeyId: "notexisting", + }, + func(request *user.RemoveKeyRequest) error { + request.UserId = userId + return nil + }, + }, + wantErr: true, + }, + { + name: "remove key, ok", + args: args{ + &user.RemoveKeyRequest{}, + func(request *user.RemoveKeyRequest) error { + key, err := Client.AddKey(IamCTX, &user.AddKeyRequest{ + ExpirationDate: expirationDate, + UserId: userId, + }) + request.KeyId = key.GetKeyId() + request.UserId = userId + return err + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + err := tt.args.prepare(tt.args.req) + require.NoError(t, err) + got, err := Client.RemoveKey(CTX, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + deletionDate := got.DeletionDate.AsTime() + assert.Greater(t, deletionDate, now, "creation date is before the test started") + assert.Less(t, deletionDate, time.Now(), "creation date is in the future") + }) + } +} + +func TestServer_RemoveKey_Permission(t *testing.T) { + OrgCTX := CTX + otherOrg := Instance.CreateOrganization(IamCTX, fmt.Sprintf("RemoveKey-%s", gofakeit.AppName()), gofakeit.Email()) + otherOrgUser, err := Client.CreateUser(IamCTX, &user.CreateUserRequest{ + OrganizationId: otherOrg.OrganizationId, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: gofakeit.Name(), + }, + }, + }) + request := &user.RemoveKeyRequest{ + UserId: otherOrgUser.GetId(), + } + prepare := func(request *user.RemoveKeyRequest) error { + key, err := Client.AddKey(IamCTX, &user.AddKeyRequest{ + ExpirationDate: timestamppb.New(time.Now().Add(time.Hour * 24)), + UserId: otherOrgUser.GetId(), + }) + request.KeyId = key.GetKeyId() + return err + } + require.NoError(t, err) + type args struct { + ctx context.Context + req *user.RemoveKeyRequest + prepare func(request *user.RemoveKeyRequest) error + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "system, ok", + args: args{SystemCTX, request, prepare}, + }, + { + name: "instance, ok", + args: args{IamCTX, request, prepare}, + }, + { + name: "org, error", + args: args{OrgCTX, request, prepare}, + wantErr: true, + }, + { + name: "user, error", + args: args{UserCTX, request, prepare}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + require.NoError(t, tt.args.prepare(tt.args.req)) + got, err := Client.RemoveKey(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.NotEmpty(t, got.DeletionDate, "client key is empty") + creationDate := got.DeletionDate.AsTime() + assert.Greater(t, creationDate, now, "creation date is before the test started") + assert.Less(t, creationDate, time.Now(), "creation date is in the future") + }) + } +} + +func TestServer_ListKeys(t *testing.T) { + type args struct { + ctx context.Context + req *user.ListKeysRequest + } + type testCase struct { + name string + args args + want *user.ListKeysResponse + } + OrgCTX := CTX + otherOrg := Instance.CreateOrganization(SystemCTX, fmt.Sprintf("ListKeys-%s", gofakeit.AppName()), gofakeit.Email()) + otherOrgUser, err := Client.CreateUser(SystemCTX, &user.CreateUserRequest{ + OrganizationId: otherOrg.OrganizationId, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: gofakeit.Name(), + }, + }, + }) + require.NoError(t, err) + otherOrgUserId := otherOrgUser.GetId() + otherUserId := Instance.CreateUserTypeMachine(SystemCTX, Instance.DefaultOrg.Id).GetId() + onlySinceTestStartFilter := &user.KeysSearchFilter{Filter: &user.KeysSearchFilter_CreatedDateFilter{CreatedDateFilter: &filter.TimestampFilter{ + Timestamp: timestamppb.Now(), + Method: filter.TimestampFilterMethod_TIMESTAMP_FILTER_METHOD_AFTER_OR_EQUALS, + }}} + myOrgId := Instance.DefaultOrg.GetId() + myUserId := Instance.Users.Get(integration.UserTypeNoPermission).ID + expiresInADay := time.Now().Truncate(time.Hour).Add(time.Hour * 24) + myDataPoint := setupKeyDataPoint(t, myUserId, myOrgId, expiresInADay) + otherUserDataPoint := setupKeyDataPoint(t, otherUserId, myOrgId, expiresInADay) + otherOrgDataPointExpiringSoon := setupKeyDataPoint(t, otherOrgUserId, otherOrg.OrganizationId, time.Now().Truncate(time.Hour).Add(time.Hour)) + otherOrgDataPointExpiringLate := setupKeyDataPoint(t, otherOrgUserId, otherOrg.OrganizationId, expiresInADay.Add(time.Hour*24*30)) + sortingColumnExpirationDate := user.KeyFieldName_KEY_FIELD_NAME_KEY_EXPIRATION_DATE + awaitKeys(t, onlySinceTestStartFilter, + otherOrgDataPointExpiringSoon.GetId(), + otherOrgDataPointExpiringLate.GetId(), + otherUserDataPoint.GetId(), + myDataPoint.GetId(), + ) + tests := []testCase{ + { + name: "list all, instance", + args: args{ + IamCTX, + &user.ListKeysRequest{Filters: []*user.KeysSearchFilter{onlySinceTestStartFilter}}, + }, + want: &user.ListKeysResponse{ + Result: []*user.Key{ + otherOrgDataPointExpiringLate, + otherOrgDataPointExpiringSoon, + otherUserDataPoint, + myDataPoint, + }, + Pagination: &filter.PaginationResponse{ + TotalResult: 4, + AppliedLimit: 100, + }, + }, + }, + { + name: "list all, org", + args: args{ + OrgCTX, + &user.ListKeysRequest{Filters: []*user.KeysSearchFilter{onlySinceTestStartFilter}}, + }, + want: &user.ListKeysResponse{ + Result: []*user.Key{ + otherUserDataPoint, + myDataPoint, + }, + Pagination: &filter.PaginationResponse{ + TotalResult: 2, + AppliedLimit: 100, + }, + }, + }, + { + name: "list all, user", + args: args{ + UserCTX, + &user.ListKeysRequest{Filters: []*user.KeysSearchFilter{onlySinceTestStartFilter}}, + }, + want: &user.ListKeysResponse{ + Result: []*user.Key{ + myDataPoint, + }, + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + }, + }, + { + name: "list by id", + args: args{ + IamCTX, + &user.ListKeysRequest{ + Filters: []*user.KeysSearchFilter{ + onlySinceTestStartFilter, + { + Filter: &user.KeysSearchFilter_KeyIdFilter{ + KeyIdFilter: &filter.IDFilter{Id: otherOrgDataPointExpiringSoon.Id}, + }, + }, + }, + }, + }, + want: &user.ListKeysResponse{ + Result: []*user.Key{ + otherOrgDataPointExpiringSoon, + }, + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + }, + }, + { + name: "list all from other org", + args: args{ + IamCTX, + &user.ListKeysRequest{ + Filters: []*user.KeysSearchFilter{ + onlySinceTestStartFilter, + { + Filter: &user.KeysSearchFilter_OrganizationIdFilter{ + OrganizationIdFilter: &filter.IDFilter{Id: otherOrg.OrganizationId}, + }, + }, + }, + }, + }, + want: &user.ListKeysResponse{ + Result: []*user.Key{ + otherOrgDataPointExpiringLate, + otherOrgDataPointExpiringSoon, + }, + Pagination: &filter.PaginationResponse{ + TotalResult: 2, + AppliedLimit: 100, + }, + }, + }, + { + name: "sort by next expiration dates", + args: args{ + IamCTX, + &user.ListKeysRequest{ + Pagination: &filter.PaginationRequest{ + Asc: true, + }, + SortingColumn: &sortingColumnExpirationDate, + Filters: []*user.KeysSearchFilter{ + onlySinceTestStartFilter, + {Filter: &user.KeysSearchFilter_OrganizationIdFilter{OrganizationIdFilter: &filter.IDFilter{Id: otherOrg.OrganizationId}}}, + }, + }, + }, + want: &user.ListKeysResponse{ + Result: []*user.Key{ + otherOrgDataPointExpiringSoon, + otherOrgDataPointExpiringLate, + }, + Pagination: &filter.PaginationResponse{ + TotalResult: 2, + AppliedLimit: 100, + }, + }, + }, + { + name: "get page", + args: args{ + IamCTX, + &user.ListKeysRequest{ + Pagination: &filter.PaginationRequest{ + Offset: 2, + Limit: 2, + Asc: true, + }, + Filters: []*user.KeysSearchFilter{ + onlySinceTestStartFilter, + }, + }, + }, + want: &user.ListKeysResponse{ + Result: []*user.Key{ + otherOrgDataPointExpiringSoon, + otherOrgDataPointExpiringLate, + }, + Pagination: &filter.PaginationResponse{ + TotalResult: 4, + AppliedLimit: 2, + }, + }, + }, + { + name: "empty list", + args: args{ + UserCTX, + &user.ListKeysRequest{ + Filters: []*user.KeysSearchFilter{ + { + Filter: &user.KeysSearchFilter_KeyIdFilter{ + KeyIdFilter: &filter.IDFilter{Id: otherUserDataPoint.Id}, + }, + }, + }, + }, + }, + want: &user.ListKeysResponse{ + Result: []*user.Key{}, + Pagination: &filter.PaginationResponse{ + TotalResult: 0, + AppliedLimit: 100, + }, + }, + }, + } + t.Run("with permission flag v2", func(t *testing.T) { + setPermissionCheckV2Flag(t, true) + defer setPermissionCheckV2Flag(t, false) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.ListKeys(tt.args.ctx, tt.args.req) + require.NoError(t, err) + assert.Len(t, got.Result, len(tt.want.Result)) + if diff := cmp.Diff(tt.want, got, protocmp.Transform()); diff != "" { + t.Errorf("ListKeys() mismatch (-want +got):\n%s", diff) + } + }) + } + }) + t.Run("without permission flag v2", func(t *testing.T) { + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.ListKeys(tt.args.ctx, tt.args.req) + require.NoError(t, err) + assert.Len(t, got.Result, len(tt.want.Result)) + // ignore the total result, as this is a known bug with the in-memory permission checks. + // The command can't know how many keys exist in the system if the SQL statement has a limit. + // This is fixed, once the in-memory permission checks are removed with https://github.com/zitadel/zitadel/issues/9188 + tt.want.Pagination.TotalResult = got.Pagination.TotalResult + if diff := cmp.Diff(tt.want, got, protocmp.Transform()); diff != "" { + t.Errorf("ListKeys() mismatch (-want +got):\n%s", diff) + } + }) + } + }) +} + +func setupKeyDataPoint(t *testing.T, userId, orgId string, expirationDate time.Time) *user.Key { + expirationDatePb := timestamppb.New(expirationDate) + newKey, err := Client.AddKey(SystemCTX, &user.AddKeyRequest{ + UserId: userId, + ExpirationDate: expirationDatePb, + PublicKey: nil, + }) + require.NoError(t, err) + return &user.Key{ + CreationDate: newKey.CreationDate, + ChangeDate: newKey.CreationDate, + Id: newKey.GetKeyId(), + UserId: userId, + OrganizationId: orgId, + ExpirationDate: expirationDatePb, + } +} + +func awaitKeys(t *testing.T, sinceTestStartFilter *user.KeysSearchFilter, keyIds ...string) { + sortingColumn := user.KeyFieldName_KEY_FIELD_NAME_ID + slices.Sort(keyIds) + require.EventuallyWithT(t, func(collect *assert.CollectT) { + result, err := Client.ListKeys(SystemCTX, &user.ListKeysRequest{ + Filters: []*user.KeysSearchFilter{sinceTestStartFilter}, + SortingColumn: &sortingColumn, + Pagination: &filter.PaginationRequest{ + Asc: true, + }, + }) + require.NoError(t, err) + if !assert.Len(collect, result.Result, len(keyIds)) { + return + } + for i := range keyIds { + keyId := keyIds[i] + require.Equal(collect, keyId, result.Result[i].GetId()) + } + }, 5*time.Second, time.Second, "key not created in time") +} diff --git a/internal/api/grpc/user/v2/integration_test/metadata_test.go b/internal/api/grpc/user/v2/integration_test/metadata_test.go new file mode 100644 index 0000000000..c1e3bced0a --- /dev/null +++ b/internal/api/grpc/user/v2/integration_test/metadata_test.go @@ -0,0 +1,403 @@ +//go:build integration + +package user_test + +import ( + "context" + "encoding/base64" + "testing" + "time" + + "github.com/brianvoe/gofakeit/v6" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/internal/integration" + "github.com/zitadel/zitadel/pkg/grpc/filter/v2" + metadata "github.com/zitadel/zitadel/pkg/grpc/metadata/v2" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +func TestServer_SetUserMetadata(t *testing.T) { + iamOwnerCTX := Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + + tests := []struct { + name string + ctx context.Context + dep func(request *user.SetUserMetadataRequest) + req *user.SetUserMetadataRequest + setDate bool + wantErr bool + }{ + { + name: "missing permission", + ctx: Instance.WithAuthorization(context.Background(), integration.UserTypeNoPermission), + dep: func(req *user.SetUserMetadataRequest) { + req.UserId = Instance.CreateUserTypeHuman(CTX, gofakeit.Email()).GetId() + }, + req: &user.SetUserMetadataRequest{ + Metadata: []*user.Metadata{{Key: "key1", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value1")))}}, + }, + wantErr: true, + }, + { + name: "set user metadata", + ctx: iamOwnerCTX, + dep: func(req *user.SetUserMetadataRequest) { + req.UserId = Instance.CreateUserTypeHuman(CTX, gofakeit.Email()).GetId() + }, + req: &user.SetUserMetadataRequest{ + Metadata: []*user.Metadata{{Key: "key1", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value1")))}}, + }, + setDate: true, + }, + { + name: "set user metadata, multiple", + ctx: iamOwnerCTX, + dep: func(req *user.SetUserMetadataRequest) { + req.UserId = Instance.CreateUserTypeHuman(CTX, gofakeit.Email()).GetId() + }, + req: &user.SetUserMetadataRequest{ + Metadata: []*user.Metadata{ + {Key: "key1", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value1")))}, + {Key: "key2", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value2")))}, + {Key: "key3", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value3")))}, + }, + }, + setDate: true, + }, + { + name: "set user metadata on non existent user", + ctx: iamOwnerCTX, + req: &user.SetUserMetadataRequest{ + UserId: "notexisting", + Metadata: []*user.Metadata{{Key: "key1", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value1")))}}, + }, + wantErr: true, + }, + { + name: "update user metadata", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + dep: func(req *user.SetUserMetadataRequest) { + req.UserId = Instance.CreateUserTypeHuman(iamOwnerCTX, gofakeit.Email()).GetId() + Instance.SetUserMetadata(iamOwnerCTX, req.UserId, "key1", "value1") + }, + req: &user.SetUserMetadataRequest{ + Metadata: []*user.Metadata{{Key: "key1", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value2")))}}, + }, + setDate: true, + }, + { + name: "update user metadata with same value", + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + dep: func(req *user.SetUserMetadataRequest) { + req.UserId = Instance.CreateUserTypeHuman(iamOwnerCTX, gofakeit.Email()).GetId() + Instance.SetUserMetadata(iamOwnerCTX, req.UserId, "key1", "value1") + }, + req: &user.SetUserMetadataRequest{ + Metadata: []*user.Metadata{{Key: "key1", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value1")))}}, + }, + setDate: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creationDate := time.Now().UTC() + if tt.dep != nil { + tt.dep(tt.req) + } + got, err := Client.SetUserMetadata(tt.ctx, tt.req) + changeDate := time.Now().UTC() + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + + assertSetUserMetadataResponse(t, creationDate, changeDate, tt.setDate, got) + }) + } +} + +func assertSetUserMetadataResponse(t *testing.T, creationDate, changeDate time.Time, expectedSetDat bool, actualResp *user.SetUserMetadataResponse) { + if expectedSetDat { + if !changeDate.IsZero() { + assert.WithinRange(t, actualResp.GetSetDate().AsTime(), creationDate, changeDate) + } else { + assert.WithinRange(t, actualResp.GetSetDate().AsTime(), creationDate, time.Now().UTC()) + } + } else { + assert.Nil(t, actualResp.SetDate) + } +} + +func TestServer_ListUserMetadata(t *testing.T) { + iamOwnerCTX := Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + + type args struct { + ctx context.Context + dep func(context.Context, *user.ListUserMetadataRequest, *user.ListUserMetadataResponse) + req *user.ListUserMetadataRequest + } + + tests := []struct { + name string + args args + want *user.ListUserMetadataResponse + wantErr bool + }{ + { + name: "missing permission", + args: args{ + ctx: Instance.WithAuthorization(context.Background(), integration.UserTypeNoPermission), + dep: func(ctx context.Context, request *user.ListUserMetadataRequest, response *user.ListUserMetadataResponse) { + userID := Instance.CreateUserTypeHuman(iamOwnerCTX, gofakeit.Email()).GetId() + request.UserId = userID + Instance.SetUserMetadata(iamOwnerCTX, userID, "key1", "value1") + }, + req: &user.ListUserMetadataRequest{}, + }, + wantErr: true, + }, + { + name: "list request", + args: args{ + ctx: iamOwnerCTX, + dep: func(ctx context.Context, request *user.ListUserMetadataRequest, response *user.ListUserMetadataResponse) { + userID := Instance.CreateUserTypeHuman(iamOwnerCTX, gofakeit.Email()).GetId() + request.UserId = userID + metadataResp := Instance.SetUserMetadata(iamOwnerCTX, userID, "key1", "value1") + + response.Metadata[0] = &metadata.Metadata{ + CreationDate: metadataResp.GetSetDate(), + ChangeDate: metadataResp.GetSetDate(), + Key: "key1", + Value: []byte(base64.StdEncoding.EncodeToString([]byte("value1"))), + } + }, + req: &user.ListUserMetadataRequest{}, + }, + want: &user.ListUserMetadataResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Metadata: []*metadata.Metadata{ + {}, + }, + }, + }, + { + name: "list request single key", + args: args{ + ctx: iamOwnerCTX, + dep: func(ctx context.Context, request *user.ListUserMetadataRequest, response *user.ListUserMetadataResponse) { + userID := Instance.CreateUserTypeHuman(iamOwnerCTX, gofakeit.Email()).GetId() + request.UserId = userID + key := "key1" + response.Metadata[0] = setUserMetadata(iamOwnerCTX, userID, key, "value1") + Instance.SetUserMetadata(iamOwnerCTX, userID, "key2", "value2") + Instance.SetUserMetadata(iamOwnerCTX, userID, "key3", "value3") + request.Filters[0] = &metadata.MetadataSearchFilter{ + Filter: &metadata.MetadataSearchFilter_KeyFilter{KeyFilter: &metadata.MetadataKeyFilter{Key: key}}, + } + }, + req: &user.ListUserMetadataRequest{ + Filters: []*metadata.MetadataSearchFilter{{}}, + }, + }, + want: &user.ListUserMetadataResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Metadata: []*metadata.Metadata{ + {}, + }, + }, + }, + { + name: "list multiple keys", + args: args{ + ctx: iamOwnerCTX, + dep: func(ctx context.Context, request *user.ListUserMetadataRequest, response *user.ListUserMetadataResponse) { + userID := Instance.CreateUserTypeHuman(iamOwnerCTX, gofakeit.Email()).GetId() + request.UserId = userID + + response.Metadata[2] = setUserMetadata(iamOwnerCTX, userID, "key1", "value1") + response.Metadata[1] = setUserMetadata(iamOwnerCTX, userID, "key2", "value2") + response.Metadata[0] = setUserMetadata(iamOwnerCTX, userID, "key3", "value3") + }, + req: &user.ListUserMetadataRequest{}, + }, + want: &user.ListUserMetadataResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + Metadata: []*metadata.Metadata{ + {}, {}, {}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.args.dep != nil { + tt.args.dep(tt.args.ctx, tt.args.req, tt.want) + } + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(iamOwnerCTX, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, listErr := Instance.Client.UserV2.ListUserMetadata(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(ttt, listErr) + return + } + require.NoError(ttt, listErr) + // always first check length, otherwise its failed anyway + if assert.Len(ttt, got.Metadata, len(tt.want.Metadata)) { + assert.EqualExportedValues(ttt, got.Metadata, tt.want.Metadata) + } + assertPaginationResponse(ttt, tt.want.Pagination, got.Pagination) + }, retryDuration, tick, "timeout waiting for expected execution result") + }) + } +} + +func setUserMetadata(ctx context.Context, userID, key, value string) *metadata.Metadata { + metadataResp := Instance.SetUserMetadata(ctx, userID, key, value) + return &metadata.Metadata{ + CreationDate: metadataResp.GetSetDate(), + ChangeDate: metadataResp.GetSetDate(), + Key: key, + Value: []byte(base64.StdEncoding.EncodeToString([]byte(value))), + } +} + +func assertPaginationResponse(t *assert.CollectT, expected *filter.PaginationResponse, actual *filter.PaginationResponse) { + assert.Equal(t, expected.AppliedLimit, actual.AppliedLimit) + assert.Equal(t, expected.TotalResult, actual.TotalResult) +} + +func TestServer_DeleteUserMetadata(t *testing.T) { + iamOwnerCTX := Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + + tests := []struct { + name string + ctx context.Context + prepare func(request *user.DeleteUserMetadataRequest) (time.Time, time.Time) + req *user.DeleteUserMetadataRequest + wantDeletionDate bool + wantErr bool + }{ + { + name: "empty id", + ctx: iamOwnerCTX, + req: &user.DeleteUserMetadataRequest{ + UserId: "", + }, + wantErr: true, + }, + { + name: "delete, user not existing", + ctx: iamOwnerCTX, + req: &user.DeleteUserMetadataRequest{ + UserId: "notexisting", + }, + wantErr: true, + }, + { + name: "delete", + ctx: iamOwnerCTX, + prepare: func(request *user.DeleteUserMetadataRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + userID := Instance.CreateUserTypeHuman(iamOwnerCTX, gofakeit.Email()).GetId() + request.UserId = userID + key := "key1" + Instance.SetUserMetadata(iamOwnerCTX, userID, key, "value1") + request.Keys = []string{key} + return creationDate, time.Time{} + }, + req: &user.DeleteUserMetadataRequest{}, + wantDeletionDate: true, + }, + { + name: "delete, empty list", + ctx: iamOwnerCTX, + prepare: func(request *user.DeleteUserMetadataRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + userID := Instance.CreateUserTypeHuman(iamOwnerCTX, gofakeit.Email()).GetId() + request.UserId = userID + key := "key1" + Instance.SetUserMetadata(iamOwnerCTX, userID, key, "value1") + Instance.DeleteUserMetadata(iamOwnerCTX, userID, key) + return creationDate, time.Now().UTC() + }, + req: &user.DeleteUserMetadataRequest{}, + wantErr: true, + }, + { + name: "delete, already removed", + ctx: iamOwnerCTX, + prepare: func(request *user.DeleteUserMetadataRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + userID := Instance.CreateUserTypeHuman(iamOwnerCTX, gofakeit.Email()).GetId() + request.UserId = userID + key := "key1" + Instance.SetUserMetadata(iamOwnerCTX, userID, key, "value1") + Instance.DeleteUserMetadata(iamOwnerCTX, userID, key) + request.Keys = []string{key} + return creationDate, time.Now().UTC() + }, + req: &user.DeleteUserMetadataRequest{}, + wantErr: true, + }, + { + name: "delete, multiple", + ctx: iamOwnerCTX, + prepare: func(request *user.DeleteUserMetadataRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + userID := Instance.CreateUserTypeHuman(iamOwnerCTX, gofakeit.Email()).GetId() + request.UserId = userID + key1 := "key1" + Instance.SetUserMetadata(iamOwnerCTX, userID, key1, "value1") + key2 := "key2" + Instance.SetUserMetadata(iamOwnerCTX, userID, key2, "value1") + key3 := "key3" + Instance.SetUserMetadata(iamOwnerCTX, userID, key3, "value1") + request.Keys = []string{key1, key2, key3} + return creationDate, time.Time{} + }, + req: &user.DeleteUserMetadataRequest{}, + wantDeletionDate: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var creationDate, deletionDate time.Time + if tt.prepare != nil { + creationDate, deletionDate = tt.prepare(tt.req) + } + got, err := Instance.Client.UserV2.DeleteUserMetadata(tt.ctx, tt.req) + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertDeleteProjectResponse(t, creationDate, deletionDate, tt.wantDeletionDate, got) + }) + } +} + +func assertDeleteProjectResponse(t *testing.T, creationDate, deletionDate time.Time, expectedDeletionDate bool, actualResp *user.DeleteUserMetadataResponse) { + if expectedDeletionDate { + if !deletionDate.IsZero() { + assert.WithinRange(t, actualResp.GetDeletionDate().AsTime(), creationDate, deletionDate) + } else { + assert.WithinRange(t, actualResp.GetDeletionDate().AsTime(), creationDate, time.Now().UTC()) + } + } else { + assert.Nil(t, actualResp.DeletionDate) + } +} diff --git a/internal/api/grpc/user/v2/integration_test/password_test.go b/internal/api/grpc/user/v2/integration_test/password_test.go index 0cd0da7454..258cdaf78d 100644 --- a/internal/api/grpc/user/v2/integration_test/password_test.go +++ b/internal/api/grpc/user/v2/integration_test/password_test.go @@ -104,7 +104,7 @@ func TestServer_RequestPasswordReset(t *testing.T) { } } -func TestServer_SetPassword(t *testing.T) { +func TestServer_Deprecated_SetPassword(t *testing.T) { type args struct { ctx context.Context req *user.SetPasswordRequest diff --git a/internal/api/grpc/user/v2/integration_test/pat_test.go b/internal/api/grpc/user/v2/integration_test/pat_test.go new file mode 100644 index 0000000000..8ca6d80139 --- /dev/null +++ b/internal/api/grpc/user/v2/integration_test/pat_test.go @@ -0,0 +1,615 @@ +//go:build integration + +package user_test + +import ( + "context" + "fmt" + "slices" + "testing" + "time" + + "github.com/brianvoe/gofakeit/v6" + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/testing/protocmp" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/integration" + "github.com/zitadel/zitadel/pkg/grpc/filter/v2" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +func TestServer_AddPersonalAccessToken(t *testing.T) { + resp := Instance.CreateUserTypeMachine(IamCTX, Instance.DefaultOrg.Id) + userId := resp.GetId() + expirationDate := timestamppb.New(time.Now().Add(time.Hour * 24)) + type args struct { + req *user.AddPersonalAccessTokenRequest + prepare func(request *user.AddPersonalAccessTokenRequest) error + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "add pat, user not existing", + args: args{ + &user.AddPersonalAccessTokenRequest{ + UserId: "notexisting", + ExpirationDate: expirationDate, + }, + func(request *user.AddPersonalAccessTokenRequest) error { return nil }, + }, + wantErr: true, + }, + { + name: "add pat, ok", + args: args{ + &user.AddPersonalAccessTokenRequest{ + ExpirationDate: expirationDate, + }, + func(request *user.AddPersonalAccessTokenRequest) error { + request.UserId = userId + return nil + }, + }, + }, + { + name: "add pat human, not ok", + args: args{ + &user.AddPersonalAccessTokenRequest{ + ExpirationDate: expirationDate, + }, + func(request *user.AddPersonalAccessTokenRequest) error { + resp := Instance.CreateUserTypeHuman(IamCTX, gofakeit.Email()) + request.UserId = resp.Id + return nil + }, + }, + wantErr: true, + }, + { + name: "add another pat, ok", + args: args{ + &user.AddPersonalAccessTokenRequest{ + ExpirationDate: expirationDate, + }, + func(request *user.AddPersonalAccessTokenRequest) error { + request.UserId = userId + _, err := Client.AddPersonalAccessToken(IamCTX, &user.AddPersonalAccessTokenRequest{ + ExpirationDate: expirationDate, + UserId: userId, + }) + return err + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + err := tt.args.prepare(tt.args.req) + require.NoError(t, err) + got, err := Client.AddPersonalAccessToken(CTX, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.NotEmpty(t, got.TokenId, "id is empty") + assert.NotEmpty(t, got.Token, "token is empty") + creationDate := got.CreationDate.AsTime() + assert.Greater(t, creationDate, now, "creation date is before the test started") + assert.Less(t, creationDate, time.Now(), "creation date is in the future") + }) + } +} + +func TestServer_AddPersonalAccessToken_Permission(t *testing.T) { + OrgCTX := CTX + otherOrg := Instance.CreateOrganization(IamCTX, fmt.Sprintf("AddPersonalAccessToken-%s", gofakeit.AppName()), gofakeit.Email()) + otherOrgUser, err := Client.CreateUser(IamCTX, &user.CreateUserRequest{ + OrganizationId: otherOrg.OrganizationId, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: gofakeit.Name(), + }, + }, + }) + require.NoError(t, err) + request := &user.AddPersonalAccessTokenRequest{ + ExpirationDate: timestamppb.New(time.Now().Add(time.Hour * 24)), + UserId: otherOrgUser.GetId(), + } + type args struct { + ctx context.Context + req *user.AddPersonalAccessTokenRequest + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "system, ok", + args: args{SystemCTX, request}, + }, + { + name: "instance, ok", + args: args{IamCTX, request}, + }, + { + name: "org, error", + args: args{OrgCTX, request}, + wantErr: true, + }, + { + name: "user, error", + args: args{UserCTX, request}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + require.NoError(t, err) + got, err := Client.AddPersonalAccessToken(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.NotEmpty(t, got.TokenId, "id is empty") + assert.NotEmpty(t, got.Token, "token is empty") + creationDate := got.CreationDate.AsTime() + assert.Greater(t, creationDate, now, "creation date is before the test started") + assert.Less(t, creationDate, time.Now(), "creation date is in the future") + }) + } +} + +func TestServer_RemovePersonalAccessToken(t *testing.T) { + resp := Instance.CreateUserTypeMachine(IamCTX, Instance.DefaultOrg.Id) + userId := resp.GetId() + expirationDate := timestamppb.New(time.Now().Add(time.Hour * 24)) + type args struct { + req *user.RemovePersonalAccessTokenRequest + prepare func(request *user.RemovePersonalAccessTokenRequest) error + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "remove pat, user not existing", + args: args{ + &user.RemovePersonalAccessTokenRequest{ + UserId: "notexisting", + }, + func(request *user.RemovePersonalAccessTokenRequest) error { + pat, err := Client.AddPersonalAccessToken(CTX, &user.AddPersonalAccessTokenRequest{ + ExpirationDate: expirationDate, + UserId: userId, + }) + request.TokenId = pat.GetTokenId() + return err + }, + }, + wantErr: true, + }, + { + name: "remove pat, not existing", + args: args{ + &user.RemovePersonalAccessTokenRequest{ + TokenId: "notexisting", + }, + func(request *user.RemovePersonalAccessTokenRequest) error { + request.UserId = userId + return nil + }, + }, + wantErr: true, + }, + { + name: "remove pat, ok", + args: args{ + &user.RemovePersonalAccessTokenRequest{}, + func(request *user.RemovePersonalAccessTokenRequest) error { + pat, err := Client.AddPersonalAccessToken(CTX, &user.AddPersonalAccessTokenRequest{ + ExpirationDate: expirationDate, + UserId: userId, + }) + request.TokenId = pat.GetTokenId() + request.UserId = userId + return err + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + err := tt.args.prepare(tt.args.req) + require.NoError(t, err) + got, err := Client.RemovePersonalAccessToken(CTX, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + deletionDate := got.DeletionDate.AsTime() + assert.Greater(t, deletionDate, now, "creation date is before the test started") + assert.Less(t, deletionDate, time.Now(), "creation date is in the future") + }) + } +} + +func TestServer_RemovePersonalAccessToken_Permission(t *testing.T) { + otherOrg := Instance.CreateOrganization(IamCTX, fmt.Sprintf("RemovePersonalAccessToken-%s", gofakeit.AppName()), gofakeit.Email()) + otherOrgUser, err := Client.CreateUser(IamCTX, &user.CreateUserRequest{ + OrganizationId: otherOrg.OrganizationId, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: gofakeit.Name(), + }, + }, + }) + request := &user.RemovePersonalAccessTokenRequest{ + UserId: otherOrgUser.GetId(), + } + prepare := func(request *user.RemovePersonalAccessTokenRequest) error { + pat, err := Client.AddPersonalAccessToken(IamCTX, &user.AddPersonalAccessTokenRequest{ + ExpirationDate: timestamppb.New(time.Now().Add(time.Hour * 24)), + UserId: otherOrgUser.GetId(), + }) + request.TokenId = pat.GetTokenId() + return err + } + require.NoError(t, err) + type args struct { + ctx context.Context + req *user.RemovePersonalAccessTokenRequest + prepare func(request *user.RemovePersonalAccessTokenRequest) error + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "system, ok", + args: args{SystemCTX, request, prepare}, + }, + { + name: "instance, ok", + args: args{IamCTX, request, prepare}, + }, + { + name: "org, error", + args: args{CTX, request, prepare}, + wantErr: true, + }, + { + name: "user, error", + args: args{UserCTX, request, prepare}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + require.NoError(t, tt.args.prepare(tt.args.req)) + got, err := Client.RemovePersonalAccessToken(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.NotEmpty(t, got.DeletionDate, "client pat is empty") + creationDate := got.DeletionDate.AsTime() + assert.Greater(t, creationDate, now, "creation date is before the test started") + assert.Less(t, creationDate, time.Now(), "creation date is in the future") + }) + } +} + +func TestServer_ListPersonalAccessTokens(t *testing.T) { + type args struct { + ctx context.Context + req *user.ListPersonalAccessTokensRequest + } + type testCase struct { + name string + args args + want *user.ListPersonalAccessTokensResponse + } + OrgCTX := CTX + otherOrg := Instance.CreateOrganization(SystemCTX, fmt.Sprintf("ListPersonalAccessTokens-%s", gofakeit.AppName()), gofakeit.Email()) + otherOrgUser, err := Client.CreateUser(SystemCTX, &user.CreateUserRequest{ + OrganizationId: otherOrg.OrganizationId, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: gofakeit.Name(), + }, + }, + }) + require.NoError(t, err) + otherOrgUserId := otherOrgUser.GetId() + otherUserId := Instance.CreateUserTypeMachine(SystemCTX, Instance.DefaultOrg.Id).GetId() + onlySinceTestStartFilter := &user.PersonalAccessTokensSearchFilter{Filter: &user.PersonalAccessTokensSearchFilter_CreatedDateFilter{CreatedDateFilter: &filter.TimestampFilter{ + Timestamp: timestamppb.Now(), + Method: filter.TimestampFilterMethod_TIMESTAMP_FILTER_METHOD_AFTER_OR_EQUALS, + }}} + myOrgId := Instance.DefaultOrg.GetId() + myUserId := Instance.Users.Get(integration.UserTypeNoPermission).ID + expiresInADay := time.Now().Truncate(time.Hour).Add(time.Hour * 24) + myDataPoint := setupPATDataPoint(t, myUserId, myOrgId, expiresInADay) + otherUserDataPoint := setupPATDataPoint(t, otherUserId, myOrgId, expiresInADay) + otherOrgDataPointExpiringSoon := setupPATDataPoint(t, otherOrgUserId, otherOrg.OrganizationId, time.Now().Truncate(time.Hour).Add(time.Hour)) + otherOrgDataPointExpiringLate := setupPATDataPoint(t, otherOrgUserId, otherOrg.OrganizationId, expiresInADay.Add(time.Hour*24*30)) + sortingColumnExpirationDate := user.PersonalAccessTokenFieldName_PERSONAL_ACCESS_TOKEN_FIELD_NAME_EXPIRATION_DATE + awaitPersonalAccessTokens(t, + onlySinceTestStartFilter, + otherOrgDataPointExpiringSoon.GetId(), + otherOrgDataPointExpiringLate.GetId(), + otherUserDataPoint.GetId(), + myDataPoint.GetId(), + ) + tests := []testCase{ + { + name: "list all, instance", + args: args{ + IamCTX, + &user.ListPersonalAccessTokensRequest{ + Filters: []*user.PersonalAccessTokensSearchFilter{onlySinceTestStartFilter}, + }, + }, + want: &user.ListPersonalAccessTokensResponse{ + Result: []*user.PersonalAccessToken{ + otherOrgDataPointExpiringLate, + otherOrgDataPointExpiringSoon, + otherUserDataPoint, + myDataPoint, + }, + Pagination: &filter.PaginationResponse{ + TotalResult: 4, + AppliedLimit: 100, + }, + }, + }, + { + name: "list all, org", + args: args{ + OrgCTX, + &user.ListPersonalAccessTokensRequest{ + Filters: []*user.PersonalAccessTokensSearchFilter{onlySinceTestStartFilter}, + }, + }, + want: &user.ListPersonalAccessTokensResponse{ + Result: []*user.PersonalAccessToken{ + otherUserDataPoint, + myDataPoint, + }, + Pagination: &filter.PaginationResponse{ + TotalResult: 2, + AppliedLimit: 100, + }, + }, + }, + { + name: "list all, user", + args: args{ + UserCTX, + &user.ListPersonalAccessTokensRequest{ + Filters: []*user.PersonalAccessTokensSearchFilter{onlySinceTestStartFilter}, + }, + }, + want: &user.ListPersonalAccessTokensResponse{ + Result: []*user.PersonalAccessToken{ + myDataPoint, + }, + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + }, + }, + { + name: "list by id", + args: args{ + IamCTX, + &user.ListPersonalAccessTokensRequest{ + Filters: []*user.PersonalAccessTokensSearchFilter{ + onlySinceTestStartFilter, + { + Filter: &user.PersonalAccessTokensSearchFilter_TokenIdFilter{ + TokenIdFilter: &filter.IDFilter{Id: otherOrgDataPointExpiringSoon.Id}, + }, + }, + }, + }, + }, + want: &user.ListPersonalAccessTokensResponse{ + Result: []*user.PersonalAccessToken{ + otherOrgDataPointExpiringSoon, + }, + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + }, + }, + { + name: "list all from other org", + args: args{ + IamCTX, + &user.ListPersonalAccessTokensRequest{ + Filters: []*user.PersonalAccessTokensSearchFilter{ + onlySinceTestStartFilter, + { + Filter: &user.PersonalAccessTokensSearchFilter_OrganizationIdFilter{ + OrganizationIdFilter: &filter.IDFilter{Id: otherOrg.OrganizationId}, + }, + }}, + }, + }, + want: &user.ListPersonalAccessTokensResponse{ + Result: []*user.PersonalAccessToken{ + otherOrgDataPointExpiringLate, + otherOrgDataPointExpiringSoon, + }, + Pagination: &filter.PaginationResponse{ + TotalResult: 2, + AppliedLimit: 100, + }, + }, + }, + { + name: "sort by next expiration dates", + args: args{ + IamCTX, + &user.ListPersonalAccessTokensRequest{ + Pagination: &filter.PaginationRequest{ + Asc: true, + }, + SortingColumn: &sortingColumnExpirationDate, + Filters: []*user.PersonalAccessTokensSearchFilter{ + onlySinceTestStartFilter, + {Filter: &user.PersonalAccessTokensSearchFilter_OrganizationIdFilter{OrganizationIdFilter: &filter.IDFilter{Id: otherOrg.OrganizationId}}}, + }, + }, + }, + want: &user.ListPersonalAccessTokensResponse{ + Result: []*user.PersonalAccessToken{ + otherOrgDataPointExpiringSoon, + otherOrgDataPointExpiringLate, + }, + Pagination: &filter.PaginationResponse{ + TotalResult: 2, + AppliedLimit: 100, + }, + }, + }, + { + name: "get page", + args: args{ + IamCTX, + &user.ListPersonalAccessTokensRequest{ + Pagination: &filter.PaginationRequest{ + Offset: 2, + Limit: 2, + Asc: true, + }, + Filters: []*user.PersonalAccessTokensSearchFilter{ + onlySinceTestStartFilter, + }, + }, + }, + want: &user.ListPersonalAccessTokensResponse{ + Result: []*user.PersonalAccessToken{ + otherOrgDataPointExpiringSoon, + otherOrgDataPointExpiringLate, + }, + Pagination: &filter.PaginationResponse{ + TotalResult: 4, + AppliedLimit: 2, + }, + }, + }, + { + name: "empty list", + args: args{ + UserCTX, + &user.ListPersonalAccessTokensRequest{ + Filters: []*user.PersonalAccessTokensSearchFilter{ + { + Filter: &user.PersonalAccessTokensSearchFilter_TokenIdFilter{ + TokenIdFilter: &filter.IDFilter{Id: otherUserDataPoint.Id}, + }, + }, + }, + }, + }, + want: &user.ListPersonalAccessTokensResponse{ + Result: []*user.PersonalAccessToken{}, + Pagination: &filter.PaginationResponse{ + TotalResult: 0, + AppliedLimit: 100, + }, + }, + }, + } + t.Run("with permission flag v2", func(t *testing.T) { + setPermissionCheckV2Flag(t, true) + defer setPermissionCheckV2Flag(t, false) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.ListPersonalAccessTokens(tt.args.ctx, tt.args.req) + require.NoError(t, err) + assert.Len(t, got.Result, len(tt.want.Result)) + if diff := cmp.Diff(tt.want, got, protocmp.Transform()); diff != "" { + t.Errorf("ListPersonalAccessTokens() mismatch (-want +got):\n%s", diff) + } + }) + } + }) + t.Run("without permission flag v2", func(t *testing.T) { + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Client.ListPersonalAccessTokens(tt.args.ctx, tt.args.req) + require.NoError(t, err) + assert.Len(t, got.Result, len(tt.want.Result)) + // ignore the total result, as this is a known bug with the in-memory permission checks. + // The command can't know how many keys exist in the system if the SQL statement has a limit. + // This is fixed, once the in-memory permission checks are removed with https://github.com/zitadel/zitadel/issues/9188 + tt.want.Pagination.TotalResult = got.Pagination.TotalResult + if diff := cmp.Diff(tt.want, got, protocmp.Transform()); diff != "" { + t.Errorf("ListPersonalAccessTokens() mismatch (-want +got):\n%s", diff) + } + }) + } + }) +} + +func setupPATDataPoint(t *testing.T, userId, orgId string, expirationDate time.Time) *user.PersonalAccessToken { + expirationDatePb := timestamppb.New(expirationDate) + newPersonalAccessToken, err := Client.AddPersonalAccessToken(SystemCTX, &user.AddPersonalAccessTokenRequest{ + UserId: userId, + ExpirationDate: expirationDatePb, + }) + require.NoError(t, err) + return &user.PersonalAccessToken{ + CreationDate: newPersonalAccessToken.CreationDate, + ChangeDate: newPersonalAccessToken.CreationDate, + Id: newPersonalAccessToken.GetTokenId(), + UserId: userId, + OrganizationId: orgId, + ExpirationDate: expirationDatePb, + } +} + +func awaitPersonalAccessTokens(t *testing.T, sinceTestStartFilter *user.PersonalAccessTokensSearchFilter, patIds ...string) { + sortingColumn := user.PersonalAccessTokenFieldName_PERSONAL_ACCESS_TOKEN_FIELD_NAME_ID + slices.Sort(patIds) + require.EventuallyWithT(t, func(collect *assert.CollectT) { + result, err := Client.ListPersonalAccessTokens(SystemCTX, &user.ListPersonalAccessTokensRequest{ + Filters: []*user.PersonalAccessTokensSearchFilter{sinceTestStartFilter}, + SortingColumn: &sortingColumn, + Pagination: &filter.PaginationRequest{ + Asc: true, + }, + }) + require.NoError(t, err) + if !assert.Len(collect, result.Result, len(patIds)) { + return + } + for i := range patIds { + patId := patIds[i] + require.Equal(collect, patId, result.Result[i].GetId()) + } + }, 5*time.Second, time.Second, "pat not created in time") +} diff --git a/internal/api/grpc/user/v2/integration_test/phone_test.go b/internal/api/grpc/user/v2/integration_test/phone_test.go index 87a8260389..25227048f9 100644 --- a/internal/api/grpc/user/v2/integration_test/phone_test.go +++ b/internal/api/grpc/user/v2/integration_test/phone_test.go @@ -17,7 +17,7 @@ import ( "github.com/zitadel/zitadel/pkg/grpc/user/v2" ) -func TestServer_SetPhone(t *testing.T) { +func TestServer_Deprecated_SetPhone(t *testing.T) { userID := Instance.CreateHumanUser(CTX).GetUserId() tests := []struct { @@ -249,7 +249,7 @@ func TestServer_VerifyPhone(t *testing.T) { } } -func TestServer_RemovePhone(t *testing.T) { +func TestServer_Deprecated_RemovePhone(t *testing.T) { userResp := Instance.CreateHumanUser(CTX) failResp := Instance.CreateHumanUserNoPhone(CTX) otherUser := Instance.CreateHumanUser(CTX).GetUserId() diff --git a/internal/api/grpc/user/v2/integration_test/secret_test.go b/internal/api/grpc/user/v2/integration_test/secret_test.go new file mode 100644 index 0000000000..4296e8e599 --- /dev/null +++ b/internal/api/grpc/user/v2/integration_test/secret_test.go @@ -0,0 +1,347 @@ +//go:build integration + +package user_test + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/brianvoe/gofakeit/v6" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +func TestServer_AddSecret(t *testing.T) { + type args struct { + ctx context.Context + req *user.AddSecretRequest + prepare func(request *user.AddSecretRequest) error + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "add secret, user not existing", + args: args{ + CTX, + &user.AddSecretRequest{ + UserId: "notexisting", + }, + func(request *user.AddSecretRequest) error { return nil }, + }, + wantErr: true, + }, + { + name: "add secret, ok", + args: args{ + CTX, + &user.AddSecretRequest{}, + func(request *user.AddSecretRequest) error { + resp := Instance.CreateUserTypeMachine(CTX, Instance.DefaultOrg.Id) + request.UserId = resp.GetId() + return nil + }, + }, + }, + { + name: "add secret human, not ok", + args: args{ + CTX, + &user.AddSecretRequest{}, + func(request *user.AddSecretRequest) error { + resp := Instance.CreateUserTypeMachine(CTX, Instance.DefaultOrg.Id) + request.UserId = resp.GetId() + return nil + }, + }, + }, + { + name: "overwrite secret, ok", + args: args{ + CTX, + &user.AddSecretRequest{}, + func(request *user.AddSecretRequest) error { + resp := Instance.CreateUserTypeMachine(CTX, Instance.DefaultOrg.Id) + request.UserId = resp.GetId() + _, err := Client.AddSecret(CTX, &user.AddSecretRequest{ + UserId: resp.GetId(), + }) + return err + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + err := tt.args.prepare(tt.args.req) + require.NoError(t, err) + got, err := Client.AddSecret(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.NotEmpty(t, got.ClientSecret, "client secret is empty") + creationDate := got.CreationDate.AsTime() + assert.Greater(t, creationDate, now, "creation date is before the test started") + assert.Less(t, creationDate, time.Now(), "creation date is in the future") + }) + } +} + +func TestServer_AddSecret_Permission(t *testing.T) { + otherOrg := Instance.CreateOrganization(IamCTX, fmt.Sprintf("AddSecret-%s", gofakeit.AppName()), gofakeit.Email()) + otherOrgUser, err := Instance.Client.UserV2.CreateUser(IamCTX, &user.CreateUserRequest{ + OrganizationId: otherOrg.OrganizationId, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: gofakeit.Name(), + }, + }, + }) + require.NoError(t, err) + + type args struct { + ctx context.Context + req *user.AddSecretRequest + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "system, ok", + args: args{ + SystemCTX, + &user.AddSecretRequest{ + UserId: otherOrgUser.GetId(), + }, + }, + }, + { + name: "instance, ok", + args: args{ + IamCTX, + &user.AddSecretRequest{ + UserId: otherOrgUser.GetId(), + }, + }, + }, + { + name: "org, error", + args: args{ + CTX, + &user.AddSecretRequest{ + UserId: otherOrgUser.GetId(), + }, + }, + wantErr: true, + }, + { + name: "user, error", + args: args{ + UserCTX, + &user.AddSecretRequest{ + UserId: otherOrgUser.GetId(), + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + require.NoError(t, err) + got, err := Client.AddSecret(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.NotEmpty(t, got.ClientSecret, "client secret is empty") + creationDate := got.CreationDate.AsTime() + assert.Greater(t, creationDate, now, "creation date is before the test started") + assert.Less(t, creationDate, time.Now(), "creation date is in the future") + }) + } +} + +func TestServer_RemoveSecret(t *testing.T) { + type args struct { + ctx context.Context + req *user.RemoveSecretRequest + prepare func(request *user.RemoveSecretRequest) error + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "remove secret, user not existing", + args: args{ + CTX, + &user.RemoveSecretRequest{ + UserId: "notexisting", + }, + func(request *user.RemoveSecretRequest) error { return nil }, + }, + wantErr: true, + }, + { + name: "remove secret, not existing", + args: args{ + CTX, + &user.RemoveSecretRequest{}, + func(request *user.RemoveSecretRequest) error { + resp := Instance.CreateUserTypeMachine(CTX, Instance.DefaultOrg.Id) + request.UserId = resp.GetId() + return nil + }, + }, + wantErr: true, + }, + { + name: "remove secret, ok", + args: args{ + CTX, + &user.RemoveSecretRequest{}, + func(request *user.RemoveSecretRequest) error { + resp := Instance.CreateUserTypeMachine(CTX, Instance.DefaultOrg.Id) + request.UserId = resp.GetId() + _, err := Instance.Client.UserV2.AddSecret(CTX, &user.AddSecretRequest{ + UserId: resp.GetId(), + }) + return err + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + err := tt.args.prepare(tt.args.req) + require.NoError(t, err) + got, err := Client.RemoveSecret(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + deletionDate := got.DeletionDate.AsTime() + assert.Greater(t, deletionDate, now, "creation date is before the test started") + assert.Less(t, deletionDate, time.Now(), "creation date is in the future") + }) + } +} + +func TestServer_RemoveSecret_Permission(t *testing.T) { + otherOrg := Instance.CreateOrganization(IamCTX, fmt.Sprintf("RemoveSecret-%s", gofakeit.AppName()), gofakeit.Email()) + otherOrgUser, err := Instance.Client.UserV2.CreateUser(IamCTX, &user.CreateUserRequest{ + OrganizationId: otherOrg.OrganizationId, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: gofakeit.Name(), + }, + }, + }) + require.NoError(t, err) + + type args struct { + ctx context.Context + req *user.RemoveSecretRequest + prepare func(request *user.RemoveSecretRequest) error + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "system, ok", + args: args{ + SystemCTX, + &user.RemoveSecretRequest{ + UserId: otherOrgUser.GetId(), + }, + func(request *user.RemoveSecretRequest) error { + _, err := Instance.Client.UserV2.AddSecret(IamCTX, &user.AddSecretRequest{ + UserId: otherOrgUser.GetId(), + }) + return err + }, + }, + }, + { + name: "instance, ok", + args: args{ + IamCTX, + &user.RemoveSecretRequest{ + UserId: otherOrgUser.GetId(), + }, + func(request *user.RemoveSecretRequest) error { + _, err := Instance.Client.UserV2.AddSecret(IamCTX, &user.AddSecretRequest{ + UserId: otherOrgUser.GetId(), + }) + return err + }, + }, + }, + { + name: "org, error", + args: args{ + CTX, + &user.RemoveSecretRequest{ + UserId: otherOrgUser.GetId(), + }, + func(request *user.RemoveSecretRequest) error { + _, err := Instance.Client.UserV2.AddSecret(IamCTX, &user.AddSecretRequest{ + UserId: otherOrgUser.GetId(), + }) + return err + }, + }, + wantErr: true, + }, + { + name: "user, error", + args: args{ + UserCTX, + &user.RemoveSecretRequest{ + UserId: otherOrgUser.GetId(), + }, + func(request *user.RemoveSecretRequest) error { + _, err := Instance.Client.UserV2.AddSecret(IamCTX, &user.AddSecretRequest{ + UserId: otherOrgUser.GetId(), + }) + return err + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + require.NoError(t, tt.args.prepare(tt.args.req)) + got, err := Client.RemoveSecret(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.NotEmpty(t, got.DeletionDate, "client secret is empty") + creationDate := got.DeletionDate.AsTime() + assert.Greater(t, creationDate, now, "creation date is before the test started") + assert.Less(t, creationDate, time.Now(), "creation date is in the future") + }) + } +} diff --git a/internal/api/grpc/user/v2/integration_test/user_test.go b/internal/api/grpc/user/v2/integration_test/user_test.go index dfedd7a404..959dbeddab 100644 --- a/internal/api/grpc/user/v2/integration_test/user_test.go +++ b/internal/api/grpc/user/v2/integration_test/user_test.go @@ -4,6 +4,7 @@ package user_test import ( "context" + "encoding/base64" "fmt" "net/url" "os" @@ -59,7 +60,7 @@ func TestMain(m *testing.M) { }()) } -func TestServer_AddHumanUser(t *testing.T) { +func TestServer_Deprecated_AddHumanUser(t *testing.T) { idpResp := Instance.AddGenericOAuthProvider(IamCTX, Instance.DefaultOrg.Id) type args struct { ctx context.Context @@ -654,6 +655,7 @@ func TestServer_AddHumanUser(t *testing.T) { t.Run(tt.name, func(t *testing.T) { userID := fmt.Sprint(time.Now().UnixNano() + int64(i)) tt.args.req.UserId = &userID + // In order to prevent unique constraint errors, we set the email to a unique value if email := tt.args.req.GetEmail(); email != nil { email.Email = fmt.Sprintf("%s@me.now", userID) } @@ -668,7 +670,6 @@ func TestServer_AddHumanUser(t *testing.T) { return } require.NoError(t, err) - assert.Equal(t, tt.want.GetUserId(), got.GetUserId()) if tt.want.GetEmailCode() != "" { assert.NotEmpty(t, got.GetEmailCode()) @@ -685,7 +686,7 @@ func TestServer_AddHumanUser(t *testing.T) { } } -func TestServer_AddHumanUser_Permission(t *testing.T) { +func TestServer_Deprecated_AddHumanUser_Permission(t *testing.T) { newOrgOwnerEmail := gofakeit.Email() newOrg := Instance.CreateOrganization(IamCTX, fmt.Sprintf("AddHuman-%s", gofakeit.AppName()), newOrgOwnerEmail) type args struct { @@ -878,7 +879,7 @@ func TestServer_AddHumanUser_Permission(t *testing.T) { } } -func TestServer_UpdateHumanUser(t *testing.T) { +func TestServer_Deprecated_UpdateHumanUser(t *testing.T) { type args struct { ctx context.Context req *user.UpdateHumanUserRequest @@ -1239,7 +1240,7 @@ func TestServer_UpdateHumanUser(t *testing.T) { } } -func TestServer_UpdateHumanUser_Permission(t *testing.T) { +func TestServer_Deprecated_UpdateHumanUser_Permission(t *testing.T) { newOrgOwnerEmail := gofakeit.Email() newOrg := Instance.CreateOrganization(IamCTX, fmt.Sprintf("UpdateHuman-%s", gofakeit.AppName()), newOrgOwnerEmail) newUserID := newOrg.CreatedAdmins[0].GetUserId() @@ -1755,12 +1756,11 @@ func TestServer_ReactivateUser(t *testing.T) { } func TestServer_DeleteUser(t *testing.T) { - projectResp, err := Instance.CreateProject(CTX) - require.NoError(t, err) + projectResp := Instance.CreateProject(CTX, t, "", gofakeit.AppName(), false, false) + type args struct { - ctx context.Context req *user.DeleteUserRequest - prepare func(request *user.DeleteUserRequest) error + prepare func(*testing.T, *user.DeleteUserRequest) context.Context } tests := []struct { name string @@ -1771,23 +1771,21 @@ func TestServer_DeleteUser(t *testing.T) { { name: "remove, not existing", args: args{ - CTX, &user.DeleteUserRequest{ UserId: "notexisting", }, - func(request *user.DeleteUserRequest) error { return nil }, + func(*testing.T, *user.DeleteUserRequest) context.Context { return CTX }, }, wantErr: true, }, { name: "remove human, ok", args: args{ - ctx: CTX, req: &user.DeleteUserRequest{}, - prepare: func(request *user.DeleteUserRequest) error { + prepare: func(_ *testing.T, request *user.DeleteUserRequest) context.Context { resp := Instance.CreateHumanUser(CTX) request.UserId = resp.GetUserId() - return err + return CTX }, }, want: &user.DeleteUserResponse{ @@ -1800,12 +1798,11 @@ func TestServer_DeleteUser(t *testing.T) { { name: "remove machine, ok", args: args{ - ctx: CTX, req: &user.DeleteUserRequest{}, - prepare: func(request *user.DeleteUserRequest) error { + prepare: func(_ *testing.T, request *user.DeleteUserRequest) context.Context { resp := Instance.CreateMachineUser(CTX) request.UserId = resp.GetUserId() - return err + return CTX }, }, want: &user.DeleteUserResponse{ @@ -1818,15 +1815,48 @@ func TestServer_DeleteUser(t *testing.T) { { name: "remove dependencies, ok", args: args{ - ctx: CTX, req: &user.DeleteUserRequest{}, - prepare: func(request *user.DeleteUserRequest) error { + prepare: func(_ *testing.T, request *user.DeleteUserRequest) context.Context { resp := Instance.CreateHumanUser(CTX) request.UserId = resp.GetUserId() Instance.CreateProjectUserGrant(t, CTX, projectResp.GetId(), request.UserId) Instance.CreateProjectMembership(t, CTX, projectResp.GetId(), request.UserId) - Instance.CreateOrgMembership(t, CTX, request.UserId) - return err + Instance.CreateOrgMembership(t, CTX, Instance.DefaultOrg.Id, request.UserId) + return CTX + }, + }, + want: &user.DeleteUserResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Instance.DefaultOrg.Id, + }, + }, + }, + { + name: "remove self, ok", + args: args{ + req: &user.DeleteUserRequest{}, + prepare: func(t *testing.T, request *user.DeleteUserRequest) context.Context { + removeUser, err := Client.CreateUser(CTX, &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "givenName", + FamilyName: "familyName", + }, + Email: &user.SetHumanEmail{ + Email: gofakeit.Email(), + Verification: &user.SetHumanEmail_IsVerified{IsVerified: true}, + }, + }, + }, + }) + require.NoError(t, err) + request.UserId = removeUser.Id + Instance.RegisterUserPasskey(CTX, removeUser.Id) + _, token, _, _ := Instance.CreateVerifiedWebAuthNSession(t, LoginCTX, removeUser.Id) + return integration.WithAuthorizationToken(UserCTX, token) }, }, want: &user.DeleteUserResponse{ @@ -1839,10 +1869,8 @@ func TestServer_DeleteUser(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := tt.args.prepare(tt.args.req) - require.NoError(t, err) - - got, err := Client.DeleteUser(tt.args.ctx, tt.args.req) + ctx := tt.args.prepare(t, tt.args.req) + got, err := Client.DeleteUser(ctx, tt.args.req) if tt.wantErr { require.Error(t, err) return @@ -2032,7 +2060,7 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) { ChangeDate: timestamppb.Now(), ResourceOwner: Instance.ID(), }, - url: "http://" + Instance.Domain + ":8000/sso", + url: "http://localhost:8000/sso", parametersExisting: []string{"RelayState", "SAMLRequest"}, }, wantErr: false, @@ -2056,7 +2084,7 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) { ChangeDate: timestamppb.Now(), ResourceOwner: Instance.ID(), }, - url: "http://" + Instance.Domain + ":8000/sso", + url: "http://localhost:8000/sso", parametersExisting: []string{"RelayState", "SAMLRequest"}, }, wantErr: false, @@ -2080,7 +2108,33 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) { ChangeDate: timestamppb.Now(), ResourceOwner: Instance.ID(), }, - postForm: true, + url: "http://localhost:8000/sso", + parametersExisting: []string{"RelayState", "SAMLRequest"}, + postForm: true, + }, + wantErr: false, + }, + { + name: "next step jwt idp", + args: args{ + CTX, + &user.StartIdentityProviderIntentRequest{ + IdpId: jwtIdPID, + Content: &user.StartIdentityProviderIntentRequest_Urls{ + Urls: &user.RedirectURLs{ + SuccessUrl: "https://example.com/success", + FailureUrl: "https://example.com/failure", + }, + }, + }, + }, + want: want{ + details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Instance.ID(), + }, + url: "https://example.com/jwt", + parametersExisting: []string{"authRequestID", "userAgentID"}, }, wantErr: false, }, @@ -2118,9 +2172,11 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) { } require.NoError(t, err) - if tt.want.url != "" { + if tt.want.url != "" && !tt.want.postForm { authUrl, err := url.Parse(got.GetAuthUrl()) require.NoError(t, err) + + assert.Equal(t, tt.want.url, authUrl.Scheme+"://"+authUrl.Host+authUrl.Path) require.Len(t, authUrl.Query(), len(tt.want.parametersEqual)+len(tt.want.parametersExisting)) for _, existing := range tt.want.parametersExisting { @@ -2131,7 +2187,15 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) { } } if tt.want.postForm { - assert.NotEmpty(t, got.GetPostForm()) + assert.Equal(t, tt.want.url, got.GetFormData().GetUrl()) + + require.Len(t, got.GetFormData().GetFields(), len(tt.want.parametersEqual)+len(tt.want.parametersExisting)) + for _, existing := range tt.want.parametersExisting { + assert.Contains(t, got.GetFormData().GetFields(), existing) + } + for key, equal := range tt.want.parametersEqual { + assert.Equal(t, got.GetFormData().GetFields()[key], equal) + } } integration.AssertDetails(t, &user.StartIdentityProviderIntentResponse{ Details: tt.want.details, @@ -3596,7 +3660,6 @@ func TestServer_HumanMFAInitSkipped(t *testing.T) { t.Run(tt.name, func(t *testing.T) { err := tt.args.prepare(tt.args.req) require.NoError(t, err) - got, err := Client.HumanMFAInitSkipped(tt.args.ctx, tt.args.req) if tt.wantErr { require.Error(t, err) @@ -3610,3 +3673,1716 @@ func TestServer_HumanMFAInitSkipped(t *testing.T) { }) } } + +func TestServer_CreateUser(t *testing.T) { + type args struct { + ctx context.Context + req *user.CreateUserRequest + } + type testCase struct { + args args + want *user.CreateUserResponse + wantErr bool + } + tests := []struct { + name string + testCase func(runId string) testCase + }{ + { + name: "default verification", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: email, + }, + }, + }, + }, + }, + want: &user.CreateUserResponse{ + Id: "is generated", + }, + wantErr: false, + } + }, + }, + { + name: "return email verification code", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: email, + Verification: &user.SetHumanEmail_ReturnCode{ + ReturnCode: &user.ReturnEmailVerificationCode{}, + }, + }, + }, + }, + }, + }, + want: &user.CreateUserResponse{ + Id: "is generated", + EmailCode: gu.Ptr("something"), + }, + } + }, + }, + { + name: "custom template", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: email, + Verification: &user.SetHumanEmail_SendCode{ + SendCode: &user.SendEmailVerificationCode{ + UrlTemplate: gu.Ptr("https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}"), + }, + }, + }, + }, + }, + }, + }, + want: &user.CreateUserResponse{ + Id: "is generated", + }, + } + }, + }, + { + name: "return phone verification code", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: email, + }, + Phone: &user.SetHumanPhone{ + Phone: "+41791234567", + Verification: &user.SetHumanPhone_ReturnCode{ + ReturnCode: &user.ReturnPhoneVerificationCode{}, + }, + }, + }, + }, + }, + }, + want: &user.CreateUserResponse{ + Id: "is generated", + PhoneCode: gu.Ptr("something"), + }, + } + }, + }, + { + name: "custom template error", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: email, + Verification: &user.SetHumanEmail_SendCode{ + SendCode: &user.SendEmailVerificationCode{ + UrlTemplate: gu.Ptr("{{"), + }, + }, + }, + }, + }, + }, + }, + wantErr: true, + } + }, + }, + { + name: "missing REQUIRED profile", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Email: &user.SetHumanEmail{ + Email: email, + Verification: &user.SetHumanEmail_ReturnCode{ + ReturnCode: &user.ReturnEmailVerificationCode{}, + }, + }, + }, + }, + }, + }, + wantErr: true, + } + }, + }, + { + name: "missing REQUIRED email", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + }, + }, + }, + }, + wantErr: true, + } + }, + }, + { + name: "missing empty email", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{}, + }, + }, + }, + }, + wantErr: true, + } + }, + }, + { + name: "missing idp", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: email, + Verification: &user.SetHumanEmail_IsVerified{ + IsVerified: true, + }, + }, + IdpLinks: []*user.IDPLink{ + { + IdpId: "idpID", + UserId: "userID", + UserName: "username", + }, + }, + }, + }, + }, + }, + wantErr: true, + } + }, + }, + { + name: "with metadata", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: email, + Verification: &user.SetHumanEmail_IsVerified{ + IsVerified: true, + }, + }, + Metadata: []*user.Metadata{ + {Key: "key1", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value1")))}, + {Key: "key2", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value2")))}, + {Key: "key3", Value: []byte(base64.StdEncoding.EncodeToString([]byte("value3")))}, + }, + }, + }, + }, + }, + want: &user.CreateUserResponse{ + Id: "is generated", + }, + } + }, + }, + { + name: "with idp", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + idpResp := Instance.AddGenericOAuthProvider(IamCTX, Instance.DefaultOrg.Id) + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: email, + Verification: &user.SetHumanEmail_IsVerified{ + IsVerified: true, + }, + }, + IdpLinks: []*user.IDPLink{ + { + IdpId: idpResp.Id, + UserId: "userID", + UserName: "username", + }, + }, + }, + }, + }, + }, + want: &user.CreateUserResponse{ + Id: "is generated", + }, + } + }, + }, + { + name: "with totp", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: email, + Verification: &user.SetHumanEmail_IsVerified{ + IsVerified: true, + }, + }, + TotpSecret: gu.Ptr("secret"), + }, + }, + }, + }, + want: &user.CreateUserResponse{ + Id: "is generated", + }, + } + }, + }, + { + name: "password not complexity conform", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + NickName: gu.Ptr("Dukkie"), + DisplayName: gu.Ptr("Donald Duck"), + PreferredLanguage: gu.Ptr("en"), + Gender: user.Gender_GENDER_DIVERSE.Enum(), + }, + Email: &user.SetHumanEmail{ + Email: email, + }, + PasswordType: &user.CreateUserRequest_Human_Password{ + Password: &user.Password{ + Password: "insufficient", + }, + }, + }, + }, + }, + }, + wantErr: true, + } + }, + }, + { + name: "hashed password", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: email, + }, + PasswordType: &user.CreateUserRequest_Human_HashedPassword{ + HashedPassword: &user.HashedPassword{ + Hash: "$2y$12$hXUrnqdq1RIIYZ2HPytIIe5lXdIvbhqrTvdPsSF7o.jFh817Z6lwm", + }, + }, + }, + }, + }, + }, + want: &user.CreateUserResponse{ + Id: "is generated", + }, + } + }, + }, + { + name: "unsupported hashed password", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: email, + }, + PasswordType: &user.CreateUserRequest_Human_HashedPassword{ + HashedPassword: &user.HashedPassword{ + Hash: "$scrypt$ln=16,r=8,p=1$cmFuZG9tc2FsdGlzaGFyZA$Rh+NnJNo1I6nRwaNqbDm6kmADswD1+7FTKZ7Ln9D8nQ", + }, + }, + }, + }, + }, + }, + wantErr: true, + } + }, + }, + { + name: "human default username", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: email, + }, + }, + }, + }, + }, + want: &user.CreateUserResponse{ + Id: "is generated", + }, + } + }, + }, + { + name: "machine user", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: "donald", + }, + }, + }, + }, + want: &user.CreateUserResponse{ + Id: "is generated", + }, + } + }, + }, + { + name: "machine default username to generated id", + testCase: func(runId string) testCase { + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: "donald", + }, + }, + }, + }, + want: &user.CreateUserResponse{ + Id: "is generated", + }, + } + }, + }, + { + name: "machine default username to given id", + testCase: func(runId string) testCase { + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + UserId: &runId, + OrganizationId: Instance.DefaultOrg.Id, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: "donald", + }, + }, + }, + }, + want: &user.CreateUserResponse{ + Id: runId, + }, + } + }, + }, + { + name: "org does not exist human, error", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: "does not exist", + Username: &username, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: email, + }, + }, + }, + }, + }, + wantErr: true, + } + }, + }, + { + name: "org does not exist machine, error", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + return testCase{ + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: "does not exist", + Username: &username, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: gofakeit.Name(), + }, + }, + }, + }, + wantErr: true, + } + }, + }, + } + for i, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + runId := fmt.Sprint(now.UnixNano() + int64(i)) + test := tt.testCase(runId) + got, err := Client.CreateUser(test.args.ctx, test.args.req) + if test.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + creationDate := got.CreationDate.AsTime() + assert.Greater(t, creationDate, now, "creation date is before the test started") + assert.Less(t, creationDate, time.Now(), "creation date is in the future") + if test.want.GetEmailCode() != "" { + assert.NotEmpty(t, got.GetEmailCode(), "email code is empty") + } else { + assert.Empty(t, got.GetEmailCode(), "email code is not empty") + } + if test.want.GetPhoneCode() != "" { + assert.NotEmpty(t, got.GetPhoneCode(), "phone code is empty") + } else { + assert.Empty(t, got.GetPhoneCode(), "phone code is not empty") + } + if test.want.GetId() == "is generated" { + assert.Len(t, got.GetId(), 18, "ID is not 18 characters") + } else { + assert.Equal(t, test.want.GetId(), got.GetId(), "ID is not the same") + } + }) + } +} + +func TestServer_CreateUser_And_Compare(t *testing.T) { + type args struct { + ctx context.Context + req *user.CreateUserRequest + } + type testCase struct { + name string + args args + assert func(t *testing.T, createResponse *user.CreateUserResponse, getResponse *user.GetUserByIDResponse) + } + tests := []struct { + name string + testCase func(runId string) testCase + }{{ + name: "human given username", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + ctx: CTX, + req: &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: email, + }, + }, + }, + }, + }, + assert: func(t *testing.T, _ *user.CreateUserResponse, getResponse *user.GetUserByIDResponse) { + assert.Equal(t, username, getResponse.GetUser().GetUsername()) + }, + } + }, + }, { + name: "human username default to email", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + ctx: CTX, + req: &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: email, + }, + }, + }, + }, + }, + assert: func(t *testing.T, _ *user.CreateUserResponse, getResponse *user.GetUserByIDResponse) { + assert.Equal(t, email, getResponse.GetUser().GetUsername()) + }, + } + }, + }, { + name: "machine username given", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + return testCase{ + args: args{ + ctx: CTX, + req: &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + Username: &username, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: "donald", + }, + }, + }, + }, + assert: func(t *testing.T, _ *user.CreateUserResponse, getResponse *user.GetUserByIDResponse) { + assert.Equal(t, username, getResponse.GetUser().GetUsername()) + }, + } + }, + }, { + name: "machine username default to generated id", + testCase: func(runId string) testCase { + return testCase{ + args: args{ + ctx: CTX, + req: &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: "donald", + }, + }, + }, + }, + assert: func(t *testing.T, createResponse *user.CreateUserResponse, getResponse *user.GetUserByIDResponse) { + assert.Equal(t, createResponse.GetId(), getResponse.GetUser().GetUsername()) + }, + } + }, + }, { + name: "machine username default to given id", + testCase: func(runId string) testCase { + return testCase{ + args: args{ + ctx: CTX, + req: &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + UserId: &runId, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: "donald", + }, + }, + }, + }, + assert: func(t *testing.T, createResponse *user.CreateUserResponse, getResponse *user.GetUserByIDResponse) { + assert.Equal(t, runId, getResponse.GetUser().GetUsername()) + }, + } + }, + }} + for i, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + runId := fmt.Sprint(now.UnixNano() + int64(i)) + test := tt.testCase(runId) + createResponse, err := Client.CreateUser(test.args.ctx, test.args.req) + require.NoError(t, err) + Instance.TriggerUserByID(test.args.ctx, createResponse.GetId()) + getResponse, err := Client.GetUserByID(test.args.ctx, &user.GetUserByIDRequest{ + UserId: createResponse.GetId(), + }) + require.NoError(t, err) + test.assert(t, createResponse, getResponse) + }) + } +} + +func TestServer_CreateUser_Permission(t *testing.T) { + newOrgOwnerEmail := gofakeit.Email() + newOrg := Instance.CreateOrganization(IamCTX, fmt.Sprintf("AddHuman-%s", gofakeit.AppName()), newOrgOwnerEmail) + type args struct { + ctx context.Context + req *user.CreateUserRequest + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "human system, ok", + args: args{ + SystemCTX, + &user.CreateUserRequest{ + OrganizationId: newOrg.GetOrganizationId(), + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: "this is overwritten with a unique address", + }, + }, + }, + }, + }, + }, + { + name: "human instance, ok", + args: args{ + IamCTX, + &user.CreateUserRequest{ + OrganizationId: newOrg.GetOrganizationId(), + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: "this is overwritten with a unique address", + }, + }, + }, + }, + }, + }, + { + name: "human org, error", + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: newOrg.GetOrganizationId(), + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: "this is overwritten with a unique address", + }, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "human user, error", + args: args{ + UserCTX, + &user.CreateUserRequest{ + OrganizationId: newOrg.GetOrganizationId(), + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: "this is overwritten with a unique address", + }, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "machine system, ok", + args: args{ + SystemCTX, + &user.CreateUserRequest{ + OrganizationId: newOrg.GetOrganizationId(), + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: "donald", + }, + }, + }, + }, + }, + { + name: "machine instance, ok", + args: args{ + IamCTX, + &user.CreateUserRequest{ + OrganizationId: newOrg.GetOrganizationId(), + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: "donald", + }, + }, + }, + }, + }, + { + name: "machine org, error", + args: args{ + CTX, + &user.CreateUserRequest{ + OrganizationId: newOrg.GetOrganizationId(), + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: "donald", + }, + }, + }, + }, + wantErr: true, + }, + { + name: "machine user, error", + args: args{ + UserCTX, + &user.CreateUserRequest{ + OrganizationId: newOrg.GetOrganizationId(), + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: "donald", + }, + }, + }, + }, + wantErr: true, + }, + } + for i, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + userID := fmt.Sprint(time.Now().UnixNano() + int64(i)) + tt.args.req.UserId = &userID + if email := tt.args.req.GetHuman().GetEmail(); email != nil { + email.Email = fmt.Sprintf("%s@example.com", userID) + } + _, err := Client.CreateUser(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + }) + } +} + +func TestServer_UpdateUserTypeHuman(t *testing.T) { + type args struct { + ctx context.Context + req *user.UpdateUserRequest + } + type testCase struct { + args args + want *user.UpdateUserResponse + wantErr bool + } + tests := []struct { + name string + testCase func(runId, userId string) testCase + }{ + { + name: "default verification", + testCase: func(runId, userId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.UpdateUserRequest{ + UserId: userId, + UserType: &user.UpdateUserRequest_Human_{ + Human: &user.UpdateUserRequest_Human{ + Email: &user.SetHumanEmail{ + Email: email, + }, + }, + }, + }, + }, + want: &user.UpdateUserResponse{}, + wantErr: false, + } + }, + }, + { + name: "return email verification code", + testCase: func(runId, userId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.UpdateUserRequest{ + UserId: userId, + UserType: &user.UpdateUserRequest_Human_{ + Human: &user.UpdateUserRequest_Human{ + Email: &user.SetHumanEmail{ + Email: email, + Verification: &user.SetHumanEmail_ReturnCode{ + ReturnCode: &user.ReturnEmailVerificationCode{}, + }, + }, + }, + }, + }, + }, + want: &user.UpdateUserResponse{ + EmailCode: gu.Ptr("something"), + }, + } + }, + }, + { + name: "custom template", + testCase: func(runId, userId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.UpdateUserRequest{ + UserId: userId, + UserType: &user.UpdateUserRequest_Human_{ + Human: &user.UpdateUserRequest_Human{ + Email: &user.SetHumanEmail{ + Email: email, + Verification: &user.SetHumanEmail_SendCode{ + SendCode: &user.SendEmailVerificationCode{ + UrlTemplate: gu.Ptr("https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}"), + }, + }, + }, + }, + }, + }, + }, + want: &user.UpdateUserResponse{}, + } + }, + }, + { + name: "return phone verification code", + testCase: func(runId, userId string) testCase { + return testCase{ + args: args{ + CTX, + &user.UpdateUserRequest{ + UserId: userId, + UserType: &user.UpdateUserRequest_Human_{ + Human: &user.UpdateUserRequest_Human{ + Phone: &user.SetHumanPhone{ + Phone: "+41791234568", + Verification: &user.SetHumanPhone_ReturnCode{ + ReturnCode: &user.ReturnPhoneVerificationCode{}, + }, + }, + }, + }, + }, + }, + want: &user.UpdateUserResponse{ + PhoneCode: gu.Ptr("something"), + }, + } + }, + }, + { + name: "custom template error", + testCase: func(runId, userId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + CTX, + &user.UpdateUserRequest{ + UserId: userId, + UserType: &user.UpdateUserRequest_Human_{ + Human: &user.UpdateUserRequest_Human{ + Email: &user.SetHumanEmail{ + Email: email, + Verification: &user.SetHumanEmail_SendCode{ + SendCode: &user.SendEmailVerificationCode{ + UrlTemplate: gu.Ptr("{{"), + }, + }, + }, + }, + }, + }, + }, + wantErr: true, + } + }, + }, + { + name: "missing empty email", + testCase: func(runId, userId string) testCase { + return testCase{ + args: args{ + CTX, + &user.UpdateUserRequest{ + UserId: userId, + UserType: &user.UpdateUserRequest_Human_{ + Human: &user.UpdateUserRequest_Human{ + Email: &user.SetHumanEmail{}, + }, + }, + }, + }, + wantErr: true, + } + }, + }, + { + name: "password not complexity conform", + testCase: func(runId, userId string) testCase { + return testCase{ + args: args{ + CTX, + &user.UpdateUserRequest{ + UserId: userId, + UserType: &user.UpdateUserRequest_Human_{ + Human: &user.UpdateUserRequest_Human{ + Password: &user.SetPassword{ + PasswordType: &user.SetPassword_Password{ + Password: &user.Password{ + Password: "insufficient", + }, + }, + }, + }, + }, + }, + }, + wantErr: true, + } + }, + }, + { + name: "hashed password", + testCase: func(runId, userId string) testCase { + return testCase{ + args: args{ + CTX, + &user.UpdateUserRequest{ + UserId: userId, + UserType: &user.UpdateUserRequest_Human_{ + Human: &user.UpdateUserRequest_Human{ + Password: &user.SetPassword{ + PasswordType: &user.SetPassword_HashedPassword{ + HashedPassword: &user.HashedPassword{ + Hash: "$2y$12$hXUrnqdq1RIIYZ2HPytIIe5lXdIvbhqrTvdPsSF7o.jFh817Z6lwm", + }, + }, + }, + }, + }, + }, + }, + want: &user.UpdateUserResponse{}, + } + }, + }, + { + name: "unsupported hashed password", + testCase: func(runId, userId string) testCase { + return testCase{ + args: args{ + CTX, + &user.UpdateUserRequest{ + UserId: userId, + UserType: &user.UpdateUserRequest_Human_{ + Human: &user.UpdateUserRequest_Human{ + Password: &user.SetPassword{ + PasswordType: &user.SetPassword_HashedPassword{ + HashedPassword: &user.HashedPassword{ + Hash: "$scrypt$ln=16,r=8,p=1$cmFuZG9tc2FsdGlzaGFyZA$Rh+NnJNo1I6nRwaNqbDm6kmADswD1+7FTKZ7Ln9D8nQ", + }, + }, + }, + }, + }, + }, + }, + wantErr: true, + } + }, + }, + { + name: "update human user with machine fields, error", + testCase: func(runId, userId string) testCase { + return testCase{ + args: args{ + CTX, + &user.UpdateUserRequest{ + UserId: userId, + UserType: &user.UpdateUserRequest_Machine_{ + Machine: &user.UpdateUserRequest_Machine{ + Name: &runId, + }, + }, + }, + }, + wantErr: true, + } + }, + }, + } + for i, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + runId := fmt.Sprint(now.UnixNano() + int64(i)) + userId := Instance.CreateUserTypeHuman(CTX, gofakeit.Email()).GetId() + test := tt.testCase(runId, userId) + got, err := Client.UpdateUser(test.args.ctx, test.args.req) + if test.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + changeDate := got.ChangeDate.AsTime() + assert.Greater(t, changeDate, now, "change date is before the test started") + assert.Less(t, changeDate, time.Now(), "change date is in the future") + if test.want.GetEmailCode() != "" { + assert.NotEmpty(t, got.GetEmailCode(), "email code is empty") + } else { + assert.Empty(t, got.GetEmailCode(), "email code is not empty") + } + if test.want.GetPhoneCode() != "" { + assert.NotEmpty(t, got.GetPhoneCode(), "phone code is empty") + } else { + assert.Empty(t, got.GetPhoneCode(), "phone code is not empty") + } + }) + } +} + +func TestServer_UpdateUserTypeMachine(t *testing.T) { + type args struct { + ctx context.Context + req *user.UpdateUserRequest + } + type testCase struct { + args args + wantErr bool + } + tests := []struct { + name string + testCase func(runId, userId string) testCase + }{ + { + name: "update machine, ok", + testCase: func(runId, userId string) testCase { + return testCase{ + args: args{ + CTX, + &user.UpdateUserRequest{ + UserId: userId, + UserType: &user.UpdateUserRequest_Machine_{ + Machine: &user.UpdateUserRequest_Machine{ + Name: gu.Ptr("donald"), + }, + }, + }, + }, + } + }, + }, + { + name: "update machine user with human fields, error", + testCase: func(runId, userId string) testCase { + return testCase{ + args: args{ + CTX, + &user.UpdateUserRequest{ + UserId: userId, + UserType: &user.UpdateUserRequest_Human_{ + Human: &user.UpdateUserRequest_Human{ + Profile: &user.UpdateUserRequest_Human_Profile{ + GivenName: gu.Ptr("Donald"), + }, + }, + }, + }, + }, + wantErr: true, + } + }, + }, + } + for i, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + runId := fmt.Sprint(now.UnixNano() + int64(i)) + userId := Instance.CreateUserTypeMachine(CTX, Instance.DefaultOrg.Id).GetId() + test := tt.testCase(runId, userId) + got, err := Client.UpdateUser(test.args.ctx, test.args.req) + if test.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + changeDate := got.ChangeDate.AsTime() + assert.Greater(t, changeDate, now, "change date is before the test started") + assert.Less(t, changeDate, time.Now(), "change date is in the future") + }) + } +} + +func TestServer_UpdateUser_And_Compare(t *testing.T) { + type args struct { + ctx context.Context + create *user.CreateUserRequest + update *user.UpdateUserRequest + } + type testCase struct { + args args + assert func(t *testing.T, getResponse *user.GetUserByIDResponse) + } + tests := []struct { + name string + testCase func(runId string) testCase + }{{ + name: "human remove phone", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + ctx: CTX, + create: &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + UserId: &runId, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: email, + }, + Phone: &user.SetHumanPhone{ + Phone: "+1234567890", + }, + }, + }, + }, + update: &user.UpdateUserRequest{ + UserId: runId, + UserType: &user.UpdateUserRequest_Human_{ + Human: &user.UpdateUserRequest_Human{ + Phone: &user.SetHumanPhone{}, + }, + }, + }, + }, + assert: func(t *testing.T, getResponse *user.GetUserByIDResponse) { + assert.Empty(t, getResponse.GetUser().GetHuman().GetPhone().GetPhone(), "phone is not empty") + }, + } + }, + }, { + name: "human username", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + email := username + "@example.com" + return testCase{ + args: args{ + ctx: CTX, + create: &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + UserId: &runId, + UserType: &user.CreateUserRequest_Human_{ + Human: &user.CreateUserRequest_Human{ + Profile: &user.SetHumanProfile{ + GivenName: "Donald", + FamilyName: "Duck", + }, + Email: &user.SetHumanEmail{ + Email: email, + }, + }, + }, + }, + update: &user.UpdateUserRequest{ + UserId: runId, + Username: &username, + UserType: &user.UpdateUserRequest_Human_{ + Human: &user.UpdateUserRequest_Human{}, + }, + }, + }, + assert: func(t *testing.T, getResponse *user.GetUserByIDResponse) { + assert.Equal(t, username, getResponse.GetUser().GetUsername()) + }, + } + }, + }, { + name: "machine username", + testCase: func(runId string) testCase { + username := fmt.Sprintf("donald.duck+%s", runId) + return testCase{ + args: args{ + ctx: CTX, + create: &user.CreateUserRequest{ + OrganizationId: Instance.DefaultOrg.Id, + UserId: &runId, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: "Donald", + }, + }, + }, + update: &user.UpdateUserRequest{ + UserId: runId, + Username: &username, + UserType: &user.UpdateUserRequest_Machine_{ + Machine: &user.UpdateUserRequest_Machine{}, + }, + }, + }, + assert: func(t *testing.T, getResponse *user.GetUserByIDResponse) { + assert.Equal(t, username, getResponse.GetUser().GetUsername()) + }, + } + }, + }} + for i, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + now := time.Now() + runId := fmt.Sprint(now.UnixNano() + int64(i)) + test := tt.testCase(runId) + createResponse, err := Client.CreateUser(test.args.ctx, test.args.create) + require.NoError(t, err) + _, err = Client.UpdateUser(test.args.ctx, test.args.update) + require.NoError(t, err) + Instance.TriggerUserByID(test.args.ctx, createResponse.GetId()) + getResponse, err := Client.GetUserByID(test.args.ctx, &user.GetUserByIDRequest{ + UserId: createResponse.GetId(), + }) + require.NoError(t, err) + test.assert(t, getResponse) + }) + } +} + +func TestServer_UpdateUser_Permission(t *testing.T) { + newOrgOwnerEmail := gofakeit.Email() + newOrg := Instance.CreateOrganization(IamCTX, fmt.Sprintf("AddHuman-%s", gofakeit.AppName()), newOrgOwnerEmail) + newHumanUserID := newOrg.CreatedAdmins[0].GetUserId() + machineUserResp, err := Instance.Client.UserV2.CreateUser(IamCTX, &user.CreateUserRequest{ + OrganizationId: newOrg.OrganizationId, + UserType: &user.CreateUserRequest_Machine_{ + Machine: &user.CreateUserRequest_Machine{ + Name: "Donald", + }, + }, + }) + require.NoError(t, err) + newMachineUserID := machineUserResp.GetId() + Instance.TriggerUserByID(IamCTX, newMachineUserID) + type args struct { + ctx context.Context + req *user.UpdateUserRequest + } + type testCase struct { + args args + wantErr bool + } + tests := []struct { + name string + testCase func() testCase + }{ + { + name: "human, system, ok", + testCase: func() testCase { + return testCase{ + args: args{ + SystemCTX, + &user.UpdateUserRequest{ + UserId: newHumanUserID, + UserType: &user.UpdateUserRequest_Human_{ + Human: &user.UpdateUserRequest_Human{ + Profile: &user.UpdateUserRequest_Human_Profile{ + GivenName: gu.Ptr("Donald"), + }, + }, + }, + }, + }, + } + }, + }, + { + name: "human instance, ok", + testCase: func() testCase { + return testCase{ + args: args{ + IamCTX, + &user.UpdateUserRequest{ + UserId: newHumanUserID, + UserType: &user.UpdateUserRequest_Human_{ + Human: &user.UpdateUserRequest_Human{ + Profile: &user.UpdateUserRequest_Human_Profile{ + GivenName: gu.Ptr("Donald"), + }, + }, + }, + }, + }, + } + }, + }, + { + name: "human org, error", + testCase: func() testCase { + return testCase{ + args: args{ + CTX, + &user.UpdateUserRequest{ + UserId: newHumanUserID, + UserType: &user.UpdateUserRequest_Human_{ + Human: &user.UpdateUserRequest_Human{ + Profile: &user.UpdateUserRequest_Human_Profile{ + GivenName: gu.Ptr("Donald"), + }, + }, + }, + }, + }, + wantErr: true, + } + }, + }, + { + name: "human user, error", + testCase: func() testCase { + return testCase{ + args: args{ + UserCTX, + &user.UpdateUserRequest{ + UserId: newHumanUserID, + UserType: &user.UpdateUserRequest_Human_{ + Human: &user.UpdateUserRequest_Human{ + Profile: &user.UpdateUserRequest_Human_Profile{ + GivenName: gu.Ptr("Donald"), + }, + }, + }, + }, + }, + wantErr: true, + } + }, + }, + { + name: "machine system, ok", + testCase: func() testCase { + return testCase{ + args: args{ + SystemCTX, + &user.UpdateUserRequest{ + UserId: newMachineUserID, + UserType: &user.UpdateUserRequest_Machine_{ + Machine: &user.UpdateUserRequest_Machine{ + Name: gu.Ptr("Donald"), + }, + }, + }, + }, + } + }, + }, + { + name: "machine instance, ok", + testCase: func() testCase { + return testCase{ + args: args{ + IamCTX, + &user.UpdateUserRequest{ + UserId: newMachineUserID, + UserType: &user.UpdateUserRequest_Machine_{ + Machine: &user.UpdateUserRequest_Machine{ + Name: gu.Ptr("Donald"), + }, + }, + }, + }, + } + }, + }, + { + name: "machine org, error", + testCase: func() testCase { + return testCase{ + args: args{ + CTX, + &user.UpdateUserRequest{ + UserId: newMachineUserID, + UserType: &user.UpdateUserRequest_Machine_{ + Machine: &user.UpdateUserRequest_Machine{ + Name: gu.Ptr("Donald"), + }, + }, + }, + }, + wantErr: true, + } + }, + }, + { + name: "machine user, error", + testCase: func() testCase { + return testCase{ + args: args{ + UserCTX, + &user.UpdateUserRequest{ + UserId: newMachineUserID, + UserType: &user.UpdateUserRequest_Machine_{ + Machine: &user.UpdateUserRequest_Machine{ + Name: gu.Ptr("Donald"), + }, + }, + }, + }, + wantErr: true, + } + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + test := tt.testCase() + _, err := Client.UpdateUser(test.args.ctx, test.args.req) + if test.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + }) + } +} diff --git a/internal/api/grpc/user/v2/intent.go b/internal/api/grpc/user/v2/intent.go index 5514b6ef03..c26adba24d 100644 --- a/internal/api/grpc/user/v2/intent.go +++ b/internal/api/grpc/user/v2/intent.go @@ -6,6 +6,7 @@ import ( "errors" "time" + "connectrpc.com/connect" oidc_pkg "github.com/zitadel/oidc/v3/pkg/oidc" "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" @@ -32,18 +33,18 @@ import ( "github.com/zitadel/zitadel/pkg/grpc/user/v2" ) -func (s *Server) StartIdentityProviderIntent(ctx context.Context, req *user.StartIdentityProviderIntentRequest) (_ *user.StartIdentityProviderIntentResponse, err error) { - switch t := req.GetContent().(type) { +func (s *Server) StartIdentityProviderIntent(ctx context.Context, req *connect.Request[user.StartIdentityProviderIntentRequest]) (_ *connect.Response[user.StartIdentityProviderIntentResponse], err error) { + switch t := req.Msg.GetContent().(type) { case *user.StartIdentityProviderIntentRequest_Urls: - return s.startIDPIntent(ctx, req.GetIdpId(), t.Urls) + return s.startIDPIntent(ctx, req.Msg.GetIdpId(), t.Urls) case *user.StartIdentityProviderIntentRequest_Ldap: - return s.startLDAPIntent(ctx, req.GetIdpId(), t.Ldap) + return s.startLDAPIntent(ctx, req.Msg.GetIdpId(), t.Ldap) default: return nil, zerrors.ThrowUnimplementedf(nil, "USERv2-S2g21", "type oneOf %T in method StartIdentityProviderIntent not implemented", t) } } -func (s *Server) startIDPIntent(ctx context.Context, idpID string, urls *user.RedirectURLs) (*user.StartIdentityProviderIntentResponse, error) { +func (s *Server) startIDPIntent(ctx context.Context, idpID string, urls *user.RedirectURLs) (*connect.Response[user.StartIdentityProviderIntentResponse], error) { state, session, err := s.command.AuthFromProvider(ctx, idpID, s.idpCallback(ctx), s.samlRootURL(ctx, idpID)) if err != nil { return nil, err @@ -52,22 +53,31 @@ func (s *Server) startIDPIntent(ctx context.Context, idpID string, urls *user.Re if err != nil { return nil, err } - content, redirect := session.GetAuth(ctx) - if redirect { - return &user.StartIdentityProviderIntentResponse{ - Details: object.DomainToDetailsPb(details), - NextStep: &user.StartIdentityProviderIntentResponse_AuthUrl{AuthUrl: content}, - }, nil + auth, err := session.GetAuth(ctx) + if err != nil { + return nil, err } - return &user.StartIdentityProviderIntentResponse{ - Details: object.DomainToDetailsPb(details), - NextStep: &user.StartIdentityProviderIntentResponse_PostForm{ - PostForm: []byte(content), - }, - }, nil + switch a := auth.(type) { + case *idp.RedirectAuth: + return connect.NewResponse(&user.StartIdentityProviderIntentResponse{ + Details: object.DomainToDetailsPb(details), + NextStep: &user.StartIdentityProviderIntentResponse_AuthUrl{AuthUrl: a.RedirectURL}, + }), nil + case *idp.FormAuth: + return connect.NewResponse(&user.StartIdentityProviderIntentResponse{ + Details: object.DomainToDetailsPb(details), + NextStep: &user.StartIdentityProviderIntentResponse_FormData{ + FormData: &user.FormData{ + Url: a.URL, + Fields: a.Fields, + }, + }, + }), nil + } + return nil, zerrors.ThrowInvalidArgumentf(nil, "USERv2-3g2j3", "type oneOf %T in method StartIdentityProviderIntent not implemented", auth) } -func (s *Server) startLDAPIntent(ctx context.Context, idpID string, ldapCredentials *user.LDAPCredentials) (*user.StartIdentityProviderIntentResponse, error) { +func (s *Server) startLDAPIntent(ctx context.Context, idpID string, ldapCredentials *user.LDAPCredentials) (*connect.Response[user.StartIdentityProviderIntentResponse], error) { intentWriteModel, details, err := s.command.CreateIntent(ctx, "", idpID, "", "", authz.GetInstance(ctx).InstanceID(), nil) if err != nil { return nil, err @@ -83,7 +93,7 @@ func (s *Server) startLDAPIntent(ctx context.Context, idpID string, ldapCredenti if err != nil { return nil, err } - return &user.StartIdentityProviderIntentResponse{ + return connect.NewResponse(&user.StartIdentityProviderIntentResponse{ Details: object.DomainToDetailsPb(details), NextStep: &user.StartIdentityProviderIntentResponse_IdpIntent{ IdpIntent: &user.IDPIntent{ @@ -92,7 +102,7 @@ func (s *Server) startLDAPIntent(ctx context.Context, idpID string, ldapCredenti UserId: userID, }, }, - }, nil + }), nil } func (s *Server) checkLinkedExternalUser(ctx context.Context, idpID, externalUserID string) (string, error) { @@ -141,12 +151,12 @@ func (s *Server) ldapLogin(ctx context.Context, idpID, username, password string return externalUser, userID, session, nil } -func (s *Server) RetrieveIdentityProviderIntent(ctx context.Context, req *user.RetrieveIdentityProviderIntentRequest) (_ *user.RetrieveIdentityProviderIntentResponse, err error) { - intent, err := s.command.GetIntentWriteModel(ctx, req.GetIdpIntentId(), "") +func (s *Server) RetrieveIdentityProviderIntent(ctx context.Context, req *connect.Request[user.RetrieveIdentityProviderIntentRequest]) (_ *connect.Response[user.RetrieveIdentityProviderIntentResponse], err error) { + intent, err := s.command.GetIntentWriteModel(ctx, req.Msg.GetIdpIntentId(), "") if err != nil { return nil, err } - if err := s.checkIntentToken(req.GetIdpIntentToken(), intent.AggregateID); err != nil { + if err := s.checkIntentToken(req.Msg.GetIdpIntentToken(), intent.AggregateID); err != nil { return nil, err } if intent.State != domain.IDPIntentStateSucceeded { @@ -194,7 +204,7 @@ func (s *Server) RetrieveIdentityProviderIntent(ctx context.Context, req *user.R } idpIntent.AddHumanUser = idpUserToAddHumanUser(idpUser, idpIntent.IdpInformation.IdpId) } - return idpIntent, nil + return connect.NewResponse(idpIntent), nil } type rawUserMapper struct { diff --git a/internal/api/grpc/user/v2/key.go b/internal/api/grpc/user/v2/key.go new file mode 100644 index 0000000000..021f4be388 --- /dev/null +++ b/internal/api/grpc/user/v2/key.go @@ -0,0 +1,63 @@ +package user + +import ( + "context" + + "connectrpc.com/connect" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore/v1/models" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +func (s *Server) AddKey(ctx context.Context, req *connect.Request[user.AddKeyRequest]) (*connect.Response[user.AddKeyResponse], error) { + newMachineKey := &command.MachineKey{ + ObjectRoot: models.ObjectRoot{ + AggregateID: req.Msg.GetUserId(), + }, + ExpirationDate: req.Msg.GetExpirationDate().AsTime(), + Type: domain.AuthNKeyTypeJSON, + PermissionCheck: s.command.NewPermissionCheckUserWrite(ctx), + } + newMachineKey.PublicKey = req.Msg.GetPublicKey() + + pubkeySupplied := len(newMachineKey.PublicKey) > 0 + details, err := s.command.AddUserMachineKey(ctx, newMachineKey) + if err != nil { + return nil, err + } + // Return key details only if the pubkey wasn't supplied, otherwise the user already has + // private key locally + var keyDetails []byte + if !pubkeySupplied { + var err error + keyDetails, err = newMachineKey.Detail() + if err != nil { + return nil, err + } + } + return connect.NewResponse(&user.AddKeyResponse{ + KeyId: newMachineKey.KeyID, + KeyContent: keyDetails, + CreationDate: timestamppb.New(details.EventDate), + }), nil +} + +func (s *Server) RemoveKey(ctx context.Context, req *connect.Request[user.RemoveKeyRequest]) (*connect.Response[user.RemoveKeyResponse], error) { + machineKey := &command.MachineKey{ + ObjectRoot: models.ObjectRoot{ + AggregateID: req.Msg.GetUserId(), + }, + PermissionCheck: s.command.NewPermissionCheckUserWrite(ctx), + KeyID: req.Msg.GetKeyId(), + } + objectDetails, err := s.command.RemoveUserMachineKey(ctx, machineKey) + if err != nil { + return nil, err + } + return connect.NewResponse(&user.RemoveKeyResponse{ + DeletionDate: timestamppb.New(objectDetails.EventDate), + }), nil +} diff --git a/internal/api/grpc/user/v2/key_query.go b/internal/api/grpc/user/v2/key_query.go new file mode 100644 index 0000000000..e9466a791b --- /dev/null +++ b/internal/api/grpc/user/v2/key_query.go @@ -0,0 +1,125 @@ +package user + +import ( + "context" + + "connectrpc.com/connect" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/api/grpc/filter/v2" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/zerrors" + filter_pb "github.com/zitadel/zitadel/pkg/grpc/filter/v2" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +func (s *Server) ListKeys(ctx context.Context, req *connect.Request[user.ListKeysRequest]) (*connect.Response[user.ListKeysResponse], error) { + offset, limit, asc, err := filter.PaginationPbToQuery(s.systemDefaults, req.Msg.GetPagination()) + if err != nil { + return nil, err + } + + filters, err := keyFiltersToQueries(req.Msg.GetFilters()) + if err != nil { + return nil, err + } + search := &query.AuthNKeySearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + SortingColumn: authnKeyFieldNameToSortingColumn(req.Msg.SortingColumn), + }, + Queries: filters, + } + result, err := s.query.SearchAuthNKeys(ctx, search, query.JoinFilterUserMachine, s.checkPermission) + if err != nil { + return nil, err + } + resp := &user.ListKeysResponse{ + Result: make([]*user.Key, len(result.AuthNKeys)), + Pagination: filter.QueryToPaginationPb(search.SearchRequest, result.SearchResponse), + } + for i, key := range result.AuthNKeys { + resp.Result[i] = &user.Key{ + CreationDate: timestamppb.New(key.CreationDate), + ChangeDate: timestamppb.New(key.ChangeDate), + Id: key.ID, + UserId: key.AggregateID, + OrganizationId: key.ResourceOwner, + ExpirationDate: timestamppb.New(key.Expiration), + } + } + return connect.NewResponse(resp), nil +} + +func keyFiltersToQueries(filters []*user.KeysSearchFilter) (_ []query.SearchQuery, err error) { + q := make([]query.SearchQuery, len(filters)) + for i, filter := range filters { + q[i], err = keyFilterToQuery(filter) + if err != nil { + return nil, err + } + } + return q, nil +} + +func keyFilterToQuery(filter *user.KeysSearchFilter) (query.SearchQuery, error) { + switch q := filter.Filter.(type) { + case *user.KeysSearchFilter_CreatedDateFilter: + return authnKeyCreatedFilterToQuery(q.CreatedDateFilter) + case *user.KeysSearchFilter_ExpirationDateFilter: + return authnKeyExpirationFilterToQuery(q.ExpirationDateFilter) + case *user.KeysSearchFilter_KeyIdFilter: + return authnKeyIdFilterToQuery(q.KeyIdFilter) + case *user.KeysSearchFilter_UserIdFilter: + return authnKeyUserIdFilterToQuery(q.UserIdFilter) + case *user.KeysSearchFilter_OrganizationIdFilter: + return authnKeyOrgIdFilterToQuery(q.OrganizationIdFilter) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "GRPC-vR9nC", "List.Query.Invalid") + } +} + +func authnKeyIdFilterToQuery(f *filter_pb.IDFilter) (query.SearchQuery, error) { + return query.NewAuthNKeyIDQuery(f.Id) +} + +func authnKeyUserIdFilterToQuery(f *filter_pb.IDFilter) (query.SearchQuery, error) { + return query.NewAuthNKeyIdentifyerQuery(f.Id) +} + +func authnKeyOrgIdFilterToQuery(f *filter_pb.IDFilter) (query.SearchQuery, error) { + return query.NewAuthNKeyResourceOwnerQuery(f.Id) +} + +func authnKeyCreatedFilterToQuery(f *filter_pb.TimestampFilter) (query.SearchQuery, error) { + return query.NewAuthNKeyCreationDateQuery(f.Timestamp.AsTime(), filter.TimestampMethodPbToQuery(f.Method)) +} + +func authnKeyExpirationFilterToQuery(f *filter_pb.TimestampFilter) (query.SearchQuery, error) { + return query.NewAuthNKeyExpirationDateDateQuery(f.Timestamp.AsTime(), filter.TimestampMethodPbToQuery(f.Method)) +} + +// authnKeyFieldNameToSortingColumn defaults to the creation date because this ensures deterministic pagination +func authnKeyFieldNameToSortingColumn(field *user.KeyFieldName) query.Column { + if field == nil { + return query.AuthNKeyColumnCreationDate + } + switch *field { + case user.KeyFieldName_KEY_FIELD_NAME_UNSPECIFIED: + return query.AuthNKeyColumnCreationDate + case user.KeyFieldName_KEY_FIELD_NAME_ID: + return query.AuthNKeyColumnID + case user.KeyFieldName_KEY_FIELD_NAME_USER_ID: + return query.AuthNKeyColumnIdentifier + case user.KeyFieldName_KEY_FIELD_NAME_ORGANIZATION_ID: + return query.AuthNKeyColumnResourceOwner + case user.KeyFieldName_KEY_FIELD_NAME_CREATED_DATE: + return query.AuthNKeyColumnCreationDate + case user.KeyFieldName_KEY_FIELD_NAME_KEY_EXPIRATION_DATE: + return query.AuthNKeyColumnExpiration + default: + return query.AuthNKeyColumnCreationDate + } +} diff --git a/internal/api/grpc/user/v2/machine.go b/internal/api/grpc/user/v2/machine.go new file mode 100644 index 0000000000..e5126b9019 --- /dev/null +++ b/internal/api/grpc/user/v2/machine.go @@ -0,0 +1,60 @@ +package user + +import ( + "context" + + "connectrpc.com/connect" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore/v1/models" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +func (s *Server) createUserTypeMachine(ctx context.Context, machinePb *user.CreateUserRequest_Machine, orgId, userName, userId string) (*connect.Response[user.CreateUserResponse], error) { + cmd := &command.Machine{ + Username: userName, + Name: machinePb.Name, + Description: machinePb.GetDescription(), + AccessTokenType: domain.OIDCTokenTypeBearer, + ObjectRoot: models.ObjectRoot{ + ResourceOwner: orgId, + AggregateID: userId, + }, + } + details, err := s.command.AddMachine( + ctx, + cmd, + nil, + s.command.NewPermissionCheckUserWrite(ctx), + command.AddMachineWithUsernameToIDFallback(), + ) + if err != nil { + return nil, err + } + return connect.NewResponse(&user.CreateUserResponse{ + Id: cmd.AggregateID, + CreationDate: timestamppb.New(details.EventDate), + }), nil +} + +func (s *Server) updateUserTypeMachine(ctx context.Context, machinePb *user.UpdateUserRequest_Machine, userId string, userName *string) (*connect.Response[user.UpdateUserResponse], error) { + cmd := updateMachineUserToCommand(userId, userName, machinePb) + err := s.command.ChangeUserMachine(ctx, cmd) + if err != nil { + return nil, err + } + return connect.NewResponse(&user.UpdateUserResponse{ + ChangeDate: timestamppb.New(cmd.Details.EventDate), + }), nil +} + +func updateMachineUserToCommand(userId string, userName *string, machine *user.UpdateUserRequest_Machine) *command.ChangeMachine { + return &command.ChangeMachine{ + ID: userId, + Username: userName, + Name: machine.Name, + Description: machine.Description, + } +} diff --git a/internal/api/grpc/user/v2/machine_test.go b/internal/api/grpc/user/v2/machine_test.go new file mode 100644 index 0000000000..96d77d8fa2 --- /dev/null +++ b/internal/api/grpc/user/v2/machine_test.go @@ -0,0 +1,62 @@ +package user + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/muhlemmer/gu" + "golang.org/x/text/language" + + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +func Test_patchMachineUserToCommand(t *testing.T) { + type args struct { + userId string + userName *string + machine *user.UpdateUserRequest_Machine + } + tests := []struct { + name string + args args + want *command.ChangeMachine + }{{ + name: "single property", + args: args{ + userId: "userId", + machine: &user.UpdateUserRequest_Machine{ + Name: gu.Ptr("name"), + }, + }, + want: &command.ChangeMachine{ + ID: "userId", + Name: gu.Ptr("name"), + }, + }, { + name: "all properties", + args: args{ + userId: "userId", + userName: gu.Ptr("userName"), + machine: &user.UpdateUserRequest_Machine{ + Name: gu.Ptr("name"), + Description: gu.Ptr("description"), + }, + }, + want: &command.ChangeMachine{ + ID: "userId", + Username: gu.Ptr("userName"), + Name: gu.Ptr("name"), + Description: gu.Ptr("description"), + }, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := updateMachineUserToCommand(tt.args.userId, tt.args.userName, tt.args.machine) + if diff := cmp.Diff(tt.want, got, cmpopts.EquateComparable(language.Tag{})); diff != "" { + t.Errorf("patchMachineUserToCommand() mismatch (-want +got):\n%s", diff) + } + }) + } +} diff --git a/internal/api/grpc/user/v2/metadata.go b/internal/api/grpc/user/v2/metadata.go new file mode 100644 index 0000000000..338ce9fc45 --- /dev/null +++ b/internal/api/grpc/user/v2/metadata.go @@ -0,0 +1,80 @@ +package user + +import ( + "context" + + "connectrpc.com/connect" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/api/grpc/filter/v2" + "github.com/zitadel/zitadel/internal/api/grpc/metadata/v2" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +func (s *Server) ListUserMetadata(ctx context.Context, req *connect.Request[user.ListUserMetadataRequest]) (*connect.Response[user.ListUserMetadataResponse], error) { + metadataQueries, err := s.listUserMetadataRequestToModel(req.Msg) + if err != nil { + return nil, err + } + res, err := s.query.SearchUserMetadata(ctx, true, req.Msg.UserId, metadataQueries, s.checkPermission) + if err != nil { + return nil, err + } + return connect.NewResponse(&user.ListUserMetadataResponse{ + Metadata: metadata.UserMetadataListToPb(res.Metadata), + Pagination: filter.QueryToPaginationPb(metadataQueries.SearchRequest, res.SearchResponse), + }), nil +} + +func (s *Server) listUserMetadataRequestToModel(req *user.ListUserMetadataRequest) (*query.UserMetadataSearchQueries, error) { + offset, limit, asc, err := filter.PaginationPbToQuery(s.systemDefaults, req.Pagination) + if err != nil { + return nil, err + } + queries, err := metadata.UserMetadataFiltersToQuery(req.Filters) + if err != nil { + return nil, err + } + return &query.UserMetadataSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + SortingColumn: query.UserMetadataCreationDateCol, + }, + Queries: queries, + }, nil +} + +func (s *Server) SetUserMetadata(ctx context.Context, req *connect.Request[user.SetUserMetadataRequest]) (*connect.Response[user.SetUserMetadataResponse], error) { + result, err := s.command.BulkSetUserMetadata(ctx, req.Msg.UserId, "", setUserMetadataToDomain(req.Msg)...) + if err != nil { + return nil, err + } + return connect.NewResponse(&user.SetUserMetadataResponse{ + SetDate: timestamppb.New(result.EventDate), + }), nil +} + +func setUserMetadataToDomain(req *user.SetUserMetadataRequest) []*domain.Metadata { + metadata := make([]*domain.Metadata, len(req.Metadata)) + for i, data := range req.Metadata { + metadata[i] = &domain.Metadata{ + Key: data.Key, + Value: data.Value, + } + } + return metadata +} + +func (s *Server) DeleteUserMetadata(ctx context.Context, req *connect.Request[user.DeleteUserMetadataRequest]) (*connect.Response[user.DeleteUserMetadataResponse], error) { + result, err := s.command.BulkRemoveUserMetadata(ctx, req.Msg.UserId, "", req.Msg.Keys...) + if err != nil { + return nil, err + } + return connect.NewResponse(&user.DeleteUserMetadataResponse{ + DeletionDate: timestamppb.New(result.EventDate), + }), nil +} diff --git a/internal/api/grpc/user/v2/otp.go b/internal/api/grpc/user/v2/otp.go index fd76cf2b93..2f04f438dd 100644 --- a/internal/api/grpc/user/v2/otp.go +++ b/internal/api/grpc/user/v2/otp.go @@ -3,39 +3,41 @@ package user import ( "context" + "connectrpc.com/connect" + "github.com/zitadel/zitadel/internal/api/grpc/object/v2" "github.com/zitadel/zitadel/pkg/grpc/user/v2" ) -func (s *Server) AddOTPSMS(ctx context.Context, req *user.AddOTPSMSRequest) (*user.AddOTPSMSResponse, error) { - details, err := s.command.AddHumanOTPSMS(ctx, req.GetUserId(), "") +func (s *Server) AddOTPSMS(ctx context.Context, req *connect.Request[user.AddOTPSMSRequest]) (*connect.Response[user.AddOTPSMSResponse], error) { + details, err := s.command.AddHumanOTPSMS(ctx, req.Msg.GetUserId(), "") if err != nil { return nil, err } - return &user.AddOTPSMSResponse{Details: object.DomainToDetailsPb(details)}, nil + return connect.NewResponse(&user.AddOTPSMSResponse{Details: object.DomainToDetailsPb(details)}), nil } -func (s *Server) RemoveOTPSMS(ctx context.Context, req *user.RemoveOTPSMSRequest) (*user.RemoveOTPSMSResponse, error) { - objectDetails, err := s.command.RemoveHumanOTPSMS(ctx, req.GetUserId(), "") +func (s *Server) RemoveOTPSMS(ctx context.Context, req *connect.Request[user.RemoveOTPSMSRequest]) (*connect.Response[user.RemoveOTPSMSResponse], error) { + objectDetails, err := s.command.RemoveHumanOTPSMS(ctx, req.Msg.GetUserId(), "") if err != nil { return nil, err } - return &user.RemoveOTPSMSResponse{Details: object.DomainToDetailsPb(objectDetails)}, nil + return connect.NewResponse(&user.RemoveOTPSMSResponse{Details: object.DomainToDetailsPb(objectDetails)}), nil } -func (s *Server) AddOTPEmail(ctx context.Context, req *user.AddOTPEmailRequest) (*user.AddOTPEmailResponse, error) { - details, err := s.command.AddHumanOTPEmail(ctx, req.GetUserId(), "") +func (s *Server) AddOTPEmail(ctx context.Context, req *connect.Request[user.AddOTPEmailRequest]) (*connect.Response[user.AddOTPEmailResponse], error) { + details, err := s.command.AddHumanOTPEmail(ctx, req.Msg.GetUserId(), "") if err != nil { return nil, err } - return &user.AddOTPEmailResponse{Details: object.DomainToDetailsPb(details)}, nil + return connect.NewResponse(&user.AddOTPEmailResponse{Details: object.DomainToDetailsPb(details)}), nil } -func (s *Server) RemoveOTPEmail(ctx context.Context, req *user.RemoveOTPEmailRequest) (*user.RemoveOTPEmailResponse, error) { - objectDetails, err := s.command.RemoveHumanOTPEmail(ctx, req.GetUserId(), "") +func (s *Server) RemoveOTPEmail(ctx context.Context, req *connect.Request[user.RemoveOTPEmailRequest]) (*connect.Response[user.RemoveOTPEmailResponse], error) { + objectDetails, err := s.command.RemoveHumanOTPEmail(ctx, req.Msg.GetUserId(), "") if err != nil { return nil, err } - return &user.RemoveOTPEmailResponse{Details: object.DomainToDetailsPb(objectDetails)}, nil + return connect.NewResponse(&user.RemoveOTPEmailResponse{Details: object.DomainToDetailsPb(objectDetails)}), nil } diff --git a/internal/api/grpc/user/v2/passkey.go b/internal/api/grpc/user/v2/passkey.go index 145c1e5716..90c6d72d13 100644 --- a/internal/api/grpc/user/v2/passkey.go +++ b/internal/api/grpc/user/v2/passkey.go @@ -3,6 +3,7 @@ package user import ( "context" + "connectrpc.com/connect" "google.golang.org/protobuf/types/known/structpb" "github.com/zitadel/zitadel/internal/api/grpc/object/v2" @@ -13,17 +14,17 @@ import ( "github.com/zitadel/zitadel/pkg/grpc/user/v2" ) -func (s *Server) RegisterPasskey(ctx context.Context, req *user.RegisterPasskeyRequest) (resp *user.RegisterPasskeyResponse, err error) { +func (s *Server) RegisterPasskey(ctx context.Context, req *connect.Request[user.RegisterPasskeyRequest]) (resp *connect.Response[user.RegisterPasskeyResponse], err error) { var ( - authenticator = passkeyAuthenticatorToDomain(req.GetAuthenticator()) + authenticator = passkeyAuthenticatorToDomain(req.Msg.GetAuthenticator()) ) - if code := req.GetCode(); code != nil { + if code := req.Msg.GetCode(); code != nil { return passkeyRegistrationDetailsToPb( - s.command.RegisterUserPasskeyWithCode(ctx, req.GetUserId(), "", authenticator, code.Id, code.Code, req.GetDomain(), s.userCodeAlg), + s.command.RegisterUserPasskeyWithCode(ctx, req.Msg.GetUserId(), "", authenticator, code.Id, code.Code, req.Msg.GetDomain(), s.userCodeAlg), ) } return passkeyRegistrationDetailsToPb( - s.command.RegisterUserPasskey(ctx, req.GetUserId(), "", req.GetDomain(), authenticator), + s.command.RegisterUserPasskey(ctx, req.Msg.GetUserId(), "", req.Msg.GetDomain(), authenticator), ) } @@ -51,86 +52,86 @@ func webAuthNRegistrationDetailsToPb(details *domain.WebAuthNRegistrationDetails return object.DomainToDetailsPb(details.ObjectDetails), options, nil } -func passkeyRegistrationDetailsToPb(details *domain.WebAuthNRegistrationDetails, err error) (*user.RegisterPasskeyResponse, error) { +func passkeyRegistrationDetailsToPb(details *domain.WebAuthNRegistrationDetails, err error) (*connect.Response[user.RegisterPasskeyResponse], error) { objectDetails, options, err := webAuthNRegistrationDetailsToPb(details, err) if err != nil { return nil, err } - return &user.RegisterPasskeyResponse{ + return connect.NewResponse(&user.RegisterPasskeyResponse{ Details: objectDetails, PasskeyId: details.ID, PublicKeyCredentialCreationOptions: options, - }, nil + }), nil } -func (s *Server) VerifyPasskeyRegistration(ctx context.Context, req *user.VerifyPasskeyRegistrationRequest) (*user.VerifyPasskeyRegistrationResponse, error) { - pkc, err := req.GetPublicKeyCredential().MarshalJSON() +func (s *Server) VerifyPasskeyRegistration(ctx context.Context, req *connect.Request[user.VerifyPasskeyRegistrationRequest]) (*connect.Response[user.VerifyPasskeyRegistrationResponse], error) { + pkc, err := req.Msg.GetPublicKeyCredential().MarshalJSON() if err != nil { return nil, zerrors.ThrowInternal(err, "USERv2-Pha2o", "Errors.Internal") } - objectDetails, err := s.command.HumanHumanPasswordlessSetup(ctx, req.GetUserId(), "", req.GetPasskeyName(), "", pkc) + objectDetails, err := s.command.HumanHumanPasswordlessSetup(ctx, req.Msg.GetUserId(), "", req.Msg.GetPasskeyName(), "", pkc) if err != nil { return nil, err } - return &user.VerifyPasskeyRegistrationResponse{ + return connect.NewResponse(&user.VerifyPasskeyRegistrationResponse{ Details: object.DomainToDetailsPb(objectDetails), - }, nil + }), nil } -func (s *Server) CreatePasskeyRegistrationLink(ctx context.Context, req *user.CreatePasskeyRegistrationLinkRequest) (resp *user.CreatePasskeyRegistrationLinkResponse, err error) { - switch medium := req.Medium.(type) { +func (s *Server) CreatePasskeyRegistrationLink(ctx context.Context, req *connect.Request[user.CreatePasskeyRegistrationLinkRequest]) (resp *connect.Response[user.CreatePasskeyRegistrationLinkResponse], err error) { + switch medium := req.Msg.Medium.(type) { case nil: return passkeyDetailsToPb( - s.command.AddUserPasskeyCode(ctx, req.GetUserId(), "", s.userCodeAlg), + s.command.AddUserPasskeyCode(ctx, req.Msg.GetUserId(), "", s.userCodeAlg), ) case *user.CreatePasskeyRegistrationLinkRequest_SendLink: return passkeyDetailsToPb( - s.command.AddUserPasskeyCodeURLTemplate(ctx, req.GetUserId(), "", s.userCodeAlg, medium.SendLink.GetUrlTemplate()), + s.command.AddUserPasskeyCodeURLTemplate(ctx, req.Msg.GetUserId(), "", s.userCodeAlg, medium.SendLink.GetUrlTemplate()), ) case *user.CreatePasskeyRegistrationLinkRequest_ReturnCode: return passkeyCodeDetailsToPb( - s.command.AddUserPasskeyCodeReturn(ctx, req.GetUserId(), "", s.userCodeAlg), + s.command.AddUserPasskeyCodeReturn(ctx, req.Msg.GetUserId(), "", s.userCodeAlg), ) default: return nil, zerrors.ThrowUnimplementedf(nil, "USERv2-gaD8y", "verification oneOf %T in method CreatePasskeyRegistrationLink not implemented", medium) } } -func passkeyDetailsToPb(details *domain.ObjectDetails, err error) (*user.CreatePasskeyRegistrationLinkResponse, error) { +func passkeyDetailsToPb(details *domain.ObjectDetails, err error) (*connect.Response[user.CreatePasskeyRegistrationLinkResponse], error) { if err != nil { return nil, err } - return &user.CreatePasskeyRegistrationLinkResponse{ + return connect.NewResponse(&user.CreatePasskeyRegistrationLinkResponse{ Details: object.DomainToDetailsPb(details), - }, nil + }), nil } -func passkeyCodeDetailsToPb(details *domain.PasskeyCodeDetails, err error) (*user.CreatePasskeyRegistrationLinkResponse, error) { +func passkeyCodeDetailsToPb(details *domain.PasskeyCodeDetails, err error) (*connect.Response[user.CreatePasskeyRegistrationLinkResponse], error) { if err != nil { return nil, err } - return &user.CreatePasskeyRegistrationLinkResponse{ + return connect.NewResponse(&user.CreatePasskeyRegistrationLinkResponse{ Details: object.DomainToDetailsPb(details.ObjectDetails), Code: &user.PasskeyRegistrationCode{ Id: details.CodeID, Code: details.Code, }, - }, nil + }), nil } -func (s *Server) RemovePasskey(ctx context.Context, req *user.RemovePasskeyRequest) (*user.RemovePasskeyResponse, error) { - objectDetails, err := s.command.HumanRemovePasswordless(ctx, req.GetUserId(), req.GetPasskeyId(), "") +func (s *Server) RemovePasskey(ctx context.Context, req *connect.Request[user.RemovePasskeyRequest]) (*connect.Response[user.RemovePasskeyResponse], error) { + objectDetails, err := s.command.HumanRemovePasswordless(ctx, req.Msg.GetUserId(), req.Msg.GetPasskeyId(), "") if err != nil { return nil, err } - return &user.RemovePasskeyResponse{ + return connect.NewResponse(&user.RemovePasskeyResponse{ Details: object.DomainToDetailsPb(objectDetails), - }, nil + }), nil } -func (s *Server) ListPasskeys(ctx context.Context, req *user.ListPasskeysRequest) (*user.ListPasskeysResponse, error) { +func (s *Server) ListPasskeys(ctx context.Context, req *connect.Request[user.ListPasskeysRequest]) (*connect.Response[user.ListPasskeysResponse], error) { query := new(query.UserAuthMethodSearchQueries) - err := query.AppendUserIDQuery(req.UserId) + err := query.AppendUserIDQuery(req.Msg.UserId) if err != nil { return nil, err } @@ -146,10 +147,10 @@ func (s *Server) ListPasskeys(ctx context.Context, req *user.ListPasskeysRequest if err != nil { return nil, err } - return &user.ListPasskeysResponse{ + return connect.NewResponse(&user.ListPasskeysResponse{ Details: object.ToListDetails(authMethods.SearchResponse), Result: authMethodsToPasskeyPb(authMethods), - }, nil + }), nil } func authMethodsToPasskeyPb(methods *query.AuthMethods) []*user.Passkey { diff --git a/internal/api/grpc/user/v2/passkey_test.go b/internal/api/grpc/user/v2/passkey_test.go index 9263012b98..6429dd7ce6 100644 --- a/internal/api/grpc/user/v2/passkey_test.go +++ b/internal/api/grpc/user/v2/passkey_test.go @@ -123,11 +123,11 @@ func Test_passkeyRegistrationDetailsToPb(t *testing.T) { t.Run(tt.name, func(t *testing.T) { got, err := passkeyRegistrationDetailsToPb(tt.args.details, tt.args.err) require.ErrorIs(t, err, tt.wantErr) - if !proto.Equal(tt.want, got) { + if tt.want != nil && !proto.Equal(tt.want, got.Msg) { t.Errorf("Not equal:\nExpected\n%s\nActual:%s", tt.want, got) } if tt.want != nil { - grpc.AllFieldsSet(t, got.ProtoReflect()) + grpc.AllFieldsSet(t, got.Msg.ProtoReflect()) } }) } @@ -181,7 +181,9 @@ func Test_passkeyDetailsToPb(t *testing.T) { t.Run(tt.name, func(t *testing.T) { got, err := passkeyDetailsToPb(tt.args.details, tt.args.err) require.ErrorIs(t, err, tt.args.err) - assert.Equal(t, tt.want, got) + if tt.want != nil { + assert.Equal(t, tt.want, got.Msg) + } }) } } @@ -242,9 +244,9 @@ func Test_passkeyCodeDetailsToPb(t *testing.T) { t.Run(tt.name, func(t *testing.T) { got, err := passkeyCodeDetailsToPb(tt.args.details, tt.args.err) require.ErrorIs(t, err, tt.args.err) - assert.Equal(t, tt.want, got) if tt.want != nil { - grpc.AllFieldsSet(t, got.ProtoReflect()) + assert.Equal(t, tt.want, got.Msg) + grpc.AllFieldsSet(t, got.Msg.ProtoReflect()) } }) } diff --git a/internal/api/grpc/user/v2/password.go b/internal/api/grpc/user/v2/password.go index 55cf225c4b..a256a00355 100644 --- a/internal/api/grpc/user/v2/password.go +++ b/internal/api/grpc/user/v2/password.go @@ -3,23 +3,25 @@ package user import ( "context" + "connectrpc.com/connect" + "github.com/zitadel/zitadel/internal/api/grpc/object/v2" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/zerrors" "github.com/zitadel/zitadel/pkg/grpc/user/v2" ) -func (s *Server) PasswordReset(ctx context.Context, req *user.PasswordResetRequest) (_ *user.PasswordResetResponse, err error) { +func (s *Server) PasswordReset(ctx context.Context, req *connect.Request[user.PasswordResetRequest]) (_ *connect.Response[user.PasswordResetResponse], err error) { var details *domain.ObjectDetails var code *string - switch m := req.GetMedium().(type) { + switch m := req.Msg.GetMedium().(type) { case *user.PasswordResetRequest_SendLink: - details, code, err = s.command.RequestPasswordResetURLTemplate(ctx, req.GetUserId(), m.SendLink.GetUrlTemplate(), notificationTypeToDomain(m.SendLink.GetNotificationType())) + details, code, err = s.command.RequestPasswordResetURLTemplate(ctx, req.Msg.GetUserId(), m.SendLink.GetUrlTemplate(), notificationTypeToDomain(m.SendLink.GetNotificationType())) case *user.PasswordResetRequest_ReturnCode: - details, code, err = s.command.RequestPasswordResetReturnCode(ctx, req.GetUserId()) + details, code, err = s.command.RequestPasswordResetReturnCode(ctx, req.Msg.GetUserId()) case nil: - details, code, err = s.command.RequestPasswordReset(ctx, req.GetUserId()) + details, code, err = s.command.RequestPasswordReset(ctx, req.Msg.GetUserId()) default: err = zerrors.ThrowUnimplementedf(nil, "USERv2-SDeeg", "verification oneOf %T in method RequestPasswordReset not implemented", m) } @@ -27,10 +29,10 @@ func (s *Server) PasswordReset(ctx context.Context, req *user.PasswordResetReque return nil, err } - return &user.PasswordResetResponse{ + return connect.NewResponse(&user.PasswordResetResponse{ Details: object.DomainToDetailsPb(details), VerificationCode: code, - }, nil + }), nil } func notificationTypeToDomain(notificationType user.NotificationType) domain.NotificationType { @@ -46,16 +48,16 @@ func notificationTypeToDomain(notificationType user.NotificationType) domain.Not } } -func (s *Server) SetPassword(ctx context.Context, req *user.SetPasswordRequest) (_ *user.SetPasswordResponse, err error) { +func (s *Server) SetPassword(ctx context.Context, req *connect.Request[user.SetPasswordRequest]) (_ *connect.Response[user.SetPasswordResponse], err error) { var details *domain.ObjectDetails - switch v := req.GetVerification().(type) { + switch v := req.Msg.GetVerification().(type) { case *user.SetPasswordRequest_CurrentPassword: - details, err = s.command.ChangePassword(ctx, "", req.GetUserId(), v.CurrentPassword, req.GetNewPassword().GetPassword(), "", req.GetNewPassword().GetChangeRequired()) + details, err = s.command.ChangePassword(ctx, "", req.Msg.GetUserId(), v.CurrentPassword, req.Msg.GetNewPassword().GetPassword(), "", req.Msg.GetNewPassword().GetChangeRequired()) case *user.SetPasswordRequest_VerificationCode: - details, err = s.command.SetPasswordWithVerifyCode(ctx, "", req.GetUserId(), v.VerificationCode, req.GetNewPassword().GetPassword(), "", req.GetNewPassword().GetChangeRequired()) + details, err = s.command.SetPasswordWithVerifyCode(ctx, "", req.Msg.GetUserId(), v.VerificationCode, req.Msg.GetNewPassword().GetPassword(), "", req.Msg.GetNewPassword().GetChangeRequired()) case nil: - details, err = s.command.SetPassword(ctx, "", req.GetUserId(), req.GetNewPassword().GetPassword(), req.GetNewPassword().GetChangeRequired()) + details, err = s.command.SetPassword(ctx, "", req.Msg.GetUserId(), req.Msg.GetNewPassword().GetPassword(), req.Msg.GetNewPassword().GetChangeRequired()) default: err = zerrors.ThrowUnimplementedf(nil, "USERv2-SFdf2", "verification oneOf %T in method SetPasswordRequest not implemented", v) } @@ -63,7 +65,7 @@ func (s *Server) SetPassword(ctx context.Context, req *user.SetPasswordRequest) return nil, err } - return &user.SetPasswordResponse{ + return connect.NewResponse(&user.SetPasswordResponse{ Details: object.DomainToDetailsPb(details), - }, nil + }), nil } diff --git a/internal/api/grpc/user/v2/pat.go b/internal/api/grpc/user/v2/pat.go new file mode 100644 index 0000000000..0c90eeaebd --- /dev/null +++ b/internal/api/grpc/user/v2/pat.go @@ -0,0 +1,57 @@ +package user + +import ( + "context" + + "connectrpc.com/connect" + "github.com/zitadel/oidc/v3/pkg/oidc" + "google.golang.org/protobuf/types/known/timestamppb" + + z_oidc "github.com/zitadel/zitadel/internal/api/oidc" + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore/v1/models" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +func (s *Server) AddPersonalAccessToken(ctx context.Context, req *connect.Request[user.AddPersonalAccessTokenRequest]) (*connect.Response[user.AddPersonalAccessTokenResponse], error) { + newPat := &command.PersonalAccessToken{ + ObjectRoot: models.ObjectRoot{ + AggregateID: req.Msg.GetUserId(), + }, + PermissionCheck: s.command.NewPermissionCheckUserWrite(ctx), + ExpirationDate: req.Msg.GetExpirationDate().AsTime(), + Scopes: []string{ + oidc.ScopeOpenID, + oidc.ScopeProfile, + z_oidc.ScopeUserMetaData, + z_oidc.ScopeResourceOwner, + }, + AllowedUserType: domain.UserTypeMachine, + } + details, err := s.command.AddPersonalAccessToken(ctx, newPat) + if err != nil { + return nil, err + } + return connect.NewResponse(&user.AddPersonalAccessTokenResponse{ + CreationDate: timestamppb.New(details.EventDate), + TokenId: newPat.TokenID, + Token: newPat.Token, + }), nil +} + +func (s *Server) RemovePersonalAccessToken(ctx context.Context, req *connect.Request[user.RemovePersonalAccessTokenRequest]) (*connect.Response[user.RemovePersonalAccessTokenResponse], error) { + objectDetails, err := s.command.RemovePersonalAccessToken(ctx, &command.PersonalAccessToken{ + TokenID: req.Msg.GetTokenId(), + ObjectRoot: models.ObjectRoot{ + AggregateID: req.Msg.GetUserId(), + }, + PermissionCheck: s.command.NewPermissionCheckUserWrite(ctx), + }) + if err != nil { + return nil, err + } + return connect.NewResponse(&user.RemovePersonalAccessTokenResponse{ + DeletionDate: timestamppb.New(objectDetails.EventDate), + }), nil +} diff --git a/internal/api/grpc/user/v2/pat_query.go b/internal/api/grpc/user/v2/pat_query.go new file mode 100644 index 0000000000..64231c1d93 --- /dev/null +++ b/internal/api/grpc/user/v2/pat_query.go @@ -0,0 +1,124 @@ +package user + +import ( + "context" + + "connectrpc.com/connect" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/api/grpc/filter/v2" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/zerrors" + filter_pb "github.com/zitadel/zitadel/pkg/grpc/filter/v2" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +func (s *Server) ListPersonalAccessTokens(ctx context.Context, req *connect.Request[user.ListPersonalAccessTokensRequest]) (*connect.Response[user.ListPersonalAccessTokensResponse], error) { + offset, limit, asc, err := filter.PaginationPbToQuery(s.systemDefaults, req.Msg.GetPagination()) + if err != nil { + return nil, err + } + filters, err := patFiltersToQueries(req.Msg.GetFilters()) + if err != nil { + return nil, err + } + search := &query.PersonalAccessTokenSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + SortingColumn: authnPersonalAccessTokenFieldNameToSortingColumn(req.Msg.SortingColumn), + }, + Queries: filters, + } + result, err := s.query.SearchPersonalAccessTokens(ctx, search, s.checkPermission) + if err != nil { + return nil, err + } + resp := &user.ListPersonalAccessTokensResponse{ + Result: make([]*user.PersonalAccessToken, len(result.PersonalAccessTokens)), + Pagination: filter.QueryToPaginationPb(search.SearchRequest, result.SearchResponse), + } + for i, pat := range result.PersonalAccessTokens { + resp.Result[i] = &user.PersonalAccessToken{ + CreationDate: timestamppb.New(pat.CreationDate), + ChangeDate: timestamppb.New(pat.ChangeDate), + Id: pat.ID, + UserId: pat.UserID, + OrganizationId: pat.ResourceOwner, + ExpirationDate: timestamppb.New(pat.Expiration), + } + } + return connect.NewResponse(resp), nil +} + +func patFiltersToQueries(filters []*user.PersonalAccessTokensSearchFilter) (_ []query.SearchQuery, err error) { + q := make([]query.SearchQuery, len(filters)) + for i, filter := range filters { + q[i], err = patFilterToQuery(filter) + if err != nil { + return nil, err + } + } + return q, nil +} + +func patFilterToQuery(filter *user.PersonalAccessTokensSearchFilter) (query.SearchQuery, error) { + switch q := filter.Filter.(type) { + case *user.PersonalAccessTokensSearchFilter_CreatedDateFilter: + return authnPersonalAccessTokenCreatedFilterToQuery(q.CreatedDateFilter) + case *user.PersonalAccessTokensSearchFilter_ExpirationDateFilter: + return authnPersonalAccessTokenExpirationFilterToQuery(q.ExpirationDateFilter) + case *user.PersonalAccessTokensSearchFilter_TokenIdFilter: + return authnPersonalAccessTokenIdFilterToQuery(q.TokenIdFilter) + case *user.PersonalAccessTokensSearchFilter_UserIdFilter: + return authnPersonalAccessTokenUserIdFilterToQuery(q.UserIdFilter) + case *user.PersonalAccessTokensSearchFilter_OrganizationIdFilter: + return authnPersonalAccessTokenOrgIdFilterToQuery(q.OrganizationIdFilter) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "GRPC-vR9nC", "List.Query.Invalid") + } +} + +func authnPersonalAccessTokenIdFilterToQuery(f *filter_pb.IDFilter) (query.SearchQuery, error) { + return query.NewPersonalAccessTokenIDQuery(f.Id) +} + +func authnPersonalAccessTokenUserIdFilterToQuery(f *filter_pb.IDFilter) (query.SearchQuery, error) { + return query.NewPersonalAccessTokenUserIDSearchQuery(f.Id) +} + +func authnPersonalAccessTokenOrgIdFilterToQuery(f *filter_pb.IDFilter) (query.SearchQuery, error) { + return query.NewPersonalAccessTokenResourceOwnerSearchQuery(f.Id) +} + +func authnPersonalAccessTokenCreatedFilterToQuery(f *filter_pb.TimestampFilter) (query.SearchQuery, error) { + return query.NewPersonalAccessTokenCreationDateQuery(f.Timestamp.AsTime(), filter.TimestampMethodPbToQuery(f.Method)) +} + +func authnPersonalAccessTokenExpirationFilterToQuery(f *filter_pb.TimestampFilter) (query.SearchQuery, error) { + return query.NewPersonalAccessTokenExpirationDateDateQuery(f.Timestamp.AsTime(), filter.TimestampMethodPbToQuery(f.Method)) +} + +// authnPersonalAccessTokenFieldNameToSortingColumn defaults to the creation date because this ensures deterministic pagination +func authnPersonalAccessTokenFieldNameToSortingColumn(field *user.PersonalAccessTokenFieldName) query.Column { + if field == nil { + return query.PersonalAccessTokenColumnCreationDate + } + switch *field { + case user.PersonalAccessTokenFieldName_PERSONAL_ACCESS_TOKEN_FIELD_NAME_UNSPECIFIED: + return query.PersonalAccessTokenColumnCreationDate + case user.PersonalAccessTokenFieldName_PERSONAL_ACCESS_TOKEN_FIELD_NAME_ID: + return query.PersonalAccessTokenColumnID + case user.PersonalAccessTokenFieldName_PERSONAL_ACCESS_TOKEN_FIELD_NAME_USER_ID: + return query.PersonalAccessTokenColumnUserID + case user.PersonalAccessTokenFieldName_PERSONAL_ACCESS_TOKEN_FIELD_NAME_ORGANIZATION_ID: + return query.PersonalAccessTokenColumnResourceOwner + case user.PersonalAccessTokenFieldName_PERSONAL_ACCESS_TOKEN_FIELD_NAME_CREATED_DATE: + return query.PersonalAccessTokenColumnCreationDate + case user.PersonalAccessTokenFieldName_PERSONAL_ACCESS_TOKEN_FIELD_NAME_EXPIRATION_DATE: + return query.PersonalAccessTokenColumnExpiration + default: + return query.PersonalAccessTokenColumnCreationDate + } +} diff --git a/internal/api/grpc/user/v2/phone.go b/internal/api/grpc/user/v2/phone.go index fdd5a140c1..4be616f7ea 100644 --- a/internal/api/grpc/user/v2/phone.go +++ b/internal/api/grpc/user/v2/phone.go @@ -3,6 +3,7 @@ package user import ( "context" + "connectrpc.com/connect" "google.golang.org/protobuf/types/known/timestamppb" "github.com/zitadel/zitadel/internal/domain" @@ -11,18 +12,18 @@ import ( "github.com/zitadel/zitadel/pkg/grpc/user/v2" ) -func (s *Server) SetPhone(ctx context.Context, req *user.SetPhoneRequest) (resp *user.SetPhoneResponse, err error) { +func (s *Server) SetPhone(ctx context.Context, req *connect.Request[user.SetPhoneRequest]) (resp *connect.Response[user.SetPhoneResponse], err error) { var phone *domain.Phone - switch v := req.GetVerification().(type) { + switch v := req.Msg.GetVerification().(type) { case *user.SetPhoneRequest_SendCode: - phone, err = s.command.ChangeUserPhone(ctx, req.GetUserId(), req.GetPhone(), s.userCodeAlg) + phone, err = s.command.ChangeUserPhone(ctx, req.Msg.GetUserId(), req.Msg.GetPhone(), s.userCodeAlg) case *user.SetPhoneRequest_ReturnCode: - phone, err = s.command.ChangeUserPhoneReturnCode(ctx, req.GetUserId(), req.GetPhone(), s.userCodeAlg) + phone, err = s.command.ChangeUserPhoneReturnCode(ctx, req.Msg.GetUserId(), req.Msg.GetPhone(), s.userCodeAlg) case *user.SetPhoneRequest_IsVerified: - phone, err = s.command.ChangeUserPhoneVerified(ctx, req.GetUserId(), req.GetPhone()) + phone, err = s.command.ChangeUserPhoneVerified(ctx, req.Msg.GetUserId(), req.Msg.GetPhone()) case nil: - phone, err = s.command.ChangeUserPhone(ctx, req.GetUserId(), req.GetPhone(), s.userCodeAlg) + phone, err = s.command.ChangeUserPhone(ctx, req.Msg.GetUserId(), req.Msg.GetPhone(), s.userCodeAlg) default: err = zerrors.ThrowUnimplementedf(nil, "USERv2-Ahng0", "verification oneOf %T in method SetPhone not implemented", v) } @@ -30,42 +31,42 @@ func (s *Server) SetPhone(ctx context.Context, req *user.SetPhoneRequest) (resp return nil, err } - return &user.SetPhoneResponse{ + return connect.NewResponse(&user.SetPhoneResponse{ Details: &object.Details{ Sequence: phone.Sequence, ChangeDate: timestamppb.New(phone.ChangeDate), ResourceOwner: phone.ResourceOwner, }, VerificationCode: phone.PlainCode, - }, nil + }), nil } -func (s *Server) RemovePhone(ctx context.Context, req *user.RemovePhoneRequest) (resp *user.RemovePhoneResponse, err error) { +func (s *Server) RemovePhone(ctx context.Context, req *connect.Request[user.RemovePhoneRequest]) (resp *connect.Response[user.RemovePhoneResponse], err error) { details, err := s.command.RemoveUserPhone(ctx, - req.GetUserId(), + req.Msg.GetUserId(), ) if err != nil { return nil, err } - return &user.RemovePhoneResponse{ + return connect.NewResponse(&user.RemovePhoneResponse{ Details: &object.Details{ Sequence: details.Sequence, ChangeDate: timestamppb.New(details.EventDate), ResourceOwner: details.ResourceOwner, }, - }, nil + }), nil } -func (s *Server) ResendPhoneCode(ctx context.Context, req *user.ResendPhoneCodeRequest) (resp *user.ResendPhoneCodeResponse, err error) { +func (s *Server) ResendPhoneCode(ctx context.Context, req *connect.Request[user.ResendPhoneCodeRequest]) (resp *connect.Response[user.ResendPhoneCodeResponse], err error) { var phone *domain.Phone - switch v := req.GetVerification().(type) { + switch v := req.Msg.GetVerification().(type) { case *user.ResendPhoneCodeRequest_SendCode: - phone, err = s.command.ResendUserPhoneCode(ctx, req.GetUserId(), s.userCodeAlg) + phone, err = s.command.ResendUserPhoneCode(ctx, req.Msg.GetUserId(), s.userCodeAlg) case *user.ResendPhoneCodeRequest_ReturnCode: - phone, err = s.command.ResendUserPhoneCodeReturnCode(ctx, req.GetUserId(), s.userCodeAlg) + phone, err = s.command.ResendUserPhoneCodeReturnCode(ctx, req.Msg.GetUserId(), s.userCodeAlg) case nil: - phone, err = s.command.ResendUserPhoneCode(ctx, req.GetUserId(), s.userCodeAlg) + phone, err = s.command.ResendUserPhoneCode(ctx, req.Msg.GetUserId(), s.userCodeAlg) default: err = zerrors.ThrowUnimplementedf(nil, "USERv2-ResendUserPhoneCode", "verification oneOf %T in method SetPhone not implemented", v) } @@ -73,30 +74,30 @@ func (s *Server) ResendPhoneCode(ctx context.Context, req *user.ResendPhoneCodeR return nil, err } - return &user.ResendPhoneCodeResponse{ + return connect.NewResponse(&user.ResendPhoneCodeResponse{ Details: &object.Details{ Sequence: phone.Sequence, ChangeDate: timestamppb.New(phone.ChangeDate), ResourceOwner: phone.ResourceOwner, }, VerificationCode: phone.PlainCode, - }, nil + }), nil } -func (s *Server) VerifyPhone(ctx context.Context, req *user.VerifyPhoneRequest) (*user.VerifyPhoneResponse, error) { +func (s *Server) VerifyPhone(ctx context.Context, req *connect.Request[user.VerifyPhoneRequest]) (*connect.Response[user.VerifyPhoneResponse], error) { details, err := s.command.VerifyUserPhone(ctx, - req.GetUserId(), - req.GetVerificationCode(), + req.Msg.GetUserId(), + req.Msg.GetVerificationCode(), s.userCodeAlg, ) if err != nil { return nil, err } - return &user.VerifyPhoneResponse{ + return connect.NewResponse(&user.VerifyPhoneResponse{ Details: &object.Details{ Sequence: details.Sequence, ChangeDate: timestamppb.New(details.EventDate), ResourceOwner: details.ResourceOwner, }, - }, nil + }), nil } diff --git a/internal/api/grpc/user/v2/secret.go b/internal/api/grpc/user/v2/secret.go new file mode 100644 index 0000000000..acc7aef8cb --- /dev/null +++ b/internal/api/grpc/user/v2/secret.go @@ -0,0 +1,40 @@ +package user + +import ( + "context" + + "connectrpc.com/connect" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/pkg/grpc/user/v2" +) + +func (s *Server) AddSecret(ctx context.Context, req *connect.Request[user.AddSecretRequest]) (*connect.Response[user.AddSecretResponse], error) { + newSecret := &command.GenerateMachineSecret{ + PermissionCheck: s.command.NewPermissionCheckUserWrite(ctx), + } + details, err := s.command.GenerateMachineSecret(ctx, req.Msg.GetUserId(), "", newSecret) + if err != nil { + return nil, err + } + return connect.NewResponse(&user.AddSecretResponse{ + CreationDate: timestamppb.New(details.EventDate), + ClientSecret: newSecret.ClientSecret, + }), nil +} + +func (s *Server) RemoveSecret(ctx context.Context, req *connect.Request[user.RemoveSecretRequest]) (*connect.Response[user.RemoveSecretResponse], error) { + details, err := s.command.RemoveMachineSecret( + ctx, + req.Msg.GetUserId(), + "", + s.command.NewPermissionCheckUserWrite(ctx), + ) + if err != nil { + return nil, err + } + return connect.NewResponse(&user.RemoveSecretResponse{ + DeletionDate: timestamppb.New(details.EventDate), + }), nil +} diff --git a/internal/api/grpc/user/v2/server.go b/internal/api/grpc/user/v2/server.go index 9272ea27ee..e89d0a7d60 100644 --- a/internal/api/grpc/user/v2/server.go +++ b/internal/api/grpc/user/v2/server.go @@ -2,28 +2,32 @@ package user import ( "context" + "net/http" - "google.golang.org/grpc" + "connectrpc.com/connect" + "google.golang.org/protobuf/reflect/protoreflect" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/grpc/server" "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/config/systemdefaults" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/pkg/grpc/user/v2" + "github.com/zitadel/zitadel/pkg/grpc/user/v2/userconnect" ) -var _ user.UserServiceServer = (*Server)(nil) +var _ userconnect.UserServiceHandler = (*Server)(nil) type Server struct { - user.UnimplementedUserServiceServer - command *command.Commands - query *query.Queries - userCodeAlg crypto.EncryptionAlgorithm - idpAlg crypto.EncryptionAlgorithm - idpCallback func(ctx context.Context) string - samlRootURL func(ctx context.Context, idpID string) string + systemDefaults systemdefaults.SystemDefaults + command *command.Commands + query *query.Queries + userCodeAlg crypto.EncryptionAlgorithm + idpAlg crypto.EncryptionAlgorithm + idpCallback func(ctx context.Context) string + samlRootURL func(ctx context.Context, idpID string) string assetAPIPrefix func(context.Context) string @@ -35,6 +39,7 @@ type Config struct{} func CreateServer( command *command.Commands, query *query.Queries, + systemDefaults systemdefaults.SystemDefaults, userCodeAlg crypto.EncryptionAlgorithm, idpAlg crypto.EncryptionAlgorithm, idpCallback func(ctx context.Context) string, @@ -51,11 +56,16 @@ func CreateServer( samlRootURL: samlRootURL, assetAPIPrefix: assetAPIPrefix, checkPermission: checkPermission, + systemDefaults: systemDefaults, } } -func (s *Server) RegisterServer(grpcServer *grpc.Server) { - user.RegisterUserServiceServer(grpcServer, s) +func (s *Server) RegisterConnectServer(interceptors ...connect.Interceptor) (string, http.Handler) { + return userconnect.NewUserServiceHandler(s, connect.WithInterceptors(interceptors...)) +} + +func (s *Server) FileDescriptor() protoreflect.FileDescriptor { + return user.File_zitadel_user_v2_user_service_proto } func (s *Server) AppName() string { diff --git a/internal/api/grpc/user/v2/totp.go b/internal/api/grpc/user/v2/totp.go index 9e2d028d72..51b615dac5 100644 --- a/internal/api/grpc/user/v2/totp.go +++ b/internal/api/grpc/user/v2/totp.go @@ -3,42 +3,44 @@ package user import ( "context" + "connectrpc.com/connect" + "github.com/zitadel/zitadel/internal/api/grpc/object/v2" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/pkg/grpc/user/v2" ) -func (s *Server) RegisterTOTP(ctx context.Context, req *user.RegisterTOTPRequest) (*user.RegisterTOTPResponse, error) { +func (s *Server) RegisterTOTP(ctx context.Context, req *connect.Request[user.RegisterTOTPRequest]) (*connect.Response[user.RegisterTOTPResponse], error) { return totpDetailsToPb( - s.command.AddUserTOTP(ctx, req.GetUserId(), ""), + s.command.AddUserTOTP(ctx, req.Msg.GetUserId(), ""), ) } -func totpDetailsToPb(totp *domain.TOTP, err error) (*user.RegisterTOTPResponse, error) { +func totpDetailsToPb(totp *domain.TOTP, err error) (*connect.Response[user.RegisterTOTPResponse], error) { if err != nil { return nil, err } - return &user.RegisterTOTPResponse{ + return connect.NewResponse(&user.RegisterTOTPResponse{ Details: object.DomainToDetailsPb(totp.ObjectDetails), Uri: totp.URI, Secret: totp.Secret, - }, nil + }), nil } -func (s *Server) VerifyTOTPRegistration(ctx context.Context, req *user.VerifyTOTPRegistrationRequest) (*user.VerifyTOTPRegistrationResponse, error) { - objectDetails, err := s.command.CheckUserTOTP(ctx, req.GetUserId(), req.GetCode(), "") +func (s *Server) VerifyTOTPRegistration(ctx context.Context, req *connect.Request[user.VerifyTOTPRegistrationRequest]) (*connect.Response[user.VerifyTOTPRegistrationResponse], error) { + objectDetails, err := s.command.CheckUserTOTP(ctx, req.Msg.GetUserId(), req.Msg.GetCode(), "") if err != nil { return nil, err } - return &user.VerifyTOTPRegistrationResponse{ + return connect.NewResponse(&user.VerifyTOTPRegistrationResponse{ Details: object.DomainToDetailsPb(objectDetails), - }, nil + }), nil } -func (s *Server) RemoveTOTP(ctx context.Context, req *user.RemoveTOTPRequest) (*user.RemoveTOTPResponse, error) { - objectDetails, err := s.command.HumanRemoveTOTP(ctx, req.GetUserId(), "") +func (s *Server) RemoveTOTP(ctx context.Context, req *connect.Request[user.RemoveTOTPRequest]) (*connect.Response[user.RemoveTOTPResponse], error) { + objectDetails, err := s.command.HumanRemoveTOTP(ctx, req.Msg.GetUserId(), "") if err != nil { return nil, err } - return &user.RemoveTOTPResponse{Details: object.DomainToDetailsPb(objectDetails)}, nil + return connect.NewResponse(&user.RemoveTOTPResponse{Details: object.DomainToDetailsPb(objectDetails)}), nil } diff --git a/internal/api/grpc/user/v2/totp_test.go b/internal/api/grpc/user/v2/totp_test.go index 27ce6fb469..259f5ab5c6 100644 --- a/internal/api/grpc/user/v2/totp_test.go +++ b/internal/api/grpc/user/v2/totp_test.go @@ -63,7 +63,7 @@ func Test_totpDetailsToPb(t *testing.T) { t.Run(tt.name, func(t *testing.T) { got, err := totpDetailsToPb(tt.args.otp, tt.args.err) require.ErrorIs(t, err, tt.wantErr) - if !proto.Equal(tt.want, got) { + if tt.want != nil && !proto.Equal(tt.want, got.Msg) { t.Errorf("RegisterTOTPResponse =\n%v\nwant\n%v", got, tt.want) } }) diff --git a/internal/api/grpc/user/v2/u2f.go b/internal/api/grpc/user/v2/u2f.go index 60c0f5ab07..bd12ea0dac 100644 --- a/internal/api/grpc/user/v2/u2f.go +++ b/internal/api/grpc/user/v2/u2f.go @@ -3,50 +3,52 @@ package user import ( "context" + "connectrpc.com/connect" + "github.com/zitadel/zitadel/internal/api/grpc/object/v2" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/zerrors" "github.com/zitadel/zitadel/pkg/grpc/user/v2" ) -func (s *Server) RegisterU2F(ctx context.Context, req *user.RegisterU2FRequest) (*user.RegisterU2FResponse, error) { +func (s *Server) RegisterU2F(ctx context.Context, req *connect.Request[user.RegisterU2FRequest]) (*connect.Response[user.RegisterU2FResponse], error) { return u2fRegistrationDetailsToPb( - s.command.RegisterUserU2F(ctx, req.GetUserId(), "", req.GetDomain()), + s.command.RegisterUserU2F(ctx, req.Msg.GetUserId(), "", req.Msg.GetDomain()), ) } -func u2fRegistrationDetailsToPb(details *domain.WebAuthNRegistrationDetails, err error) (*user.RegisterU2FResponse, error) { +func u2fRegistrationDetailsToPb(details *domain.WebAuthNRegistrationDetails, err error) (*connect.Response[user.RegisterU2FResponse], error) { objectDetails, options, err := webAuthNRegistrationDetailsToPb(details, err) if err != nil { return nil, err } - return &user.RegisterU2FResponse{ + return connect.NewResponse(&user.RegisterU2FResponse{ Details: objectDetails, U2FId: details.ID, PublicKeyCredentialCreationOptions: options, - }, nil + }), nil } -func (s *Server) VerifyU2FRegistration(ctx context.Context, req *user.VerifyU2FRegistrationRequest) (*user.VerifyU2FRegistrationResponse, error) { - pkc, err := req.GetPublicKeyCredential().MarshalJSON() +func (s *Server) VerifyU2FRegistration(ctx context.Context, req *connect.Request[user.VerifyU2FRegistrationRequest]) (*connect.Response[user.VerifyU2FRegistrationResponse], error) { + pkc, err := req.Msg.GetPublicKeyCredential().MarshalJSON() if err != nil { return nil, zerrors.ThrowInternal(err, "USERv2-IeTh4", "Errors.Internal") } - objectDetails, err := s.command.HumanVerifyU2FSetup(ctx, req.GetUserId(), "", req.GetTokenName(), "", pkc) + objectDetails, err := s.command.HumanVerifyU2FSetup(ctx, req.Msg.GetUserId(), "", req.Msg.GetTokenName(), "", pkc) if err != nil { return nil, err } - return &user.VerifyU2FRegistrationResponse{ + return connect.NewResponse(&user.VerifyU2FRegistrationResponse{ Details: object.DomainToDetailsPb(objectDetails), - }, nil + }), nil } -func (s *Server) RemoveU2F(ctx context.Context, req *user.RemoveU2FRequest) (*user.RemoveU2FResponse, error) { - objectDetails, err := s.command.HumanRemoveU2F(ctx, req.GetUserId(), req.GetU2FId(), "") +func (s *Server) RemoveU2F(ctx context.Context, req *connect.Request[user.RemoveU2FRequest]) (*connect.Response[user.RemoveU2FResponse], error) { + objectDetails, err := s.command.HumanRemoveU2F(ctx, req.Msg.GetUserId(), req.Msg.GetU2FId(), "") if err != nil { return nil, err } - return &user.RemoveU2FResponse{ + return connect.NewResponse(&user.RemoveU2FResponse{ Details: object.DomainToDetailsPb(objectDetails), - }, nil + }), nil } diff --git a/internal/api/grpc/user/v2/u2f_test.go b/internal/api/grpc/user/v2/u2f_test.go index fae3ba1cdb..f6798a6f89 100644 --- a/internal/api/grpc/user/v2/u2f_test.go +++ b/internal/api/grpc/user/v2/u2f_test.go @@ -92,11 +92,11 @@ func Test_u2fRegistrationDetailsToPb(t *testing.T) { t.Run(tt.name, func(t *testing.T) { got, err := u2fRegistrationDetailsToPb(tt.args.details, tt.args.err) require.ErrorIs(t, err, tt.wantErr) - if !proto.Equal(tt.want, got) { + if tt.want != nil && !proto.Equal(tt.want, got.Msg) { t.Errorf("Not equal:\nExpected\n%s\nActual:%s", tt.want, got) } if tt.want != nil { - grpc.AllFieldsSet(t, got.ProtoReflect()) + grpc.AllFieldsSet(t, got.Msg.ProtoReflect()) } }) } diff --git a/internal/api/grpc/user/v2/user.go b/internal/api/grpc/user/v2/user.go index 0f958f0d40..3eeda8da5f 100644 --- a/internal/api/grpc/user/v2/user.go +++ b/internal/api/grpc/user/v2/user.go @@ -4,6 +4,7 @@ import ( "context" "io" + "connectrpc.com/connect" "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/api/authz" @@ -11,11 +12,12 @@ import ( "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/zerrors" "github.com/zitadel/zitadel/pkg/grpc/user/v2" ) -func (s *Server) AddHumanUser(ctx context.Context, req *user.AddHumanUserRequest) (_ *user.AddHumanUserResponse, err error) { - human, err := AddUserRequestToAddHuman(req) +func (s *Server) AddHumanUser(ctx context.Context, req *connect.Request[user.AddHumanUserRequest]) (_ *connect.Response[user.AddHumanUserResponse], err error) { + human, err := AddUserRequestToAddHuman(req.Msg) if err != nil { return nil, err } @@ -23,12 +25,12 @@ func (s *Server) AddHumanUser(ctx context.Context, req *user.AddHumanUserRequest if err = s.command.AddUserHuman(ctx, orgID, human, false, s.userCodeAlg); err != nil { return nil, err } - return &user.AddHumanUserResponse{ + return connect.NewResponse(&user.AddHumanUserResponse{ UserId: human.ID, Details: object.DomainToDetailsPb(human.Details), EmailCode: human.EmailCode, PhoneCode: human.PhoneCode, - }, nil + }), nil } func AddUserRequestToAddHuman(req *user.AddHumanUserRequest) (*command.AddHuman, error) { @@ -116,8 +118,8 @@ func genderToDomain(gender user.Gender) domain.Gender { } } -func (s *Server) UpdateHumanUser(ctx context.Context, req *user.UpdateHumanUserRequest) (_ *user.UpdateHumanUserResponse, err error) { - human, err := UpdateUserRequestToChangeHuman(req) +func (s *Server) UpdateHumanUser(ctx context.Context, req *connect.Request[user.UpdateHumanUserRequest]) (_ *connect.Response[user.UpdateHumanUserResponse], err error) { + human, err := updateHumanUserRequestToChangeHuman(req.Msg) if err != nil { return nil, err } @@ -125,51 +127,51 @@ func (s *Server) UpdateHumanUser(ctx context.Context, req *user.UpdateHumanUserR if err != nil { return nil, err } - return &user.UpdateHumanUserResponse{ + return connect.NewResponse(&user.UpdateHumanUserResponse{ Details: object.DomainToDetailsPb(human.Details), EmailCode: human.EmailCode, PhoneCode: human.PhoneCode, - }, nil + }), nil } -func (s *Server) LockUser(ctx context.Context, req *user.LockUserRequest) (_ *user.LockUserResponse, err error) { - details, err := s.command.LockUserV2(ctx, req.UserId) +func (s *Server) LockUser(ctx context.Context, req *connect.Request[user.LockUserRequest]) (_ *connect.Response[user.LockUserResponse], err error) { + details, err := s.command.LockUserV2(ctx, req.Msg.GetUserId()) if err != nil { return nil, err } - return &user.LockUserResponse{ + return connect.NewResponse(&user.LockUserResponse{ Details: object.DomainToDetailsPb(details), - }, nil + }), nil } -func (s *Server) UnlockUser(ctx context.Context, req *user.UnlockUserRequest) (_ *user.UnlockUserResponse, err error) { - details, err := s.command.UnlockUserV2(ctx, req.UserId) +func (s *Server) UnlockUser(ctx context.Context, req *connect.Request[user.UnlockUserRequest]) (_ *connect.Response[user.UnlockUserResponse], err error) { + details, err := s.command.UnlockUserV2(ctx, req.Msg.GetUserId()) if err != nil { return nil, err } - return &user.UnlockUserResponse{ + return connect.NewResponse(&user.UnlockUserResponse{ Details: object.DomainToDetailsPb(details), - }, nil + }), nil } -func (s *Server) DeactivateUser(ctx context.Context, req *user.DeactivateUserRequest) (_ *user.DeactivateUserResponse, err error) { - details, err := s.command.DeactivateUserV2(ctx, req.UserId) +func (s *Server) DeactivateUser(ctx context.Context, req *connect.Request[user.DeactivateUserRequest]) (_ *connect.Response[user.DeactivateUserResponse], err error) { + details, err := s.command.DeactivateUserV2(ctx, req.Msg.GetUserId()) if err != nil { return nil, err } - return &user.DeactivateUserResponse{ + return connect.NewResponse(&user.DeactivateUserResponse{ Details: object.DomainToDetailsPb(details), - }, nil + }), nil } -func (s *Server) ReactivateUser(ctx context.Context, req *user.ReactivateUserRequest) (_ *user.ReactivateUserResponse, err error) { - details, err := s.command.ReactivateUserV2(ctx, req.UserId) +func (s *Server) ReactivateUser(ctx context.Context, req *connect.Request[user.ReactivateUserRequest]) (_ *connect.Response[user.ReactivateUserResponse], err error) { + details, err := s.command.ReactivateUserV2(ctx, req.Msg.GetUserId()) if err != nil { return nil, err } - return &user.ReactivateUserResponse{ + return connect.NewResponse(&user.ReactivateUserResponse{ Details: object.DomainToDetailsPb(details), - }, nil + }), nil } func ifNotNilPtr[v, p any](value *v, conv func(v) p) *p { @@ -181,98 +183,18 @@ func ifNotNilPtr[v, p any](value *v, conv func(v) p) *p { return &pVal } -func UpdateUserRequestToChangeHuman(req *user.UpdateHumanUserRequest) (*command.ChangeHuman, error) { - email, err := SetHumanEmailToEmail(req.Email, req.GetUserId()) +func (s *Server) DeleteUser(ctx context.Context, req *connect.Request[user.DeleteUserRequest]) (_ *connect.Response[user.DeleteUserResponse], err error) { + memberships, grants, err := s.removeUserDependencies(ctx, req.Msg.GetUserId()) if err != nil { return nil, err } - return &command.ChangeHuman{ - ID: req.GetUserId(), - Username: req.Username, - Profile: SetHumanProfileToProfile(req.Profile), - Email: email, - Phone: SetHumanPhoneToPhone(req.Phone), - Password: SetHumanPasswordToPassword(req.Password), - }, nil -} - -func SetHumanProfileToProfile(profile *user.SetHumanProfile) *command.Profile { - if profile == nil { - return nil - } - var firstName *string - if profile.GivenName != "" { - firstName = &profile.GivenName - } - var lastName *string - if profile.FamilyName != "" { - lastName = &profile.FamilyName - } - return &command.Profile{ - FirstName: firstName, - LastName: lastName, - NickName: profile.NickName, - DisplayName: profile.DisplayName, - PreferredLanguage: ifNotNilPtr(profile.PreferredLanguage, language.Make), - Gender: ifNotNilPtr(profile.Gender, genderToDomain), - } -} - -func SetHumanEmailToEmail(email *user.SetHumanEmail, userID string) (*command.Email, error) { - if email == nil { - return nil, nil - } - var urlTemplate string - if email.GetSendCode() != nil && email.GetSendCode().UrlTemplate != nil { - urlTemplate = *email.GetSendCode().UrlTemplate - if err := domain.RenderConfirmURLTemplate(io.Discard, urlTemplate, userID, "code", "orgID"); err != nil { - return nil, err - } - } - return &command.Email{ - Address: domain.EmailAddress(email.Email), - Verified: email.GetIsVerified(), - ReturnCode: email.GetReturnCode() != nil, - URLTemplate: urlTemplate, - }, nil -} - -func SetHumanPhoneToPhone(phone *user.SetHumanPhone) *command.Phone { - if phone == nil { - return nil - } - return &command.Phone{ - Number: domain.PhoneNumber(phone.GetPhone()), - Verified: phone.GetIsVerified(), - ReturnCode: phone.GetReturnCode() != nil, - } -} - -func SetHumanPasswordToPassword(password *user.SetPassword) *command.Password { - if password == nil { - return nil - } - return &command.Password{ - PasswordCode: password.GetVerificationCode(), - OldPassword: password.GetCurrentPassword(), - Password: password.GetPassword().GetPassword(), - EncodedPasswordHash: password.GetHashedPassword().GetHash(), - ChangeRequired: password.GetPassword().GetChangeRequired() || password.GetHashedPassword().GetChangeRequired(), - } -} - -func (s *Server) DeleteUser(ctx context.Context, req *user.DeleteUserRequest) (_ *user.DeleteUserResponse, err error) { - memberships, grants, err := s.removeUserDependencies(ctx, req.GetUserId()) + details, err := s.command.RemoveUserV2(ctx, req.Msg.GetUserId(), "", memberships, grants...) if err != nil { return nil, err } - details, err := s.command.RemoveUserV2(ctx, req.UserId, "", memberships, grants...) - if err != nil { - return nil, err - } - return &user.DeleteUserResponse{ + return connect.NewResponse(&user.DeleteUserResponse{ Details: object.DomainToDetailsPb(details), - }, nil + }), nil } func (s *Server) removeUserDependencies(ctx context.Context, userID string) ([]*command.CascadingMembership, []string, error) { @@ -282,7 +204,7 @@ func (s *Server) removeUserDependencies(ctx context.Context, userID string) ([]* } grants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{ Queries: []query.SearchQuery{userGrantUserQuery}, - }, true) + }, true, nil) if err != nil { return nil, nil, err } @@ -347,35 +269,35 @@ func userGrantsToIDs(userGrants []*query.UserGrant) []string { return converted } -func (s *Server) ListAuthenticationMethodTypes(ctx context.Context, req *user.ListAuthenticationMethodTypesRequest) (*user.ListAuthenticationMethodTypesResponse, error) { - authMethods, err := s.query.ListUserAuthMethodTypes(ctx, req.GetUserId(), true, req.GetDomainQuery().GetIncludeWithoutDomain(), req.GetDomainQuery().GetDomain()) +func (s *Server) ListAuthenticationMethodTypes(ctx context.Context, req *connect.Request[user.ListAuthenticationMethodTypesRequest]) (*connect.Response[user.ListAuthenticationMethodTypesResponse], error) { + authMethods, err := s.query.ListUserAuthMethodTypes(ctx, req.Msg.GetUserId(), true, req.Msg.GetDomainQuery().GetIncludeWithoutDomain(), req.Msg.GetDomainQuery().GetDomain()) if err != nil { return nil, err } - return &user.ListAuthenticationMethodTypesResponse{ + return connect.NewResponse(&user.ListAuthenticationMethodTypesResponse{ Details: object.ToListDetails(authMethods.SearchResponse), AuthMethodTypes: authMethodTypesToPb(authMethods.AuthMethodTypes), - }, nil + }), nil } -func (s *Server) ListAuthenticationFactors(ctx context.Context, req *user.ListAuthenticationFactorsRequest) (*user.ListAuthenticationFactorsResponse, error) { +func (s *Server) ListAuthenticationFactors(ctx context.Context, req *connect.Request[user.ListAuthenticationFactorsRequest]) (*connect.Response[user.ListAuthenticationFactorsResponse], error) { query := new(query.UserAuthMethodSearchQueries) - if err := query.AppendUserIDQuery(req.UserId); err != nil { + if err := query.AppendUserIDQuery(req.Msg.GetUserId()); err != nil { return nil, err } authMethodsType := []domain.UserAuthMethodType{domain.UserAuthMethodTypeU2F, domain.UserAuthMethodTypeTOTP, domain.UserAuthMethodTypeOTPSMS, domain.UserAuthMethodTypeOTPEmail} - if len(req.GetAuthFactors()) > 0 { - authMethodsType = object.AuthFactorsToPb(req.GetAuthFactors()) + if len(req.Msg.GetAuthFactors()) > 0 { + authMethodsType = object.AuthFactorsToPb(req.Msg.GetAuthFactors()) } if err := query.AppendAuthMethodsQuery(authMethodsType...); err != nil { return nil, err } states := []domain.MFAState{domain.MFAStateReady} - if len(req.GetStates()) > 0 { - states = object.AuthFactorStatesToPb(req.GetStates()) + if len(req.Msg.GetStates()) > 0 { + states = object.AuthFactorStatesToPb(req.Msg.GetStates()) } if err := query.AppendStatesQuery(states...); err != nil { return nil, err @@ -386,9 +308,9 @@ func (s *Server) ListAuthenticationFactors(ctx context.Context, req *user.ListAu return nil, err } - return &user.ListAuthenticationFactorsResponse{ + return connect.NewResponse(&user.ListAuthenticationFactorsResponse{ Result: object.AuthMethodsToPb(authMethods), - }, nil + }), nil } func authMethodTypesToPb(methodTypes []domain.UserAuthMethodType) []user.AuthenticationMethodType { @@ -422,8 +344,8 @@ func authMethodTypeToPb(methodType domain.UserAuthMethodType) user.Authenticatio } } -func (s *Server) CreateInviteCode(ctx context.Context, req *user.CreateInviteCodeRequest) (*user.CreateInviteCodeResponse, error) { - invite, err := createInviteCodeRequestToCommand(req) +func (s *Server) CreateInviteCode(ctx context.Context, req *connect.Request[user.CreateInviteCodeRequest]) (*connect.Response[user.CreateInviteCodeResponse], error) { + invite, err := createInviteCodeRequestToCommand(req.Msg) if err != nil { return nil, err } @@ -431,30 +353,30 @@ func (s *Server) CreateInviteCode(ctx context.Context, req *user.CreateInviteCod if err != nil { return nil, err } - return &user.CreateInviteCodeResponse{ + return connect.NewResponse(&user.CreateInviteCodeResponse{ Details: object.DomainToDetailsPb(details), InviteCode: code, - }, nil + }), nil } -func (s *Server) ResendInviteCode(ctx context.Context, req *user.ResendInviteCodeRequest) (*user.ResendInviteCodeResponse, error) { - details, err := s.command.ResendInviteCode(ctx, req.GetUserId(), "", "") +func (s *Server) ResendInviteCode(ctx context.Context, req *connect.Request[user.ResendInviteCodeRequest]) (*connect.Response[user.ResendInviteCodeResponse], error) { + details, err := s.command.ResendInviteCode(ctx, req.Msg.GetUserId(), "", "") if err != nil { return nil, err } - return &user.ResendInviteCodeResponse{ + return connect.NewResponse(&user.ResendInviteCodeResponse{ Details: object.DomainToDetailsPb(details), - }, nil + }), nil } -func (s *Server) VerifyInviteCode(ctx context.Context, req *user.VerifyInviteCodeRequest) (*user.VerifyInviteCodeResponse, error) { - details, err := s.command.VerifyInviteCode(ctx, req.GetUserId(), req.GetVerificationCode()) +func (s *Server) VerifyInviteCode(ctx context.Context, req *connect.Request[user.VerifyInviteCodeRequest]) (*connect.Response[user.VerifyInviteCodeResponse], error) { + details, err := s.command.VerifyInviteCode(ctx, req.Msg.GetUserId(), req.Msg.GetVerificationCode()) if err != nil { return nil, err } - return &user.VerifyInviteCodeResponse{ + return connect.NewResponse(&user.VerifyInviteCodeResponse{ Details: object.DomainToDetailsPb(details), - }, nil + }), nil } func createInviteCodeRequestToCommand(req *user.CreateInviteCodeRequest) (*command.CreateUserInvite, error) { @@ -473,12 +395,34 @@ func createInviteCodeRequestToCommand(req *user.CreateInviteCodeRequest) (*comma } } -func (s *Server) HumanMFAInitSkipped(ctx context.Context, req *user.HumanMFAInitSkippedRequest) (_ *user.HumanMFAInitSkippedResponse, err error) { - details, err := s.command.HumanMFAInitSkippedV2(ctx, req.UserId) +func (s *Server) HumanMFAInitSkipped(ctx context.Context, req *connect.Request[user.HumanMFAInitSkippedRequest]) (_ *connect.Response[user.HumanMFAInitSkippedResponse], err error) { + details, err := s.command.HumanMFAInitSkippedV2(ctx, req.Msg.GetUserId()) if err != nil { return nil, err } - return &user.HumanMFAInitSkippedResponse{ + return connect.NewResponse(&user.HumanMFAInitSkippedResponse{ Details: object.DomainToDetailsPb(details), - }, nil + }), nil +} + +func (s *Server) CreateUser(ctx context.Context, req *connect.Request[user.CreateUserRequest]) (*connect.Response[user.CreateUserResponse], error) { + switch userType := req.Msg.GetUserType().(type) { + case *user.CreateUserRequest_Human_: + return s.createUserTypeHuman(ctx, userType.Human, req.Msg.GetOrganizationId(), req.Msg.Username, req.Msg.UserId) + case *user.CreateUserRequest_Machine_: + return s.createUserTypeMachine(ctx, userType.Machine, req.Msg.GetOrganizationId(), req.Msg.GetUsername(), req.Msg.GetUserId()) + default: + return nil, zerrors.ThrowInternal(nil, "", "user type is not implemented") + } +} + +func (s *Server) UpdateUser(ctx context.Context, req *connect.Request[user.UpdateUserRequest]) (*connect.Response[user.UpdateUserResponse], error) { + switch userType := req.Msg.GetUserType().(type) { + case *user.UpdateUserRequest_Human_: + return s.updateUserTypeHuman(ctx, userType.Human, req.Msg.GetUserId(), req.Msg.Username) + case *user.UpdateUserRequest_Machine_: + return s.updateUserTypeMachine(ctx, userType.Machine, req.Msg.GetUserId(), req.Msg.Username) + default: + return nil, zerrors.ThrowUnimplemented(nil, "", "user type is not implemented") + } } diff --git a/internal/api/grpc/user/v2/query.go b/internal/api/grpc/user/v2/user_query.go similarity index 90% rename from internal/api/grpc/user/v2/query.go rename to internal/api/grpc/user/v2/user_query.go index e65551d1c0..5f5603af31 100644 --- a/internal/api/grpc/user/v2/query.go +++ b/internal/api/grpc/user/v2/user_query.go @@ -3,6 +3,7 @@ package user import ( "context" + "connectrpc.com/connect" "github.com/muhlemmer/gu" "google.golang.org/protobuf/types/known/timestamppb" @@ -13,12 +14,12 @@ import ( "github.com/zitadel/zitadel/pkg/grpc/user/v2" ) -func (s *Server) GetUserByID(ctx context.Context, req *user.GetUserByIDRequest) (_ *user.GetUserByIDResponse, err error) { - resp, err := s.query.GetUserByIDWithPermission(ctx, true, req.GetUserId(), s.checkPermission) +func (s *Server) GetUserByID(ctx context.Context, req *connect.Request[user.GetUserByIDRequest]) (_ *connect.Response[user.GetUserByIDResponse], err error) { + resp, err := s.query.GetUserByIDWithPermission(ctx, true, req.Msg.GetUserId(), s.checkPermission) if err != nil { return nil, err } - return &user.GetUserByIDResponse{ + return connect.NewResponse(&user.GetUserByIDResponse{ Details: object.DomainToDetailsPb(&domain.ObjectDetails{ Sequence: resp.Sequence, CreationDate: resp.CreationDate, @@ -26,22 +27,22 @@ func (s *Server) GetUserByID(ctx context.Context, req *user.GetUserByIDRequest) ResourceOwner: resp.ResourceOwner, }), User: userToPb(resp, s.assetAPIPrefix(ctx)), - }, nil + }), nil } -func (s *Server) ListUsers(ctx context.Context, req *user.ListUsersRequest) (*user.ListUsersResponse, error) { - queries, filterOrgId, err := listUsersRequestToModel(req) +func (s *Server) ListUsers(ctx context.Context, req *connect.Request[user.ListUsersRequest]) (*connect.Response[user.ListUsersResponse], error) { + queries, err := listUsersRequestToModel(req.Msg) if err != nil { return nil, err } - res, err := s.query.SearchUsers(ctx, queries, filterOrgId, s.checkPermission) + res, err := s.query.SearchUsers(ctx, queries, s.checkPermission) if err != nil { return nil, err } - return &user.ListUsersResponse{ + return connect.NewResponse(&user.ListUsersResponse{ Result: UsersToPb(res.Users, s.assetAPIPrefix(ctx)), Details: object.ToListDetails(res.SearchResponse), - }, nil + }), nil } func UsersToPb(users []*query.User, assetPrefix string) []*user.User { @@ -171,11 +172,11 @@ func accessTokenTypeToPb(accessTokenType domain.OIDCTokenType) user.AccessTokenT } } -func listUsersRequestToModel(req *user.ListUsersRequest) (*query.UserSearchQueries, string, error) { +func listUsersRequestToModel(req *user.ListUsersRequest) (*query.UserSearchQueries, error) { offset, limit, asc := object.ListQueryToQuery(req.Query) - queries, filterOrgId, err := userQueriesToQuery(req.Queries, 0 /*start from level 0*/) + queries, err := userQueriesToQuery(req.Queries, 0 /*start from level 0*/) if err != nil { - return nil, "", err + return nil, err } return &query.UserSearchQueries{ SearchRequest: query.SearchRequest{ @@ -185,7 +186,7 @@ func listUsersRequestToModel(req *user.ListUsersRequest) (*query.UserSearchQueri SortingColumn: userFieldNameToSortingColumn(req.SortingColumn), }, Queries: queries, - }, filterOrgId, nil + }, nil } func userFieldNameToSortingColumn(field user.UserFieldName) query.Column { @@ -215,18 +216,15 @@ func userFieldNameToSortingColumn(field user.UserFieldName) query.Column { } } -func userQueriesToQuery(queries []*user.SearchQuery, level uint8) (_ []query.SearchQuery, filterOrgId string, err error) { +func userQueriesToQuery(queries []*user.SearchQuery, level uint8) (_ []query.SearchQuery, err error) { q := make([]query.SearchQuery, len(queries)) for i, query := range queries { - if orgFilter := query.GetOrganizationIdQuery(); orgFilter != nil { - filterOrgId = orgFilter.OrganizationId - } q[i], err = userQueryToQuery(query, level) if err != nil { - return nil, filterOrgId, err + return nil, err } } - return q, filterOrgId, nil + return q, nil } func userQueryToQuery(query *user.SearchQuery, level uint8) (query.SearchQuery, error) { @@ -320,14 +318,14 @@ func inUserIdsQueryToQuery(q *user.InUserIDQuery) (query.SearchQuery, error) { return query.NewUserInUserIdsSearchQuery(q.UserIds) } func orQueryToQuery(q *user.OrQuery, level uint8) (query.SearchQuery, error) { - mappedQueries, _, err := userQueriesToQuery(q.Queries, level+1) + mappedQueries, err := userQueriesToQuery(q.Queries, level+1) if err != nil { return nil, err } return query.NewUserOrSearchQuery(mappedQueries) } func andQueryToQuery(q *user.AndQuery, level uint8) (query.SearchQuery, error) { - mappedQueries, _, err := userQueriesToQuery(q.Queries, level+1) + mappedQueries, err := userQueriesToQuery(q.Queries, level+1) if err != nil { return nil, err } diff --git a/internal/api/grpc/user/v2beta/email.go b/internal/api/grpc/user/v2beta/email.go index 38cc73c75c..474111f767 100644 --- a/internal/api/grpc/user/v2beta/email.go +++ b/internal/api/grpc/user/v2beta/email.go @@ -3,6 +3,7 @@ package user import ( "context" + "connectrpc.com/connect" "google.golang.org/protobuf/types/known/timestamppb" "github.com/zitadel/zitadel/internal/domain" @@ -11,18 +12,18 @@ import ( user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" ) -func (s *Server) SetEmail(ctx context.Context, req *user.SetEmailRequest) (resp *user.SetEmailResponse, err error) { +func (s *Server) SetEmail(ctx context.Context, req *connect.Request[user.SetEmailRequest]) (resp *connect.Response[user.SetEmailResponse], err error) { var email *domain.Email - switch v := req.GetVerification().(type) { + switch v := req.Msg.GetVerification().(type) { case *user.SetEmailRequest_SendCode: - email, err = s.command.ChangeUserEmailURLTemplate(ctx, req.GetUserId(), req.GetEmail(), s.userCodeAlg, v.SendCode.GetUrlTemplate()) + email, err = s.command.ChangeUserEmailURLTemplate(ctx, req.Msg.GetUserId(), req.Msg.GetEmail(), s.userCodeAlg, v.SendCode.GetUrlTemplate()) case *user.SetEmailRequest_ReturnCode: - email, err = s.command.ChangeUserEmailReturnCode(ctx, req.GetUserId(), req.GetEmail(), s.userCodeAlg) + email, err = s.command.ChangeUserEmailReturnCode(ctx, req.Msg.GetUserId(), req.Msg.GetEmail(), s.userCodeAlg) case *user.SetEmailRequest_IsVerified: - email, err = s.command.ChangeUserEmailVerified(ctx, req.GetUserId(), req.GetEmail()) + email, err = s.command.ChangeUserEmailVerified(ctx, req.Msg.GetUserId(), req.Msg.GetEmail()) case nil: - email, err = s.command.ChangeUserEmail(ctx, req.GetUserId(), req.GetEmail(), s.userCodeAlg) + email, err = s.command.ChangeUserEmail(ctx, req.Msg.GetUserId(), req.Msg.GetEmail(), s.userCodeAlg) default: err = zerrors.ThrowUnimplementedf(nil, "USERv2-Ahng0", "verification oneOf %T in method SetEmail not implemented", v) } @@ -30,26 +31,26 @@ func (s *Server) SetEmail(ctx context.Context, req *user.SetEmailRequest) (resp return nil, err } - return &user.SetEmailResponse{ + return connect.NewResponse(&user.SetEmailResponse{ Details: &object.Details{ Sequence: email.Sequence, ChangeDate: timestamppb.New(email.ChangeDate), ResourceOwner: email.ResourceOwner, }, VerificationCode: email.PlainCode, - }, nil + }), nil } -func (s *Server) ResendEmailCode(ctx context.Context, req *user.ResendEmailCodeRequest) (resp *user.ResendEmailCodeResponse, err error) { +func (s *Server) ResendEmailCode(ctx context.Context, req *connect.Request[user.ResendEmailCodeRequest]) (resp *connect.Response[user.ResendEmailCodeResponse], err error) { var email *domain.Email - switch v := req.GetVerification().(type) { + switch v := req.Msg.GetVerification().(type) { case *user.ResendEmailCodeRequest_SendCode: - email, err = s.command.ResendUserEmailCodeURLTemplate(ctx, req.GetUserId(), s.userCodeAlg, v.SendCode.GetUrlTemplate()) + email, err = s.command.ResendUserEmailCodeURLTemplate(ctx, req.Msg.GetUserId(), s.userCodeAlg, v.SendCode.GetUrlTemplate()) case *user.ResendEmailCodeRequest_ReturnCode: - email, err = s.command.ResendUserEmailReturnCode(ctx, req.GetUserId(), s.userCodeAlg) + email, err = s.command.ResendUserEmailReturnCode(ctx, req.Msg.GetUserId(), s.userCodeAlg) case nil: - email, err = s.command.ResendUserEmailCode(ctx, req.GetUserId(), s.userCodeAlg) + email, err = s.command.ResendUserEmailCode(ctx, req.Msg.GetUserId(), s.userCodeAlg) default: err = zerrors.ThrowUnimplementedf(nil, "USERv2-faj0l0nj5x", "verification oneOf %T in method ResendEmailCode not implemented", v) } @@ -57,30 +58,30 @@ func (s *Server) ResendEmailCode(ctx context.Context, req *user.ResendEmailCodeR return nil, err } - return &user.ResendEmailCodeResponse{ + return connect.NewResponse(&user.ResendEmailCodeResponse{ Details: &object.Details{ Sequence: email.Sequence, ChangeDate: timestamppb.New(email.ChangeDate), ResourceOwner: email.ResourceOwner, }, VerificationCode: email.PlainCode, - }, nil + }), nil } -func (s *Server) VerifyEmail(ctx context.Context, req *user.VerifyEmailRequest) (*user.VerifyEmailResponse, error) { +func (s *Server) VerifyEmail(ctx context.Context, req *connect.Request[user.VerifyEmailRequest]) (*connect.Response[user.VerifyEmailResponse], error) { details, err := s.command.VerifyUserEmail(ctx, - req.GetUserId(), - req.GetVerificationCode(), + req.Msg.GetUserId(), + req.Msg.GetVerificationCode(), s.userCodeAlg, ) if err != nil { return nil, err } - return &user.VerifyEmailResponse{ + return connect.NewResponse(&user.VerifyEmailResponse{ Details: &object.Details{ Sequence: details.Sequence, ChangeDate: timestamppb.New(details.EventDate), ResourceOwner: details.ResourceOwner, }, - }, nil + }), nil } diff --git a/internal/api/grpc/user/v2beta/integration_test/user_test.go b/internal/api/grpc/user/v2beta/integration_test/user_test.go index 85e8ea65b4..dd75986a3f 100644 --- a/internal/api/grpc/user/v2beta/integration_test/user_test.go +++ b/internal/api/grpc/user/v2beta/integration_test/user_test.go @@ -1708,12 +1708,11 @@ func TestServer_ReactivateUser(t *testing.T) { } func TestServer_DeleteUser(t *testing.T) { - projectResp, err := Instance.CreateProject(CTX) - require.NoError(t, err) + projectResp := Instance.CreateProject(CTX, t, "", gofakeit.AppName(), false, false) type args struct { ctx context.Context req *user.DeleteUserRequest - prepare func(request *user.DeleteUserRequest) error + prepare func(request *user.DeleteUserRequest) } tests := []struct { name string @@ -1728,7 +1727,7 @@ func TestServer_DeleteUser(t *testing.T) { &user.DeleteUserRequest{ UserId: "notexisting", }, - func(request *user.DeleteUserRequest) error { return nil }, + nil, }, wantErr: true, }, @@ -1737,10 +1736,9 @@ func TestServer_DeleteUser(t *testing.T) { args: args{ ctx: CTX, req: &user.DeleteUserRequest{}, - prepare: func(request *user.DeleteUserRequest) error { + prepare: func(request *user.DeleteUserRequest) { resp := Instance.CreateHumanUser(CTX) request.UserId = resp.GetUserId() - return err }, }, want: &user.DeleteUserResponse{ @@ -1755,10 +1753,9 @@ func TestServer_DeleteUser(t *testing.T) { args: args{ ctx: CTX, req: &user.DeleteUserRequest{}, - prepare: func(request *user.DeleteUserRequest) error { + prepare: func(request *user.DeleteUserRequest) { resp := Instance.CreateMachineUser(CTX) request.UserId = resp.GetUserId() - return err }, }, want: &user.DeleteUserResponse{ @@ -1773,13 +1770,12 @@ func TestServer_DeleteUser(t *testing.T) { args: args{ ctx: CTX, req: &user.DeleteUserRequest{}, - prepare: func(request *user.DeleteUserRequest) error { + prepare: func(request *user.DeleteUserRequest) { resp := Instance.CreateHumanUser(CTX) request.UserId = resp.GetUserId() Instance.CreateProjectUserGrant(t, CTX, projectResp.GetId(), request.UserId) Instance.CreateProjectMembership(t, CTX, projectResp.GetId(), request.UserId) - Instance.CreateOrgMembership(t, CTX, request.UserId) - return err + Instance.CreateOrgMembership(t, CTX, Instance.DefaultOrg.Id, request.UserId) }, }, want: &user.DeleteUserResponse{ @@ -1792,8 +1788,9 @@ func TestServer_DeleteUser(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := tt.args.prepare(tt.args.req) - require.NoError(t, err) + if tt.args.prepare != nil { + tt.args.prepare(tt.args.req) + } got, err := Client.DeleteUser(tt.args.ctx, tt.args.req) if tt.wantErr { @@ -2063,7 +2060,7 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) { ChangeDate: timestamppb.Now(), ResourceOwner: Instance.ID(), }, - url: "http://" + Instance.Domain + ":8000/sso", + url: "http://localhost:8000/sso", parametersExisting: []string{"RelayState", "SAMLRequest"}, }, wantErr: false, @@ -2087,7 +2084,7 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) { ChangeDate: timestamppb.Now(), ResourceOwner: Instance.ID(), }, - url: "http://" + Instance.Domain + ":8000/sso", + url: "http://localhost:8000/sso", parametersExisting: []string{"RelayState", "SAMLRequest"}, }, wantErr: false, @@ -2111,7 +2108,9 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) { ChangeDate: timestamppb.Now(), ResourceOwner: Instance.ID(), }, - postForm: true, + url: "http://localhost:8000/sso", + parametersExisting: []string{"RelayState", "SAMLRequest"}, + postForm: true, }, wantErr: false, }, @@ -2125,9 +2124,11 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) { } require.NoError(t, err) - if tt.want.url != "" { + if tt.want.url != "" && !tt.want.postForm { authUrl, err := url.Parse(got.GetAuthUrl()) require.NoError(t, err) + + assert.Equal(t, tt.want.url, authUrl.Scheme+"://"+authUrl.Host+authUrl.Path) require.Len(t, authUrl.Query(), len(tt.want.parametersEqual)+len(tt.want.parametersExisting)) for _, existing := range tt.want.parametersExisting { @@ -2138,7 +2139,15 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) { } } if tt.want.postForm { - assert.NotEmpty(t, got.GetPostForm()) + assert.Equal(t, tt.want.url, got.GetFormData().GetUrl()) + + require.Len(t, got.GetFormData().GetFields(), len(tt.want.parametersEqual)+len(tt.want.parametersExisting)) + for _, existing := range tt.want.parametersExisting { + assert.Contains(t, got.GetFormData().GetFields(), existing) + } + for key, equal := range tt.want.parametersEqual { + assert.Equal(t, got.GetFormData().GetFields()[key], equal) + } } integration.AssertDetails(t, &user.StartIdentityProviderIntentResponse{ Details: tt.want.details, diff --git a/internal/api/grpc/user/v2beta/otp.go b/internal/api/grpc/user/v2beta/otp.go index c11aa4c1a4..99919ce047 100644 --- a/internal/api/grpc/user/v2beta/otp.go +++ b/internal/api/grpc/user/v2beta/otp.go @@ -3,40 +3,42 @@ package user import ( "context" + "connectrpc.com/connect" + object "github.com/zitadel/zitadel/internal/api/grpc/object/v2beta" user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" ) -func (s *Server) AddOTPSMS(ctx context.Context, req *user.AddOTPSMSRequest) (*user.AddOTPSMSResponse, error) { - details, err := s.command.AddHumanOTPSMS(ctx, req.GetUserId(), "") +func (s *Server) AddOTPSMS(ctx context.Context, req *connect.Request[user.AddOTPSMSRequest]) (*connect.Response[user.AddOTPSMSResponse], error) { + details, err := s.command.AddHumanOTPSMS(ctx, req.Msg.GetUserId(), "") if err != nil { return nil, err } - return &user.AddOTPSMSResponse{Details: object.DomainToDetailsPb(details)}, nil + return connect.NewResponse(&user.AddOTPSMSResponse{Details: object.DomainToDetailsPb(details)}), nil } -func (s *Server) RemoveOTPSMS(ctx context.Context, req *user.RemoveOTPSMSRequest) (*user.RemoveOTPSMSResponse, error) { - objectDetails, err := s.command.RemoveHumanOTPSMS(ctx, req.GetUserId(), "") +func (s *Server) RemoveOTPSMS(ctx context.Context, req *connect.Request[user.RemoveOTPSMSRequest]) (*connect.Response[user.RemoveOTPSMSResponse], error) { + objectDetails, err := s.command.RemoveHumanOTPSMS(ctx, req.Msg.GetUserId(), "") if err != nil { return nil, err } - return &user.RemoveOTPSMSResponse{Details: object.DomainToDetailsPb(objectDetails)}, nil + return connect.NewResponse(&user.RemoveOTPSMSResponse{Details: object.DomainToDetailsPb(objectDetails)}), nil } -func (s *Server) AddOTPEmail(ctx context.Context, req *user.AddOTPEmailRequest) (*user.AddOTPEmailResponse, error) { - details, err := s.command.AddHumanOTPEmail(ctx, req.GetUserId(), "") +func (s *Server) AddOTPEmail(ctx context.Context, req *connect.Request[user.AddOTPEmailRequest]) (*connect.Response[user.AddOTPEmailResponse], error) { + details, err := s.command.AddHumanOTPEmail(ctx, req.Msg.GetUserId(), "") if err != nil { return nil, err } - return &user.AddOTPEmailResponse{Details: object.DomainToDetailsPb(details)}, nil + return connect.NewResponse(&user.AddOTPEmailResponse{Details: object.DomainToDetailsPb(details)}), nil } -func (s *Server) RemoveOTPEmail(ctx context.Context, req *user.RemoveOTPEmailRequest) (*user.RemoveOTPEmailResponse, error) { - objectDetails, err := s.command.RemoveHumanOTPEmail(ctx, req.GetUserId(), "") +func (s *Server) RemoveOTPEmail(ctx context.Context, req *connect.Request[user.RemoveOTPEmailRequest]) (*connect.Response[user.RemoveOTPEmailResponse], error) { + objectDetails, err := s.command.RemoveHumanOTPEmail(ctx, req.Msg.GetUserId(), "") if err != nil { return nil, err } - return &user.RemoveOTPEmailResponse{Details: object.DomainToDetailsPb(objectDetails)}, nil + return connect.NewResponse(&user.RemoveOTPEmailResponse{Details: object.DomainToDetailsPb(objectDetails)}), nil } diff --git a/internal/api/grpc/user/v2beta/passkey.go b/internal/api/grpc/user/v2beta/passkey.go index 2df267f3fd..a63ac708b4 100644 --- a/internal/api/grpc/user/v2beta/passkey.go +++ b/internal/api/grpc/user/v2beta/passkey.go @@ -3,6 +3,7 @@ package user import ( "context" + "connectrpc.com/connect" "google.golang.org/protobuf/types/known/structpb" object "github.com/zitadel/zitadel/internal/api/grpc/object/v2beta" @@ -12,17 +13,17 @@ import ( user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" ) -func (s *Server) RegisterPasskey(ctx context.Context, req *user.RegisterPasskeyRequest) (resp *user.RegisterPasskeyResponse, err error) { +func (s *Server) RegisterPasskey(ctx context.Context, req *connect.Request[user.RegisterPasskeyRequest]) (resp *connect.Response[user.RegisterPasskeyResponse], err error) { var ( - authenticator = passkeyAuthenticatorToDomain(req.GetAuthenticator()) + authenticator = passkeyAuthenticatorToDomain(req.Msg.GetAuthenticator()) ) - if code := req.GetCode(); code != nil { + if code := req.Msg.GetCode(); code != nil { return passkeyRegistrationDetailsToPb( - s.command.RegisterUserPasskeyWithCode(ctx, req.GetUserId(), "", authenticator, code.Id, code.Code, req.GetDomain(), s.userCodeAlg), + s.command.RegisterUserPasskeyWithCode(ctx, req.Msg.GetUserId(), "", authenticator, code.Id, code.Code, req.Msg.GetDomain(), s.userCodeAlg), ) } return passkeyRegistrationDetailsToPb( - s.command.RegisterUserPasskey(ctx, req.GetUserId(), "", req.GetDomain(), authenticator), + s.command.RegisterUserPasskey(ctx, req.Msg.GetUserId(), "", req.Msg.GetDomain(), authenticator), ) } @@ -50,69 +51,69 @@ func webAuthNRegistrationDetailsToPb(details *domain.WebAuthNRegistrationDetails return object.DomainToDetailsPb(details.ObjectDetails), options, nil } -func passkeyRegistrationDetailsToPb(details *domain.WebAuthNRegistrationDetails, err error) (*user.RegisterPasskeyResponse, error) { +func passkeyRegistrationDetailsToPb(details *domain.WebAuthNRegistrationDetails, err error) (*connect.Response[user.RegisterPasskeyResponse], error) { objectDetails, options, err := webAuthNRegistrationDetailsToPb(details, err) if err != nil { return nil, err } - return &user.RegisterPasskeyResponse{ + return connect.NewResponse(&user.RegisterPasskeyResponse{ Details: objectDetails, PasskeyId: details.ID, PublicKeyCredentialCreationOptions: options, - }, nil + }), nil } -func (s *Server) VerifyPasskeyRegistration(ctx context.Context, req *user.VerifyPasskeyRegistrationRequest) (*user.VerifyPasskeyRegistrationResponse, error) { - pkc, err := req.GetPublicKeyCredential().MarshalJSON() +func (s *Server) VerifyPasskeyRegistration(ctx context.Context, req *connect.Request[user.VerifyPasskeyRegistrationRequest]) (*connect.Response[user.VerifyPasskeyRegistrationResponse], error) { + pkc, err := req.Msg.GetPublicKeyCredential().MarshalJSON() if err != nil { return nil, zerrors.ThrowInternal(err, "USERv2-Pha2o", "Errors.Internal") } - objectDetails, err := s.command.HumanHumanPasswordlessSetup(ctx, req.GetUserId(), "", req.GetPasskeyName(), "", pkc) + objectDetails, err := s.command.HumanHumanPasswordlessSetup(ctx, req.Msg.GetUserId(), "", req.Msg.GetPasskeyName(), "", pkc) if err != nil { return nil, err } - return &user.VerifyPasskeyRegistrationResponse{ + return connect.NewResponse(&user.VerifyPasskeyRegistrationResponse{ Details: object.DomainToDetailsPb(objectDetails), - }, nil + }), nil } -func (s *Server) CreatePasskeyRegistrationLink(ctx context.Context, req *user.CreatePasskeyRegistrationLinkRequest) (resp *user.CreatePasskeyRegistrationLinkResponse, err error) { - switch medium := req.Medium.(type) { +func (s *Server) CreatePasskeyRegistrationLink(ctx context.Context, req *connect.Request[user.CreatePasskeyRegistrationLinkRequest]) (resp *connect.Response[user.CreatePasskeyRegistrationLinkResponse], err error) { + switch medium := req.Msg.Medium.(type) { case nil: return passkeyDetailsToPb( - s.command.AddUserPasskeyCode(ctx, req.GetUserId(), "", s.userCodeAlg), + s.command.AddUserPasskeyCode(ctx, req.Msg.GetUserId(), "", s.userCodeAlg), ) case *user.CreatePasskeyRegistrationLinkRequest_SendLink: return passkeyDetailsToPb( - s.command.AddUserPasskeyCodeURLTemplate(ctx, req.GetUserId(), "", s.userCodeAlg, medium.SendLink.GetUrlTemplate()), + s.command.AddUserPasskeyCodeURLTemplate(ctx, req.Msg.GetUserId(), "", s.userCodeAlg, medium.SendLink.GetUrlTemplate()), ) case *user.CreatePasskeyRegistrationLinkRequest_ReturnCode: return passkeyCodeDetailsToPb( - s.command.AddUserPasskeyCodeReturn(ctx, req.GetUserId(), "", s.userCodeAlg), + s.command.AddUserPasskeyCodeReturn(ctx, req.Msg.GetUserId(), "", s.userCodeAlg), ) default: return nil, zerrors.ThrowUnimplementedf(nil, "USERv2-gaD8y", "verification oneOf %T in method CreatePasskeyRegistrationLink not implemented", medium) } } -func passkeyDetailsToPb(details *domain.ObjectDetails, err error) (*user.CreatePasskeyRegistrationLinkResponse, error) { +func passkeyDetailsToPb(details *domain.ObjectDetails, err error) (*connect.Response[user.CreatePasskeyRegistrationLinkResponse], error) { if err != nil { return nil, err } - return &user.CreatePasskeyRegistrationLinkResponse{ + return connect.NewResponse(&user.CreatePasskeyRegistrationLinkResponse{ Details: object.DomainToDetailsPb(details), - }, nil + }), nil } -func passkeyCodeDetailsToPb(details *domain.PasskeyCodeDetails, err error) (*user.CreatePasskeyRegistrationLinkResponse, error) { +func passkeyCodeDetailsToPb(details *domain.PasskeyCodeDetails, err error) (*connect.Response[user.CreatePasskeyRegistrationLinkResponse], error) { if err != nil { return nil, err } - return &user.CreatePasskeyRegistrationLinkResponse{ + return connect.NewResponse(&user.CreatePasskeyRegistrationLinkResponse{ Details: object.DomainToDetailsPb(details.ObjectDetails), Code: &user.PasskeyRegistrationCode{ Id: details.CodeID, Code: details.Code, }, - }, nil + }), nil } diff --git a/internal/api/grpc/user/v2beta/passkey_test.go b/internal/api/grpc/user/v2beta/passkey_test.go index f4a48ed941..12ef8ed02f 100644 --- a/internal/api/grpc/user/v2beta/passkey_test.go +++ b/internal/api/grpc/user/v2beta/passkey_test.go @@ -123,11 +123,11 @@ func Test_passkeyRegistrationDetailsToPb(t *testing.T) { t.Run(tt.name, func(t *testing.T) { got, err := passkeyRegistrationDetailsToPb(tt.args.details, tt.args.err) require.ErrorIs(t, err, tt.wantErr) - if !proto.Equal(tt.want, got) { + if tt.want != nil && !proto.Equal(tt.want, got.Msg) { t.Errorf("Not equal:\nExpected\n%s\nActual:%s", tt.want, got) } if tt.want != nil { - grpc.AllFieldsSet(t, got.ProtoReflect()) + grpc.AllFieldsSet(t, got.Msg.ProtoReflect()) } }) } @@ -181,7 +181,9 @@ func Test_passkeyDetailsToPb(t *testing.T) { t.Run(tt.name, func(t *testing.T) { got, err := passkeyDetailsToPb(tt.args.details, tt.args.err) require.ErrorIs(t, err, tt.args.err) - assert.Equal(t, tt.want, got) + if tt.want != nil { + assert.Equal(t, tt.want, got.Msg) + } }) } } @@ -242,9 +244,9 @@ func Test_passkeyCodeDetailsToPb(t *testing.T) { t.Run(tt.name, func(t *testing.T) { got, err := passkeyCodeDetailsToPb(tt.args.details, tt.args.err) require.ErrorIs(t, err, tt.args.err) - assert.Equal(t, tt.want, got) if tt.want != nil { - grpc.AllFieldsSet(t, got.ProtoReflect()) + assert.Equal(t, tt.want, got.Msg) + grpc.AllFieldsSet(t, got.Msg.ProtoReflect()) } }) } diff --git a/internal/api/grpc/user/v2beta/password.go b/internal/api/grpc/user/v2beta/password.go index 0de1262215..ae9a549db0 100644 --- a/internal/api/grpc/user/v2beta/password.go +++ b/internal/api/grpc/user/v2beta/password.go @@ -3,23 +3,25 @@ package user import ( "context" + "connectrpc.com/connect" + object "github.com/zitadel/zitadel/internal/api/grpc/object/v2beta" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/zerrors" user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" ) -func (s *Server) PasswordReset(ctx context.Context, req *user.PasswordResetRequest) (_ *user.PasswordResetResponse, err error) { +func (s *Server) PasswordReset(ctx context.Context, req *connect.Request[user.PasswordResetRequest]) (_ *connect.Response[user.PasswordResetResponse], err error) { var details *domain.ObjectDetails var code *string - switch m := req.GetMedium().(type) { + switch m := req.Msg.GetMedium().(type) { case *user.PasswordResetRequest_SendLink: - details, code, err = s.command.RequestPasswordResetURLTemplate(ctx, req.GetUserId(), m.SendLink.GetUrlTemplate(), notificationTypeToDomain(m.SendLink.GetNotificationType())) + details, code, err = s.command.RequestPasswordResetURLTemplate(ctx, req.Msg.GetUserId(), m.SendLink.GetUrlTemplate(), notificationTypeToDomain(m.SendLink.GetNotificationType())) case *user.PasswordResetRequest_ReturnCode: - details, code, err = s.command.RequestPasswordResetReturnCode(ctx, req.GetUserId()) + details, code, err = s.command.RequestPasswordResetReturnCode(ctx, req.Msg.GetUserId()) case nil: - details, code, err = s.command.RequestPasswordReset(ctx, req.GetUserId()) + details, code, err = s.command.RequestPasswordReset(ctx, req.Msg.GetUserId()) default: err = zerrors.ThrowUnimplementedf(nil, "USERv2-SDeeg", "verification oneOf %T in method RequestPasswordReset not implemented", m) } @@ -27,10 +29,10 @@ func (s *Server) PasswordReset(ctx context.Context, req *user.PasswordResetReque return nil, err } - return &user.PasswordResetResponse{ + return connect.NewResponse(&user.PasswordResetResponse{ Details: object.DomainToDetailsPb(details), VerificationCode: code, - }, nil + }), nil } func notificationTypeToDomain(notificationType user.NotificationType) domain.NotificationType { @@ -46,16 +48,16 @@ func notificationTypeToDomain(notificationType user.NotificationType) domain.Not } } -func (s *Server) SetPassword(ctx context.Context, req *user.SetPasswordRequest) (_ *user.SetPasswordResponse, err error) { +func (s *Server) SetPassword(ctx context.Context, req *connect.Request[user.SetPasswordRequest]) (_ *connect.Response[user.SetPasswordResponse], err error) { var details *domain.ObjectDetails - switch v := req.GetVerification().(type) { + switch v := req.Msg.GetVerification().(type) { case *user.SetPasswordRequest_CurrentPassword: - details, err = s.command.ChangePassword(ctx, "", req.GetUserId(), v.CurrentPassword, req.GetNewPassword().GetPassword(), "", req.GetNewPassword().GetChangeRequired()) + details, err = s.command.ChangePassword(ctx, "", req.Msg.GetUserId(), v.CurrentPassword, req.Msg.GetNewPassword().GetPassword(), "", req.Msg.GetNewPassword().GetChangeRequired()) case *user.SetPasswordRequest_VerificationCode: - details, err = s.command.SetPasswordWithVerifyCode(ctx, "", req.GetUserId(), v.VerificationCode, req.GetNewPassword().GetPassword(), "", req.GetNewPassword().GetChangeRequired()) + details, err = s.command.SetPasswordWithVerifyCode(ctx, "", req.Msg.GetUserId(), v.VerificationCode, req.Msg.GetNewPassword().GetPassword(), "", req.Msg.GetNewPassword().GetChangeRequired()) case nil: - details, err = s.command.SetPassword(ctx, "", req.GetUserId(), req.GetNewPassword().GetPassword(), req.GetNewPassword().GetChangeRequired()) + details, err = s.command.SetPassword(ctx, "", req.Msg.GetUserId(), req.Msg.GetNewPassword().GetPassword(), req.Msg.GetNewPassword().GetChangeRequired()) default: err = zerrors.ThrowUnimplementedf(nil, "USERv2-SFdf2", "verification oneOf %T in method SetPasswordRequest not implemented", v) } @@ -63,7 +65,7 @@ func (s *Server) SetPassword(ctx context.Context, req *user.SetPasswordRequest) return nil, err } - return &user.SetPasswordResponse{ + return connect.NewResponse(&user.SetPasswordResponse{ Details: object.DomainToDetailsPb(details), - }, nil + }), nil } diff --git a/internal/api/grpc/user/v2beta/phone.go b/internal/api/grpc/user/v2beta/phone.go index eac7eb4e31..20ef2075ab 100644 --- a/internal/api/grpc/user/v2beta/phone.go +++ b/internal/api/grpc/user/v2beta/phone.go @@ -3,6 +3,7 @@ package user import ( "context" + "connectrpc.com/connect" "google.golang.org/protobuf/types/known/timestamppb" "github.com/zitadel/zitadel/internal/domain" @@ -11,18 +12,18 @@ import ( user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" ) -func (s *Server) SetPhone(ctx context.Context, req *user.SetPhoneRequest) (resp *user.SetPhoneResponse, err error) { +func (s *Server) SetPhone(ctx context.Context, req *connect.Request[user.SetPhoneRequest]) (resp *connect.Response[user.SetPhoneResponse], err error) { var phone *domain.Phone - switch v := req.GetVerification().(type) { + switch v := req.Msg.GetVerification().(type) { case *user.SetPhoneRequest_SendCode: - phone, err = s.command.ChangeUserPhone(ctx, req.GetUserId(), req.GetPhone(), s.userCodeAlg) + phone, err = s.command.ChangeUserPhone(ctx, req.Msg.GetUserId(), req.Msg.GetPhone(), s.userCodeAlg) case *user.SetPhoneRequest_ReturnCode: - phone, err = s.command.ChangeUserPhoneReturnCode(ctx, req.GetUserId(), req.GetPhone(), s.userCodeAlg) + phone, err = s.command.ChangeUserPhoneReturnCode(ctx, req.Msg.GetUserId(), req.Msg.GetPhone(), s.userCodeAlg) case *user.SetPhoneRequest_IsVerified: - phone, err = s.command.ChangeUserPhoneVerified(ctx, req.GetUserId(), req.GetPhone()) + phone, err = s.command.ChangeUserPhoneVerified(ctx, req.Msg.GetUserId(), req.Msg.GetPhone()) case nil: - phone, err = s.command.ChangeUserPhone(ctx, req.GetUserId(), req.GetPhone(), s.userCodeAlg) + phone, err = s.command.ChangeUserPhone(ctx, req.Msg.GetUserId(), req.Msg.GetPhone(), s.userCodeAlg) default: err = zerrors.ThrowUnimplementedf(nil, "USERv2-Ahng0", "verification oneOf %T in method SetPhone not implemented", v) } @@ -30,42 +31,42 @@ func (s *Server) SetPhone(ctx context.Context, req *user.SetPhoneRequest) (resp return nil, err } - return &user.SetPhoneResponse{ + return connect.NewResponse(&user.SetPhoneResponse{ Details: &object.Details{ Sequence: phone.Sequence, ChangeDate: timestamppb.New(phone.ChangeDate), ResourceOwner: phone.ResourceOwner, }, VerificationCode: phone.PlainCode, - }, nil + }), nil } -func (s *Server) RemovePhone(ctx context.Context, req *user.RemovePhoneRequest) (resp *user.RemovePhoneResponse, err error) { +func (s *Server) RemovePhone(ctx context.Context, req *connect.Request[user.RemovePhoneRequest]) (resp *connect.Response[user.RemovePhoneResponse], err error) { details, err := s.command.RemoveUserPhone(ctx, - req.GetUserId(), + req.Msg.GetUserId(), ) if err != nil { return nil, err } - return &user.RemovePhoneResponse{ + return connect.NewResponse(&user.RemovePhoneResponse{ Details: &object.Details{ Sequence: details.Sequence, ChangeDate: timestamppb.New(details.EventDate), ResourceOwner: details.ResourceOwner, }, - }, nil + }), nil } -func (s *Server) ResendPhoneCode(ctx context.Context, req *user.ResendPhoneCodeRequest) (resp *user.ResendPhoneCodeResponse, err error) { +func (s *Server) ResendPhoneCode(ctx context.Context, req *connect.Request[user.ResendPhoneCodeRequest]) (resp *connect.Response[user.ResendPhoneCodeResponse], err error) { var phone *domain.Phone - switch v := req.GetVerification().(type) { + switch v := req.Msg.GetVerification().(type) { case *user.ResendPhoneCodeRequest_SendCode: - phone, err = s.command.ResendUserPhoneCode(ctx, req.GetUserId(), s.userCodeAlg) + phone, err = s.command.ResendUserPhoneCode(ctx, req.Msg.GetUserId(), s.userCodeAlg) case *user.ResendPhoneCodeRequest_ReturnCode: - phone, err = s.command.ResendUserPhoneCodeReturnCode(ctx, req.GetUserId(), s.userCodeAlg) + phone, err = s.command.ResendUserPhoneCodeReturnCode(ctx, req.Msg.GetUserId(), s.userCodeAlg) case nil: - phone, err = s.command.ResendUserPhoneCode(ctx, req.GetUserId(), s.userCodeAlg) + phone, err = s.command.ResendUserPhoneCode(ctx, req.Msg.GetUserId(), s.userCodeAlg) default: err = zerrors.ThrowUnimplementedf(nil, "USERv2-ResendUserPhoneCode", "verification oneOf %T in method SetPhone not implemented", v) } @@ -73,30 +74,30 @@ func (s *Server) ResendPhoneCode(ctx context.Context, req *user.ResendPhoneCodeR return nil, err } - return &user.ResendPhoneCodeResponse{ + return connect.NewResponse(&user.ResendPhoneCodeResponse{ Details: &object.Details{ Sequence: phone.Sequence, ChangeDate: timestamppb.New(phone.ChangeDate), ResourceOwner: phone.ResourceOwner, }, VerificationCode: phone.PlainCode, - }, nil + }), nil } -func (s *Server) VerifyPhone(ctx context.Context, req *user.VerifyPhoneRequest) (*user.VerifyPhoneResponse, error) { +func (s *Server) VerifyPhone(ctx context.Context, req *connect.Request[user.VerifyPhoneRequest]) (*connect.Response[user.VerifyPhoneResponse], error) { details, err := s.command.VerifyUserPhone(ctx, - req.GetUserId(), - req.GetVerificationCode(), + req.Msg.GetUserId(), + req.Msg.GetVerificationCode(), s.userCodeAlg, ) if err != nil { return nil, err } - return &user.VerifyPhoneResponse{ + return connect.NewResponse(&user.VerifyPhoneResponse{ Details: &object.Details{ Sequence: details.Sequence, ChangeDate: timestamppb.New(details.EventDate), ResourceOwner: details.ResourceOwner, }, - }, nil + }), nil } diff --git a/internal/api/grpc/user/v2beta/query.go b/internal/api/grpc/user/v2beta/query.go index 993e2d16b7..b9654ea97c 100644 --- a/internal/api/grpc/user/v2beta/query.go +++ b/internal/api/grpc/user/v2beta/query.go @@ -3,6 +3,7 @@ package user import ( "context" + "connectrpc.com/connect" "github.com/muhlemmer/gu" "google.golang.org/protobuf/types/known/timestamppb" @@ -13,34 +14,34 @@ import ( user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" ) -func (s *Server) GetUserByID(ctx context.Context, req *user.GetUserByIDRequest) (_ *user.GetUserByIDResponse, err error) { - resp, err := s.query.GetUserByIDWithPermission(ctx, true, req.GetUserId(), s.checkPermission) +func (s *Server) GetUserByID(ctx context.Context, req *connect.Request[user.GetUserByIDRequest]) (_ *connect.Response[user.GetUserByIDResponse], err error) { + resp, err := s.query.GetUserByIDWithPermission(ctx, true, req.Msg.GetUserId(), s.checkPermission) if err != nil { return nil, err } - return &user.GetUserByIDResponse{ + return connect.NewResponse(&user.GetUserByIDResponse{ Details: object.DomainToDetailsPb(&domain.ObjectDetails{ Sequence: resp.Sequence, EventDate: resp.ChangeDate, ResourceOwner: resp.ResourceOwner, }), User: userToPb(resp, s.assetAPIPrefix(ctx)), - }, nil + }), nil } -func (s *Server) ListUsers(ctx context.Context, req *user.ListUsersRequest) (*user.ListUsersResponse, error) { - queries, filterOrgIds, err := listUsersRequestToModel(req) +func (s *Server) ListUsers(ctx context.Context, req *connect.Request[user.ListUsersRequest]) (*connect.Response[user.ListUsersResponse], error) { + queries, err := listUsersRequestToModel(req.Msg) if err != nil { return nil, err } - res, err := s.query.SearchUsers(ctx, queries, filterOrgIds, s.checkPermission) + res, err := s.query.SearchUsers(ctx, queries, s.checkPermission) if err != nil { return nil, err } - return &user.ListUsersResponse{ + return connect.NewResponse(&user.ListUsersResponse{ Result: UsersToPb(res.Users, s.assetAPIPrefix(ctx)), Details: object.ToListDetails(res.SearchResponse), - }, nil + }), nil } func UsersToPb(users []*query.User, assetPrefix string) []*user.User { @@ -165,11 +166,11 @@ func accessTokenTypeToPb(accessTokenType domain.OIDCTokenType) user.AccessTokenT } } -func listUsersRequestToModel(req *user.ListUsersRequest) (*query.UserSearchQueries, string, error) { +func listUsersRequestToModel(req *user.ListUsersRequest) (*query.UserSearchQueries, error) { offset, limit, asc := object.ListQueryToQuery(req.Query) - queries, filterOrgId, err := userQueriesToQuery(req.Queries, 0 /*start from level 0*/) + queries, err := userQueriesToQuery(req.Queries, 0 /*start from level 0*/) if err != nil { - return nil, "", err + return nil, err } return &query.UserSearchQueries{ SearchRequest: query.SearchRequest{ @@ -179,7 +180,7 @@ func listUsersRequestToModel(req *user.ListUsersRequest) (*query.UserSearchQueri SortingColumn: userFieldNameToSortingColumn(req.SortingColumn), }, Queries: queries, - }, filterOrgId, nil + }, nil } func userFieldNameToSortingColumn(field user.UserFieldName) query.Column { @@ -209,18 +210,15 @@ func userFieldNameToSortingColumn(field user.UserFieldName) query.Column { } } -func userQueriesToQuery(queries []*user.SearchQuery, level uint8) (_ []query.SearchQuery, filterOrgId string, err error) { +func userQueriesToQuery(queries []*user.SearchQuery, level uint8) (_ []query.SearchQuery, err error) { q := make([]query.SearchQuery, len(queries)) for i, query := range queries { - if orgFilter := query.GetOrganizationIdQuery(); orgFilter != nil { - filterOrgId = orgFilter.OrganizationId - } q[i], err = userQueryToQuery(query, level) if err != nil { - return nil, filterOrgId, err + return nil, err } } - return q, filterOrgId, nil + return q, nil } func userQueryToQuery(query *user.SearchQuery, level uint8) (query.SearchQuery, error) { @@ -314,14 +312,14 @@ func inUserIdsQueryToQuery(q *user.InUserIDQuery) (query.SearchQuery, error) { return query.NewUserInUserIdsSearchQuery(q.UserIds) } func orQueryToQuery(q *user.OrQuery, level uint8) (query.SearchQuery, error) { - mappedQueries, _, err := userQueriesToQuery(q.Queries, level+1) + mappedQueries, err := userQueriesToQuery(q.Queries, level+1) if err != nil { return nil, err } return query.NewUserOrSearchQuery(mappedQueries) } func andQueryToQuery(q *user.AndQuery, level uint8) (query.SearchQuery, error) { - mappedQueries, _, err := userQueriesToQuery(q.Queries, level+1) + mappedQueries, err := userQueriesToQuery(q.Queries, level+1) if err != nil { return nil, err } diff --git a/internal/api/grpc/user/v2beta/server.go b/internal/api/grpc/user/v2beta/server.go index 93af47f58b..7e3934a2c1 100644 --- a/internal/api/grpc/user/v2beta/server.go +++ b/internal/api/grpc/user/v2beta/server.go @@ -2,8 +2,10 @@ package user import ( "context" + "net/http" - "google.golang.org/grpc" + "connectrpc.com/connect" + "google.golang.org/protobuf/reflect/protoreflect" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/grpc/server" @@ -12,12 +14,12 @@ import ( "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query" user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/user/v2beta/userconnect" ) -var _ user.UserServiceServer = (*Server)(nil) +var _ userconnect.UserServiceHandler = (*Server)(nil) type Server struct { - user.UnimplementedUserServiceServer command *command.Commands query *query.Queries userCodeAlg crypto.EncryptionAlgorithm @@ -54,8 +56,12 @@ func CreateServer( } } -func (s *Server) RegisterServer(grpcServer *grpc.Server) { - user.RegisterUserServiceServer(grpcServer, s) +func (s *Server) RegisterConnectServer(interceptors ...connect.Interceptor) (string, http.Handler) { + return userconnect.NewUserServiceHandler(s, connect.WithInterceptors(interceptors...)) +} + +func (s *Server) FileDescriptor() protoreflect.FileDescriptor { + return user.File_zitadel_user_v2beta_user_service_proto } func (s *Server) AppName() string { diff --git a/internal/api/grpc/user/v2beta/totp.go b/internal/api/grpc/user/v2beta/totp.go index 2ef47a9817..e7bd01b2b6 100644 --- a/internal/api/grpc/user/v2beta/totp.go +++ b/internal/api/grpc/user/v2beta/totp.go @@ -3,42 +3,44 @@ package user import ( "context" + "connectrpc.com/connect" + object "github.com/zitadel/zitadel/internal/api/grpc/object/v2beta" "github.com/zitadel/zitadel/internal/domain" user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" ) -func (s *Server) RegisterTOTP(ctx context.Context, req *user.RegisterTOTPRequest) (*user.RegisterTOTPResponse, error) { +func (s *Server) RegisterTOTP(ctx context.Context, req *connect.Request[user.RegisterTOTPRequest]) (*connect.Response[user.RegisterTOTPResponse], error) { return totpDetailsToPb( - s.command.AddUserTOTP(ctx, req.GetUserId(), ""), + s.command.AddUserTOTP(ctx, req.Msg.GetUserId(), ""), ) } -func totpDetailsToPb(totp *domain.TOTP, err error) (*user.RegisterTOTPResponse, error) { +func totpDetailsToPb(totp *domain.TOTP, err error) (*connect.Response[user.RegisterTOTPResponse], error) { if err != nil { return nil, err } - return &user.RegisterTOTPResponse{ + return connect.NewResponse(&user.RegisterTOTPResponse{ Details: object.DomainToDetailsPb(totp.ObjectDetails), Uri: totp.URI, Secret: totp.Secret, - }, nil + }), nil } -func (s *Server) VerifyTOTPRegistration(ctx context.Context, req *user.VerifyTOTPRegistrationRequest) (*user.VerifyTOTPRegistrationResponse, error) { - objectDetails, err := s.command.CheckUserTOTP(ctx, req.GetUserId(), req.GetCode(), "") +func (s *Server) VerifyTOTPRegistration(ctx context.Context, req *connect.Request[user.VerifyTOTPRegistrationRequest]) (*connect.Response[user.VerifyTOTPRegistrationResponse], error) { + objectDetails, err := s.command.CheckUserTOTP(ctx, req.Msg.GetUserId(), req.Msg.GetCode(), "") if err != nil { return nil, err } - return &user.VerifyTOTPRegistrationResponse{ + return connect.NewResponse(&user.VerifyTOTPRegistrationResponse{ Details: object.DomainToDetailsPb(objectDetails), - }, nil + }), nil } -func (s *Server) RemoveTOTP(ctx context.Context, req *user.RemoveTOTPRequest) (*user.RemoveTOTPResponse, error) { - objectDetails, err := s.command.HumanRemoveTOTP(ctx, req.GetUserId(), "") +func (s *Server) RemoveTOTP(ctx context.Context, req *connect.Request[user.RemoveTOTPRequest]) (*connect.Response[user.RemoveTOTPResponse], error) { + objectDetails, err := s.command.HumanRemoveTOTP(ctx, req.Msg.GetUserId(), "") if err != nil { return nil, err } - return &user.RemoveTOTPResponse{Details: object.DomainToDetailsPb(objectDetails)}, nil + return connect.NewResponse(&user.RemoveTOTPResponse{Details: object.DomainToDetailsPb(objectDetails)}), nil } diff --git a/internal/api/grpc/user/v2beta/totp_test.go b/internal/api/grpc/user/v2beta/totp_test.go index 81a54675f2..77c6e5c343 100644 --- a/internal/api/grpc/user/v2beta/totp_test.go +++ b/internal/api/grpc/user/v2beta/totp_test.go @@ -63,7 +63,7 @@ func Test_totpDetailsToPb(t *testing.T) { t.Run(tt.name, func(t *testing.T) { got, err := totpDetailsToPb(tt.args.otp, tt.args.err) require.ErrorIs(t, err, tt.wantErr) - if !proto.Equal(tt.want, got) { + if tt.want != nil && !proto.Equal(tt.want, got.Msg) { t.Errorf("RegisterTOTPResponse =\n%v\nwant\n%v", got, tt.want) } }) diff --git a/internal/api/grpc/user/v2beta/u2f.go b/internal/api/grpc/user/v2beta/u2f.go index e23a22b8b5..a6823a4bc0 100644 --- a/internal/api/grpc/user/v2beta/u2f.go +++ b/internal/api/grpc/user/v2beta/u2f.go @@ -3,40 +3,42 @@ package user import ( "context" + "connectrpc.com/connect" + object "github.com/zitadel/zitadel/internal/api/grpc/object/v2beta" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/zerrors" user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" ) -func (s *Server) RegisterU2F(ctx context.Context, req *user.RegisterU2FRequest) (*user.RegisterU2FResponse, error) { +func (s *Server) RegisterU2F(ctx context.Context, req *connect.Request[user.RegisterU2FRequest]) (*connect.Response[user.RegisterU2FResponse], error) { return u2fRegistrationDetailsToPb( - s.command.RegisterUserU2F(ctx, req.GetUserId(), "", req.GetDomain()), + s.command.RegisterUserU2F(ctx, req.Msg.GetUserId(), "", req.Msg.GetDomain()), ) } -func u2fRegistrationDetailsToPb(details *domain.WebAuthNRegistrationDetails, err error) (*user.RegisterU2FResponse, error) { +func u2fRegistrationDetailsToPb(details *domain.WebAuthNRegistrationDetails, err error) (*connect.Response[user.RegisterU2FResponse], error) { objectDetails, options, err := webAuthNRegistrationDetailsToPb(details, err) if err != nil { return nil, err } - return &user.RegisterU2FResponse{ + return connect.NewResponse(&user.RegisterU2FResponse{ Details: objectDetails, U2FId: details.ID, PublicKeyCredentialCreationOptions: options, - }, nil + }), nil } -func (s *Server) VerifyU2FRegistration(ctx context.Context, req *user.VerifyU2FRegistrationRequest) (*user.VerifyU2FRegistrationResponse, error) { - pkc, err := req.GetPublicKeyCredential().MarshalJSON() +func (s *Server) VerifyU2FRegistration(ctx context.Context, req *connect.Request[user.VerifyU2FRegistrationRequest]) (*connect.Response[user.VerifyU2FRegistrationResponse], error) { + pkc, err := req.Msg.GetPublicKeyCredential().MarshalJSON() if err != nil { return nil, zerrors.ThrowInternal(err, "USERv2-IeTh4", "Errors.Internal") } - objectDetails, err := s.command.HumanVerifyU2FSetup(ctx, req.GetUserId(), "", req.GetTokenName(), "", pkc) + objectDetails, err := s.command.HumanVerifyU2FSetup(ctx, req.Msg.GetUserId(), "", req.Msg.GetTokenName(), "", pkc) if err != nil { return nil, err } - return &user.VerifyU2FRegistrationResponse{ + return connect.NewResponse(&user.VerifyU2FRegistrationResponse{ Details: object.DomainToDetailsPb(objectDetails), - }, nil + }), nil } diff --git a/internal/api/grpc/user/v2beta/u2f_test.go b/internal/api/grpc/user/v2beta/u2f_test.go index 53f2a0bb8c..ac99c0d1eb 100644 --- a/internal/api/grpc/user/v2beta/u2f_test.go +++ b/internal/api/grpc/user/v2beta/u2f_test.go @@ -92,11 +92,11 @@ func Test_u2fRegistrationDetailsToPb(t *testing.T) { t.Run(tt.name, func(t *testing.T) { got, err := u2fRegistrationDetailsToPb(tt.args.details, tt.args.err) require.ErrorIs(t, err, tt.wantErr) - if !proto.Equal(tt.want, got) { + if tt.want != nil && !proto.Equal(tt.want, got.Msg) { t.Errorf("Not equal:\nExpected\n%s\nActual:%s", tt.want, got) } if tt.want != nil { - grpc.AllFieldsSet(t, got.ProtoReflect()) + grpc.AllFieldsSet(t, got.Msg.ProtoReflect()) } }) } diff --git a/internal/api/grpc/user/v2beta/user.go b/internal/api/grpc/user/v2beta/user.go index 93afbde0aa..3cde7b773e 100644 --- a/internal/api/grpc/user/v2beta/user.go +++ b/internal/api/grpc/user/v2beta/user.go @@ -6,6 +6,7 @@ import ( "io" "time" + "connectrpc.com/connect" "golang.org/x/text/language" "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" @@ -23,8 +24,8 @@ import ( user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" ) -func (s *Server) AddHumanUser(ctx context.Context, req *user.AddHumanUserRequest) (_ *user.AddHumanUserResponse, err error) { - human, err := AddUserRequestToAddHuman(req) +func (s *Server) AddHumanUser(ctx context.Context, req *connect.Request[user.AddHumanUserRequest]) (_ *connect.Response[user.AddHumanUserResponse], err error) { + human, err := AddUserRequestToAddHuman(req.Msg) if err != nil { return nil, err } @@ -32,12 +33,12 @@ func (s *Server) AddHumanUser(ctx context.Context, req *user.AddHumanUserRequest if err = s.command.AddUserHuman(ctx, orgID, human, false, s.userCodeAlg); err != nil { return nil, err } - return &user.AddHumanUserResponse{ + return connect.NewResponse(&user.AddHumanUserResponse{ UserId: human.ID, Details: object.DomainToDetailsPb(human.Details), EmailCode: human.EmailCode, PhoneCode: human.PhoneCode, - }, nil + }), nil } func AddUserRequestToAddHuman(req *user.AddHumanUserRequest) (*command.AddHuman, error) { @@ -115,8 +116,8 @@ func genderToDomain(gender user.Gender) domain.Gender { } } -func (s *Server) UpdateHumanUser(ctx context.Context, req *user.UpdateHumanUserRequest) (_ *user.UpdateHumanUserResponse, err error) { - human, err := UpdateUserRequestToChangeHuman(req) +func (s *Server) UpdateHumanUser(ctx context.Context, req *connect.Request[user.UpdateHumanUserRequest]) (_ *connect.Response[user.UpdateHumanUserResponse], err error) { + human, err := UpdateUserRequestToChangeHuman(req.Msg) if err != nil { return nil, err } @@ -124,51 +125,51 @@ func (s *Server) UpdateHumanUser(ctx context.Context, req *user.UpdateHumanUserR if err != nil { return nil, err } - return &user.UpdateHumanUserResponse{ + return connect.NewResponse(&user.UpdateHumanUserResponse{ Details: object.DomainToDetailsPb(human.Details), EmailCode: human.EmailCode, PhoneCode: human.PhoneCode, - }, nil + }), nil } -func (s *Server) LockUser(ctx context.Context, req *user.LockUserRequest) (_ *user.LockUserResponse, err error) { - details, err := s.command.LockUserV2(ctx, req.UserId) +func (s *Server) LockUser(ctx context.Context, req *connect.Request[user.LockUserRequest]) (_ *connect.Response[user.LockUserResponse], err error) { + details, err := s.command.LockUserV2(ctx, req.Msg.GetUserId()) if err != nil { return nil, err } - return &user.LockUserResponse{ + return connect.NewResponse(&user.LockUserResponse{ Details: object.DomainToDetailsPb(details), - }, nil + }), nil } -func (s *Server) UnlockUser(ctx context.Context, req *user.UnlockUserRequest) (_ *user.UnlockUserResponse, err error) { - details, err := s.command.UnlockUserV2(ctx, req.UserId) +func (s *Server) UnlockUser(ctx context.Context, req *connect.Request[user.UnlockUserRequest]) (_ *connect.Response[user.UnlockUserResponse], err error) { + details, err := s.command.UnlockUserV2(ctx, req.Msg.GetUserId()) if err != nil { return nil, err } - return &user.UnlockUserResponse{ + return connect.NewResponse(&user.UnlockUserResponse{ Details: object.DomainToDetailsPb(details), - }, nil + }), nil } -func (s *Server) DeactivateUser(ctx context.Context, req *user.DeactivateUserRequest) (_ *user.DeactivateUserResponse, err error) { - details, err := s.command.DeactivateUserV2(ctx, req.UserId) +func (s *Server) DeactivateUser(ctx context.Context, req *connect.Request[user.DeactivateUserRequest]) (_ *connect.Response[user.DeactivateUserResponse], err error) { + details, err := s.command.DeactivateUserV2(ctx, req.Msg.GetUserId()) if err != nil { return nil, err } - return &user.DeactivateUserResponse{ + return connect.NewResponse(&user.DeactivateUserResponse{ Details: object.DomainToDetailsPb(details), - }, nil + }), nil } -func (s *Server) ReactivateUser(ctx context.Context, req *user.ReactivateUserRequest) (_ *user.ReactivateUserResponse, err error) { - details, err := s.command.ReactivateUserV2(ctx, req.UserId) +func (s *Server) ReactivateUser(ctx context.Context, req *connect.Request[user.ReactivateUserRequest]) (_ *connect.Response[user.ReactivateUserResponse], err error) { + details, err := s.command.ReactivateUserV2(ctx, req.Msg.GetUserId()) if err != nil { return nil, err } - return &user.ReactivateUserResponse{ + return connect.NewResponse(&user.ReactivateUserResponse{ Details: object.DomainToDetailsPb(details), - }, nil + }), nil } func ifNotNilPtr[v, p any](value *v, conv func(v) p) *p { @@ -260,32 +261,32 @@ func SetHumanPasswordToPassword(password *user.SetPassword) *command.Password { } } -func (s *Server) AddIDPLink(ctx context.Context, req *user.AddIDPLinkRequest) (_ *user.AddIDPLinkResponse, err error) { - details, err := s.command.AddUserIDPLink(ctx, req.UserId, "", &command.AddLink{ - IDPID: req.GetIdpLink().GetIdpId(), - DisplayName: req.GetIdpLink().GetUserName(), - IDPExternalID: req.GetIdpLink().GetUserId(), +func (s *Server) AddIDPLink(ctx context.Context, req *connect.Request[user.AddIDPLinkRequest]) (_ *connect.Response[user.AddIDPLinkResponse], err error) { + details, err := s.command.AddUserIDPLink(ctx, req.Msg.GetUserId(), "", &command.AddLink{ + IDPID: req.Msg.GetIdpLink().GetIdpId(), + DisplayName: req.Msg.GetIdpLink().GetUserName(), + IDPExternalID: req.Msg.GetIdpLink().GetUserId(), }) if err != nil { return nil, err } - return &user.AddIDPLinkResponse{ + return connect.NewResponse(&user.AddIDPLinkResponse{ Details: object.DomainToDetailsPb(details), - }, nil + }), nil } -func (s *Server) DeleteUser(ctx context.Context, req *user.DeleteUserRequest) (_ *user.DeleteUserResponse, err error) { - memberships, grants, err := s.removeUserDependencies(ctx, req.GetUserId()) +func (s *Server) DeleteUser(ctx context.Context, req *connect.Request[user.DeleteUserRequest]) (_ *connect.Response[user.DeleteUserResponse], err error) { + memberships, grants, err := s.removeUserDependencies(ctx, req.Msg.GetUserId()) if err != nil { return nil, err } - details, err := s.command.RemoveUserV2(ctx, req.UserId, "", memberships, grants...) + details, err := s.command.RemoveUserV2(ctx, req.Msg.GetUserId(), "", memberships, grants...) if err != nil { return nil, err } - return &user.DeleteUserResponse{ + return connect.NewResponse(&user.DeleteUserResponse{ Details: object.DomainToDetailsPb(details), - }, nil + }), nil } func (s *Server) removeUserDependencies(ctx context.Context, userID string) ([]*command.CascadingMembership, []string, error) { @@ -295,7 +296,7 @@ func (s *Server) removeUserDependencies(ctx context.Context, userID string) ([]* } grants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{ Queries: []query.SearchQuery{userGrantUserQuery}, - }, true) + }, true, nil) if err != nil { return nil, nil, err } @@ -360,18 +361,18 @@ func userGrantsToIDs(userGrants []*query.UserGrant) []string { return converted } -func (s *Server) StartIdentityProviderIntent(ctx context.Context, req *user.StartIdentityProviderIntentRequest) (_ *user.StartIdentityProviderIntentResponse, err error) { - switch t := req.GetContent().(type) { +func (s *Server) StartIdentityProviderIntent(ctx context.Context, req *connect.Request[user.StartIdentityProviderIntentRequest]) (_ *connect.Response[user.StartIdentityProviderIntentResponse], err error) { + switch t := req.Msg.GetContent().(type) { case *user.StartIdentityProviderIntentRequest_Urls: - return s.startIDPIntent(ctx, req.GetIdpId(), t.Urls) + return s.startIDPIntent(ctx, req.Msg.GetIdpId(), t.Urls) case *user.StartIdentityProviderIntentRequest_Ldap: - return s.startLDAPIntent(ctx, req.GetIdpId(), t.Ldap) + return s.startLDAPIntent(ctx, req.Msg.GetIdpId(), t.Ldap) default: return nil, zerrors.ThrowUnimplementedf(nil, "USERv2-S2g21", "type oneOf %T in method StartIdentityProviderIntent not implemented", t) } } -func (s *Server) startIDPIntent(ctx context.Context, idpID string, urls *user.RedirectURLs) (*user.StartIdentityProviderIntentResponse, error) { +func (s *Server) startIDPIntent(ctx context.Context, idpID string, urls *user.RedirectURLs) (*connect.Response[user.StartIdentityProviderIntentResponse], error) { state, session, err := s.command.AuthFromProvider(ctx, idpID, s.idpCallback(ctx), s.samlRootURL(ctx, idpID)) if err != nil { return nil, err @@ -380,22 +381,31 @@ func (s *Server) startIDPIntent(ctx context.Context, idpID string, urls *user.Re if err != nil { return nil, err } - content, redirect := session.GetAuth(ctx) - if redirect { - return &user.StartIdentityProviderIntentResponse{ - Details: object.DomainToDetailsPb(details), - NextStep: &user.StartIdentityProviderIntentResponse_AuthUrl{AuthUrl: content}, - }, nil + auth, err := session.GetAuth(ctx) + if err != nil { + return nil, err } - return &user.StartIdentityProviderIntentResponse{ - Details: object.DomainToDetailsPb(details), - NextStep: &user.StartIdentityProviderIntentResponse_PostForm{ - PostForm: []byte(content), - }, - }, nil + switch a := auth.(type) { + case *idp.RedirectAuth: + return connect.NewResponse(&user.StartIdentityProviderIntentResponse{ + Details: object.DomainToDetailsPb(details), + NextStep: &user.StartIdentityProviderIntentResponse_AuthUrl{AuthUrl: a.RedirectURL}, + }), nil + case *idp.FormAuth: + return connect.NewResponse(&user.StartIdentityProviderIntentResponse{ + Details: object.DomainToDetailsPb(details), + NextStep: &user.StartIdentityProviderIntentResponse_FormData{ + FormData: &user.FormData{ + Url: a.URL, + Fields: a.Fields, + }, + }, + }), nil + } + return nil, zerrors.ThrowInvalidArgumentf(nil, "USERv2-3g2j3", "type oneOf %T in method StartIdentityProviderIntent not implemented", auth) } -func (s *Server) startLDAPIntent(ctx context.Context, idpID string, ldapCredentials *user.LDAPCredentials) (*user.StartIdentityProviderIntentResponse, error) { +func (s *Server) startLDAPIntent(ctx context.Context, idpID string, ldapCredentials *user.LDAPCredentials) (*connect.Response[user.StartIdentityProviderIntentResponse], error) { intentWriteModel, details, err := s.command.CreateIntent(ctx, "", idpID, "", "", authz.GetInstance(ctx).InstanceID(), nil) if err != nil { return nil, err @@ -411,7 +421,7 @@ func (s *Server) startLDAPIntent(ctx context.Context, idpID string, ldapCredenti if err != nil { return nil, err } - return &user.StartIdentityProviderIntentResponse{ + return connect.NewResponse(&user.StartIdentityProviderIntentResponse{ Details: object.DomainToDetailsPb(details), NextStep: &user.StartIdentityProviderIntentResponse_IdpIntent{ IdpIntent: &user.IDPIntent{ @@ -420,7 +430,7 @@ func (s *Server) startLDAPIntent(ctx context.Context, idpID string, ldapCredenti UserId: userID, }, }, - }, nil + }), nil } func (s *Server) checkLinkedExternalUser(ctx context.Context, idpID, externalUserID string) (string, error) { @@ -474,12 +484,12 @@ func (s *Server) ldapLogin(ctx context.Context, idpID, username, password string return externalUser, userID, session, nil } -func (s *Server) RetrieveIdentityProviderIntent(ctx context.Context, req *user.RetrieveIdentityProviderIntentRequest) (_ *user.RetrieveIdentityProviderIntentResponse, err error) { - intent, err := s.command.GetIntentWriteModel(ctx, req.GetIdpIntentId(), "") +func (s *Server) RetrieveIdentityProviderIntent(ctx context.Context, req *connect.Request[user.RetrieveIdentityProviderIntentRequest]) (_ *connect.Response[user.RetrieveIdentityProviderIntentResponse], err error) { + intent, err := s.command.GetIntentWriteModel(ctx, req.Msg.GetIdpIntentId(), "") if err != nil { return nil, err } - if err := s.checkIntentToken(req.GetIdpIntentToken(), intent.AggregateID); err != nil { + if err := s.checkIntentToken(req.Msg.GetIdpIntentToken(), intent.AggregateID); err != nil { return nil, err } if intent.State != domain.IDPIntentStateSucceeded { @@ -491,7 +501,7 @@ func (s *Server) RetrieveIdentityProviderIntent(ctx context.Context, req *user.R return idpIntentToIDPIntentPb(intent, s.idpAlg) } -func idpIntentToIDPIntentPb(intent *command.IDPIntentWriteModel, alg crypto.EncryptionAlgorithm) (_ *user.RetrieveIdentityProviderIntentResponse, err error) { +func idpIntentToIDPIntentPb(intent *command.IDPIntentWriteModel, alg crypto.EncryptionAlgorithm) (_ *connect.Response[user.RetrieveIdentityProviderIntentResponse], err error) { rawInformation := new(structpb.Struct) err = rawInformation.UnmarshalJSON(intent.IDPUser) if err != nil { @@ -530,7 +540,7 @@ func idpIntentToIDPIntentPb(intent *command.IDPIntentWriteModel, alg crypto.Encr information.IdpInformation.Access = IDPSAMLResponseToPb(assertion) } - return information, nil + return connect.NewResponse(information), nil } func idpOAuthTokensToPb(idpIDToken string, idpAccessToken *crypto.CryptoValue, alg crypto.EncryptionAlgorithm) (_ *user.IDPInformation_Oauth, err error) { @@ -593,15 +603,15 @@ func (s *Server) checkIntentToken(token string, intentID string) error { return crypto.CheckToken(s.idpAlg, token, intentID) } -func (s *Server) ListAuthenticationMethodTypes(ctx context.Context, req *user.ListAuthenticationMethodTypesRequest) (*user.ListAuthenticationMethodTypesResponse, error) { - authMethods, err := s.query.ListUserAuthMethodTypes(ctx, req.GetUserId(), true, false, "") +func (s *Server) ListAuthenticationMethodTypes(ctx context.Context, req *connect.Request[user.ListAuthenticationMethodTypesRequest]) (*connect.Response[user.ListAuthenticationMethodTypesResponse], error) { + authMethods, err := s.query.ListUserAuthMethodTypes(ctx, req.Msg.GetUserId(), true, false, "") if err != nil { return nil, err } - return &user.ListAuthenticationMethodTypesResponse{ + return connect.NewResponse(&user.ListAuthenticationMethodTypesResponse{ Details: object.ToListDetails(authMethods.SearchResponse), AuthMethodTypes: authMethodTypesToPb(authMethods.AuthMethodTypes), - }, nil + }), nil } func authMethodTypesToPb(methodTypes []domain.UserAuthMethodType) []user.AuthenticationMethodType { diff --git a/internal/api/grpc/user/v2beta/user_test.go b/internal/api/grpc/user/v2beta/user_test.go index 9e398e83ff..8973d61fcc 100644 --- a/internal/api/grpc/user/v2beta/user_test.go +++ b/internal/api/grpc/user/v2beta/user_test.go @@ -322,7 +322,9 @@ func Test_idpIntentToIDPIntentPb(t *testing.T) { t.Run(tt.name, func(t *testing.T) { got, err := idpIntentToIDPIntentPb(tt.args.intent, tt.args.alg) require.ErrorIs(t, err, tt.res.err) - grpc.AllFieldsEqual(t, tt.res.resp.ProtoReflect(), got.ProtoReflect(), grpc.CustomMappers) + if tt.res.resp != nil { + grpc.AllFieldsEqual(t, tt.res.resp.ProtoReflect(), got.Msg.ProtoReflect(), grpc.CustomMappers) + } }) } } diff --git a/internal/api/grpc/webkey/v2/integration_test/webkey_integration_test.go b/internal/api/grpc/webkey/v2/integration_test/webkey_integration_test.go new file mode 100644 index 0000000000..48777927cf --- /dev/null +++ b/internal/api/grpc/webkey/v2/integration_test/webkey_integration_test.go @@ -0,0 +1,216 @@ +//go:build integration + +package webkey_test + +import ( + "context" + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/integration" + "github.com/zitadel/zitadel/pkg/grpc/webkey/v2" +) + +var ( + CTX context.Context +) + +func TestMain(m *testing.M) { + os.Exit(func() int { + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) + defer cancel() + CTX = ctx + return m.Run() + }()) +} + +func TestServer_ListWebKeys(t *testing.T) { + instance, iamCtx, creationDate := createInstance(t) + // After the feature is first enabled, we can expect 2 generated keys with the default config. + checkWebKeyListState(iamCtx, t, instance, 2, "", &webkey.WebKey_Rsa{ + Rsa: &webkey.RSA{ + Bits: webkey.RSABits_RSA_BITS_2048, + Hasher: webkey.RSAHasher_RSA_HASHER_SHA256, + }, + }, creationDate) +} + +func TestServer_CreateWebKey(t *testing.T) { + instance, iamCtx, creationDate := createInstance(t) + client := instance.Client.WebKeyV2 + + _, err := client.CreateWebKey(iamCtx, &webkey.CreateWebKeyRequest{ + Key: &webkey.CreateWebKeyRequest_Rsa{ + Rsa: &webkey.RSA{ + Bits: webkey.RSABits_RSA_BITS_2048, + Hasher: webkey.RSAHasher_RSA_HASHER_SHA256, + }, + }, + }) + require.NoError(t, err) + + checkWebKeyListState(iamCtx, t, instance, 3, "", &webkey.WebKey_Rsa{ + Rsa: &webkey.RSA{ + Bits: webkey.RSABits_RSA_BITS_2048, + Hasher: webkey.RSAHasher_RSA_HASHER_SHA256, + }, + }, creationDate) +} + +func TestServer_ActivateWebKey(t *testing.T) { + instance, iamCtx, creationDate := createInstance(t) + client := instance.Client.WebKeyV2 + + resp, err := client.CreateWebKey(iamCtx, &webkey.CreateWebKeyRequest{ + Key: &webkey.CreateWebKeyRequest_Rsa{ + Rsa: &webkey.RSA{ + Bits: webkey.RSABits_RSA_BITS_2048, + Hasher: webkey.RSAHasher_RSA_HASHER_SHA256, + }, + }, + }) + require.NoError(t, err) + + _, err = client.ActivateWebKey(iamCtx, &webkey.ActivateWebKeyRequest{ + Id: resp.GetId(), + }) + require.NoError(t, err) + + checkWebKeyListState(iamCtx, t, instance, 3, resp.GetId(), &webkey.WebKey_Rsa{ + Rsa: &webkey.RSA{ + Bits: webkey.RSABits_RSA_BITS_2048, + Hasher: webkey.RSAHasher_RSA_HASHER_SHA256, + }, + }, creationDate) +} + +func TestServer_DeleteWebKey(t *testing.T) { + instance, iamCtx, creationDate := createInstance(t) + client := instance.Client.WebKeyV2 + + keyIDs := make([]string, 2) + for i := 0; i < 2; i++ { + resp, err := client.CreateWebKey(iamCtx, &webkey.CreateWebKeyRequest{ + Key: &webkey.CreateWebKeyRequest_Rsa{ + Rsa: &webkey.RSA{ + Bits: webkey.RSABits_RSA_BITS_2048, + Hasher: webkey.RSAHasher_RSA_HASHER_SHA256, + }, + }, + }) + require.NoError(t, err) + keyIDs[i] = resp.GetId() + } + _, err := client.ActivateWebKey(iamCtx, &webkey.ActivateWebKeyRequest{ + Id: keyIDs[0], + }) + require.NoError(t, err) + + ok := t.Run("cannot delete active key", func(t *testing.T) { + _, err := client.DeleteWebKey(iamCtx, &webkey.DeleteWebKeyRequest{ + Id: keyIDs[0], + }) + require.Error(t, err) + s := status.Convert(err) + assert.Equal(t, codes.FailedPrecondition, s.Code()) + assert.Contains(t, s.Message(), "COMMAND-Chai1") + }) + if !ok { + return + } + + start := time.Now() + ok = t.Run("delete inactive key", func(t *testing.T) { + resp, err := client.DeleteWebKey(iamCtx, &webkey.DeleteWebKeyRequest{ + Id: keyIDs[1], + }) + require.NoError(t, err) + require.WithinRange(t, resp.GetDeletionDate().AsTime(), start, time.Now()) + }) + if !ok { + return + } + + ok = t.Run("delete inactive key again", func(t *testing.T) { + resp, err := client.DeleteWebKey(iamCtx, &webkey.DeleteWebKeyRequest{ + Id: keyIDs[1], + }) + require.NoError(t, err) + require.WithinRange(t, resp.GetDeletionDate().AsTime(), start, time.Now()) + }) + if !ok { + return + } + + ok = t.Run("delete not existing key", func(t *testing.T) { + resp, err := client.DeleteWebKey(iamCtx, &webkey.DeleteWebKeyRequest{ + Id: "not-existing", + }) + require.NoError(t, err) + require.Nil(t, resp.DeletionDate) + }) + if !ok { + return + } + + // There are 2 keys from feature setup, +2 created, -1 deleted = 3 + checkWebKeyListState(iamCtx, t, instance, 3, keyIDs[0], &webkey.WebKey_Rsa{ + Rsa: &webkey.RSA{ + Bits: webkey.RSABits_RSA_BITS_2048, + Hasher: webkey.RSAHasher_RSA_HASHER_SHA256, + }, + }, creationDate) +} + +func createInstance(t *testing.T) (*integration.Instance, context.Context, *timestamppb.Timestamp) { + instance := integration.NewInstance(CTX) + creationDate := timestamppb.Now() + iamCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(iamCTX, time.Minute) + assert.EventuallyWithT(t, func(collect *assert.CollectT) { + resp, err := instance.Client.WebKeyV2.ListWebKeys(iamCTX, &webkey.ListWebKeysRequest{}) + assert.NoError(collect, err) + assert.Len(collect, resp.GetWebKeys(), 2) + + }, retryDuration, tick) + + return instance, iamCTX, creationDate +} + +func checkWebKeyListState(ctx context.Context, t *testing.T, instance *integration.Instance, nKeys int, expectActiveKeyID string, config any, creationDate *timestamppb.Timestamp) { + t.Helper() + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, time.Minute) + assert.EventuallyWithT(t, func(collect *assert.CollectT) { + resp, err := instance.Client.WebKeyV2.ListWebKeys(ctx, &webkey.ListWebKeysRequest{}) + require.NoError(collect, err) + list := resp.GetWebKeys() + assert.Len(collect, list, nKeys) + + now := time.Now() + var gotActiveKeyID string + for _, key := range list { + assert.WithinRange(collect, key.GetCreationDate().AsTime(), now.Add(-time.Minute), now.Add(time.Minute)) + assert.WithinRange(collect, key.GetChangeDate().AsTime(), now.Add(-time.Minute), now.Add(time.Minute)) + assert.NotEqual(collect, webkey.State_STATE_UNSPECIFIED, key.GetState()) + assert.NotEqual(collect, webkey.State_STATE_REMOVED, key.GetState()) + assert.Equal(collect, config, key.GetKey()) + + if key.GetState() == webkey.State_STATE_ACTIVE { + gotActiveKeyID = key.GetId() + } + } + assert.NotEmpty(collect, gotActiveKeyID) + if expectActiveKeyID != "" { + assert.Equal(collect, expectActiveKeyID, gotActiveKeyID) + } + }, retryDuration, tick) +} diff --git a/internal/api/grpc/webkey/v2/server.go b/internal/api/grpc/webkey/v2/server.go new file mode 100644 index 0000000000..a62c29e2b9 --- /dev/null +++ b/internal/api/grpc/webkey/v2/server.go @@ -0,0 +1,51 @@ +package webkey + +import ( + "net/http" + + "connectrpc.com/connect" + "google.golang.org/protobuf/reflect/protoreflect" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/pkg/grpc/webkey/v2" + "github.com/zitadel/zitadel/pkg/grpc/webkey/v2/webkeyconnect" +) + +var _ webkeyconnect.WebKeyServiceHandler = (*Server)(nil) + +type Server struct { + command *command.Commands + query *query.Queries +} + +func CreateServer( + command *command.Commands, + query *query.Queries, +) *Server { + return &Server{ + command: command, + query: query, + } +} + +func (s *Server) AppName() string { + return webkey.WebKeyService_ServiceDesc.ServiceName +} + +func (s *Server) MethodPrefix() string { + return webkey.WebKeyService_ServiceDesc.ServiceName +} + +func (s *Server) AuthMethods() authz.MethodMapping { + return webkey.WebKeyService_AuthMethods +} + +func (s *Server) RegisterConnectServer(interceptors ...connect.Interceptor) (string, http.Handler) { + return webkeyconnect.NewWebKeyServiceHandler(s, connect.WithInterceptors(interceptors...)) +} + +func (s *Server) FileDescriptor() protoreflect.FileDescriptor { + return webkey.File_zitadel_webkey_v2_webkey_service_proto +} diff --git a/internal/api/grpc/webkey/v2/webkey.go b/internal/api/grpc/webkey/v2/webkey.go new file mode 100644 index 0000000000..d1a10a31d0 --- /dev/null +++ b/internal/api/grpc/webkey/v2/webkey.go @@ -0,0 +1,72 @@ +package webkey + +import ( + "context" + + "connectrpc.com/connect" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/telemetry/tracing" + "github.com/zitadel/zitadel/pkg/grpc/webkey/v2" +) + +func (s *Server) CreateWebKey(ctx context.Context, req *connect.Request[webkey.CreateWebKeyRequest]) (_ *connect.Response[webkey.CreateWebKeyResponse], err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + webKey, err := s.command.CreateWebKey(ctx, createWebKeyRequestToConfig(req.Msg)) + if err != nil { + return nil, err + } + + return connect.NewResponse(&webkey.CreateWebKeyResponse{ + Id: webKey.KeyID, + CreationDate: timestamppb.New(webKey.ObjectDetails.EventDate), + }), nil +} + +func (s *Server) ActivateWebKey(ctx context.Context, req *connect.Request[webkey.ActivateWebKeyRequest]) (_ *connect.Response[webkey.ActivateWebKeyResponse], err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + details, err := s.command.ActivateWebKey(ctx, req.Msg.GetId()) + if err != nil { + return nil, err + } + + return connect.NewResponse(&webkey.ActivateWebKeyResponse{ + ChangeDate: timestamppb.New(details.EventDate), + }), nil +} + +func (s *Server) DeleteWebKey(ctx context.Context, req *connect.Request[webkey.DeleteWebKeyRequest]) (_ *connect.Response[webkey.DeleteWebKeyResponse], err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + deletedAt, err := s.command.DeleteWebKey(ctx, req.Msg.GetId()) + if err != nil { + return nil, err + } + + var deletionDate *timestamppb.Timestamp + if !deletedAt.IsZero() { + deletionDate = timestamppb.New(deletedAt) + } + return connect.NewResponse(&webkey.DeleteWebKeyResponse{ + DeletionDate: deletionDate, + }), nil +} + +func (s *Server) ListWebKeys(ctx context.Context, _ *connect.Request[webkey.ListWebKeysRequest]) (_ *connect.Response[webkey.ListWebKeysResponse], err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + list, err := s.query.ListWebKeys(ctx) + if err != nil { + return nil, err + } + + return connect.NewResponse(&webkey.ListWebKeysResponse{ + WebKeys: webKeyDetailsListToPb(list), + }), nil +} diff --git a/internal/api/grpc/webkey/v2/webkey_converter.go b/internal/api/grpc/webkey/v2/webkey_converter.go new file mode 100644 index 0000000000..7ee7fbce05 --- /dev/null +++ b/internal/api/grpc/webkey/v2/webkey_converter.go @@ -0,0 +1,170 @@ +package webkey + +import ( + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/pkg/grpc/webkey/v2" +) + +func createWebKeyRequestToConfig(req *webkey.CreateWebKeyRequest) crypto.WebKeyConfig { + switch config := req.GetKey().(type) { + case *webkey.CreateWebKeyRequest_Rsa: + return rsaToCrypto(config.Rsa) + case *webkey.CreateWebKeyRequest_Ecdsa: + return ecdsaToCrypto(config.Ecdsa) + case *webkey.CreateWebKeyRequest_Ed25519: + return new(crypto.WebKeyED25519Config) + default: + return rsaToCrypto(nil) + } +} + +func rsaToCrypto(config *webkey.RSA) *crypto.WebKeyRSAConfig { + out := new(crypto.WebKeyRSAConfig) + + switch config.GetBits() { + case webkey.RSABits_RSA_BITS_UNSPECIFIED: + out.Bits = crypto.RSABits2048 + case webkey.RSABits_RSA_BITS_2048: + out.Bits = crypto.RSABits2048 + case webkey.RSABits_RSA_BITS_3072: + out.Bits = crypto.RSABits3072 + case webkey.RSABits_RSA_BITS_4096: + out.Bits = crypto.RSABits4096 + default: + out.Bits = crypto.RSABits2048 + } + + switch config.GetHasher() { + case webkey.RSAHasher_RSA_HASHER_UNSPECIFIED: + out.Hasher = crypto.RSAHasherSHA256 + case webkey.RSAHasher_RSA_HASHER_SHA256: + out.Hasher = crypto.RSAHasherSHA256 + case webkey.RSAHasher_RSA_HASHER_SHA384: + out.Hasher = crypto.RSAHasherSHA384 + case webkey.RSAHasher_RSA_HASHER_SHA512: + out.Hasher = crypto.RSAHasherSHA512 + default: + out.Hasher = crypto.RSAHasherSHA256 + } + + return out +} + +func ecdsaToCrypto(config *webkey.ECDSA) *crypto.WebKeyECDSAConfig { + out := new(crypto.WebKeyECDSAConfig) + + switch config.GetCurve() { + case webkey.ECDSACurve_ECDSA_CURVE_UNSPECIFIED: + out.Curve = crypto.EllipticCurveP256 + case webkey.ECDSACurve_ECDSA_CURVE_P256: + out.Curve = crypto.EllipticCurveP256 + case webkey.ECDSACurve_ECDSA_CURVE_P384: + out.Curve = crypto.EllipticCurveP384 + case webkey.ECDSACurve_ECDSA_CURVE_P512: + out.Curve = crypto.EllipticCurveP512 + default: + out.Curve = crypto.EllipticCurveP256 + } + + return out +} + +func webKeyDetailsListToPb(list []query.WebKeyDetails) []*webkey.WebKey { + out := make([]*webkey.WebKey, len(list)) + for i := range list { + out[i] = webKeyDetailsToPb(&list[i]) + } + return out +} + +func webKeyDetailsToPb(details *query.WebKeyDetails) *webkey.WebKey { + out := &webkey.WebKey{ + Id: details.KeyID, + CreationDate: timestamppb.New(details.CreationDate), + ChangeDate: timestamppb.New(details.ChangeDate), + State: webKeyStateToPb(details.State), + } + + switch config := details.Config.(type) { + case *crypto.WebKeyRSAConfig: + out.Key = &webkey.WebKey_Rsa{ + Rsa: webKeyRSAConfigToPb(config), + } + case *crypto.WebKeyECDSAConfig: + out.Key = &webkey.WebKey_Ecdsa{ + Ecdsa: webKeyECDSAConfigToPb(config), + } + case *crypto.WebKeyED25519Config: + out.Key = &webkey.WebKey_Ed25519{ + Ed25519: new(webkey.ED25519), + } + } + + return out +} + +func webKeyStateToPb(state domain.WebKeyState) webkey.State { + switch state { + case domain.WebKeyStateUnspecified: + return webkey.State_STATE_UNSPECIFIED + case domain.WebKeyStateInitial: + return webkey.State_STATE_INITIAL + case domain.WebKeyStateActive: + return webkey.State_STATE_ACTIVE + case domain.WebKeyStateInactive: + return webkey.State_STATE_INACTIVE + case domain.WebKeyStateRemoved: + return webkey.State_STATE_REMOVED + default: + return webkey.State_STATE_UNSPECIFIED + } +} + +func webKeyRSAConfigToPb(config *crypto.WebKeyRSAConfig) *webkey.RSA { + out := new(webkey.RSA) + + switch config.Bits { + case crypto.RSABitsUnspecified: + out.Bits = webkey.RSABits_RSA_BITS_UNSPECIFIED + case crypto.RSABits2048: + out.Bits = webkey.RSABits_RSA_BITS_2048 + case crypto.RSABits3072: + out.Bits = webkey.RSABits_RSA_BITS_3072 + case crypto.RSABits4096: + out.Bits = webkey.RSABits_RSA_BITS_4096 + } + + switch config.Hasher { + case crypto.RSAHasherUnspecified: + out.Hasher = webkey.RSAHasher_RSA_HASHER_UNSPECIFIED + case crypto.RSAHasherSHA256: + out.Hasher = webkey.RSAHasher_RSA_HASHER_SHA256 + case crypto.RSAHasherSHA384: + out.Hasher = webkey.RSAHasher_RSA_HASHER_SHA384 + case crypto.RSAHasherSHA512: + out.Hasher = webkey.RSAHasher_RSA_HASHER_SHA512 + } + + return out +} + +func webKeyECDSAConfigToPb(config *crypto.WebKeyECDSAConfig) *webkey.ECDSA { + out := new(webkey.ECDSA) + + switch config.Curve { + case crypto.EllipticCurveUnspecified: + out.Curve = webkey.ECDSACurve_ECDSA_CURVE_UNSPECIFIED + case crypto.EllipticCurveP256: + out.Curve = webkey.ECDSACurve_ECDSA_CURVE_P256 + case crypto.EllipticCurveP384: + out.Curve = webkey.ECDSACurve_ECDSA_CURVE_P384 + case crypto.EllipticCurveP512: + out.Curve = webkey.ECDSACurve_ECDSA_CURVE_P512 + } + + return out +} diff --git a/internal/api/grpc/webkey/v2/webkey_converter_test.go b/internal/api/grpc/webkey/v2/webkey_converter_test.go new file mode 100644 index 0000000000..e7387d96ad --- /dev/null +++ b/internal/api/grpc/webkey/v2/webkey_converter_test.go @@ -0,0 +1,494 @@ +package webkey + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/pkg/grpc/webkey/v2" +) + +func Test_createWebKeyRequestToConfig(t *testing.T) { + type args struct { + req *webkey.CreateWebKeyRequest + } + tests := []struct { + name string + args args + want crypto.WebKeyConfig + }{ + { + name: "RSA", + args: args{&webkey.CreateWebKeyRequest{ + Key: &webkey.CreateWebKeyRequest_Rsa{ + Rsa: &webkey.RSA{ + Bits: webkey.RSABits_RSA_BITS_3072, + Hasher: webkey.RSAHasher_RSA_HASHER_SHA384, + }, + }, + }}, + want: &crypto.WebKeyRSAConfig{ + Bits: crypto.RSABits3072, + Hasher: crypto.RSAHasherSHA384, + }, + }, + { + name: "ECDSA", + args: args{&webkey.CreateWebKeyRequest{ + Key: &webkey.CreateWebKeyRequest_Ecdsa{ + Ecdsa: &webkey.ECDSA{ + Curve: webkey.ECDSACurve_ECDSA_CURVE_P384, + }, + }, + }}, + want: &crypto.WebKeyECDSAConfig{ + Curve: crypto.EllipticCurveP384, + }, + }, + { + name: "ED25519", + args: args{&webkey.CreateWebKeyRequest{ + Key: &webkey.CreateWebKeyRequest_Ed25519{ + Ed25519: &webkey.ED25519{}, + }, + }}, + want: &crypto.WebKeyED25519Config{}, + }, + { + name: "default", + args: args{&webkey.CreateWebKeyRequest{}}, + want: &crypto.WebKeyRSAConfig{ + Bits: crypto.RSABits2048, + Hasher: crypto.RSAHasherSHA256, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := createWebKeyRequestToConfig(tt.args.req) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_webKeyRSAConfigToCrypto(t *testing.T) { + type args struct { + config *webkey.RSA + } + tests := []struct { + name string + args args + want *crypto.WebKeyRSAConfig + }{ + { + name: "unspecified", + args: args{&webkey.RSA{ + Bits: webkey.RSABits_RSA_BITS_UNSPECIFIED, + Hasher: webkey.RSAHasher_RSA_HASHER_UNSPECIFIED, + }}, + want: &crypto.WebKeyRSAConfig{ + Bits: crypto.RSABits2048, + Hasher: crypto.RSAHasherSHA256, + }, + }, + { + name: "2048, RSA256", + args: args{&webkey.RSA{ + Bits: webkey.RSABits_RSA_BITS_2048, + Hasher: webkey.RSAHasher_RSA_HASHER_SHA256, + }}, + want: &crypto.WebKeyRSAConfig{ + Bits: crypto.RSABits2048, + Hasher: crypto.RSAHasherSHA256, + }, + }, + { + name: "3072, RSA384", + args: args{&webkey.RSA{ + Bits: webkey.RSABits_RSA_BITS_3072, + Hasher: webkey.RSAHasher_RSA_HASHER_SHA384, + }}, + want: &crypto.WebKeyRSAConfig{ + Bits: crypto.RSABits3072, + Hasher: crypto.RSAHasherSHA384, + }, + }, + { + name: "4096, RSA512", + args: args{&webkey.RSA{ + Bits: webkey.RSABits_RSA_BITS_4096, + Hasher: webkey.RSAHasher_RSA_HASHER_SHA512, + }}, + want: &crypto.WebKeyRSAConfig{ + Bits: crypto.RSABits4096, + Hasher: crypto.RSAHasherSHA512, + }, + }, + { + name: "invalid", + args: args{&webkey.RSA{ + Bits: 99, + Hasher: 99, + }}, + want: &crypto.WebKeyRSAConfig{ + Bits: crypto.RSABits2048, + Hasher: crypto.RSAHasherSHA256, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := rsaToCrypto(tt.args.config) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_webKeyECDSAConfigToCrypto(t *testing.T) { + type args struct { + config *webkey.ECDSA + } + tests := []struct { + name string + args args + want *crypto.WebKeyECDSAConfig + }{ + { + name: "unspecified", + args: args{&webkey.ECDSA{ + Curve: webkey.ECDSACurve_ECDSA_CURVE_UNSPECIFIED, + }}, + want: &crypto.WebKeyECDSAConfig{ + Curve: crypto.EllipticCurveP256, + }, + }, + { + name: "P256", + args: args{&webkey.ECDSA{ + Curve: webkey.ECDSACurve_ECDSA_CURVE_P256, + }}, + want: &crypto.WebKeyECDSAConfig{ + Curve: crypto.EllipticCurveP256, + }, + }, + { + name: "P384", + args: args{&webkey.ECDSA{ + Curve: webkey.ECDSACurve_ECDSA_CURVE_P384, + }}, + want: &crypto.WebKeyECDSAConfig{ + Curve: crypto.EllipticCurveP384, + }, + }, + { + name: "P512", + args: args{&webkey.ECDSA{ + Curve: webkey.ECDSACurve_ECDSA_CURVE_P512, + }}, + want: &crypto.WebKeyECDSAConfig{ + Curve: crypto.EllipticCurveP512, + }, + }, + { + name: "invalid", + args: args{&webkey.ECDSA{ + Curve: 99, + }}, + want: &crypto.WebKeyECDSAConfig{ + Curve: crypto.EllipticCurveP256, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ecdsaToCrypto(tt.args.config) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_webKeyDetailsListToPb(t *testing.T) { + list := []query.WebKeyDetails{ + { + KeyID: "key1", + CreationDate: time.Unix(123, 456), + ChangeDate: time.Unix(789, 0), + Sequence: 123, + State: domain.WebKeyStateActive, + Config: &crypto.WebKeyRSAConfig{ + Bits: crypto.RSABits3072, + Hasher: crypto.RSAHasherSHA384, + }, + }, + { + KeyID: "key2", + CreationDate: time.Unix(123, 456), + ChangeDate: time.Unix(789, 0), + Sequence: 123, + State: domain.WebKeyStateActive, + Config: &crypto.WebKeyED25519Config{}, + }, + } + want := []*webkey.WebKey{ + { + Id: "key1", + CreationDate: ×tamppb.Timestamp{Seconds: 123, Nanos: 456}, + ChangeDate: ×tamppb.Timestamp{Seconds: 789, Nanos: 0}, + State: webkey.State_STATE_ACTIVE, + Key: &webkey.WebKey_Rsa{ + Rsa: &webkey.RSA{ + Bits: webkey.RSABits_RSA_BITS_3072, + Hasher: webkey.RSAHasher_RSA_HASHER_SHA384, + }, + }, + }, + { + Id: "key2", + CreationDate: ×tamppb.Timestamp{Seconds: 123, Nanos: 456}, + ChangeDate: ×tamppb.Timestamp{Seconds: 789, Nanos: 0}, + State: webkey.State_STATE_ACTIVE, + Key: &webkey.WebKey_Ed25519{ + Ed25519: &webkey.ED25519{}, + }, + }, + } + got := webKeyDetailsListToPb(list) + assert.Equal(t, want, got) +} + +func Test_webKeyDetailsToPb(t *testing.T) { + type args struct { + details *query.WebKeyDetails + } + tests := []struct { + name string + args args + want *webkey.WebKey + }{ + { + name: "RSA", + args: args{&query.WebKeyDetails{ + KeyID: "keyID", + CreationDate: time.Unix(123, 456), + ChangeDate: time.Unix(789, 0), + Sequence: 123, + State: domain.WebKeyStateActive, + Config: &crypto.WebKeyRSAConfig{ + Bits: crypto.RSABits3072, + Hasher: crypto.RSAHasherSHA384, + }, + }}, + want: &webkey.WebKey{ + Id: "keyID", + CreationDate: ×tamppb.Timestamp{Seconds: 123, Nanos: 456}, + ChangeDate: ×tamppb.Timestamp{Seconds: 789, Nanos: 0}, + State: webkey.State_STATE_ACTIVE, + Key: &webkey.WebKey_Rsa{ + Rsa: &webkey.RSA{ + Bits: webkey.RSABits_RSA_BITS_3072, + Hasher: webkey.RSAHasher_RSA_HASHER_SHA384, + }, + }, + }, + }, + { + name: "ECDSA", + args: args{&query.WebKeyDetails{ + KeyID: "keyID", + CreationDate: time.Unix(123, 456), + ChangeDate: time.Unix(789, 0), + Sequence: 123, + State: domain.WebKeyStateActive, + Config: &crypto.WebKeyECDSAConfig{ + Curve: crypto.EllipticCurveP384, + }, + }}, + want: &webkey.WebKey{ + Id: "keyID", + CreationDate: ×tamppb.Timestamp{Seconds: 123, Nanos: 456}, + ChangeDate: ×tamppb.Timestamp{Seconds: 789, Nanos: 0}, + State: webkey.State_STATE_ACTIVE, + Key: &webkey.WebKey_Ecdsa{ + Ecdsa: &webkey.ECDSA{ + Curve: webkey.ECDSACurve_ECDSA_CURVE_P384, + }, + }, + }, + }, + { + name: "ED25519", + args: args{&query.WebKeyDetails{ + KeyID: "keyID", + CreationDate: time.Unix(123, 456), + ChangeDate: time.Unix(789, 0), + Sequence: 123, + State: domain.WebKeyStateActive, + Config: &crypto.WebKeyED25519Config{}, + }}, + want: &webkey.WebKey{ + Id: "keyID", + CreationDate: ×tamppb.Timestamp{Seconds: 123, Nanos: 456}, + ChangeDate: ×tamppb.Timestamp{Seconds: 789, Nanos: 0}, + State: webkey.State_STATE_ACTIVE, + Key: &webkey.WebKey_Ed25519{ + Ed25519: &webkey.ED25519{}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := webKeyDetailsToPb(tt.args.details) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_webKeyStateToPb(t *testing.T) { + type args struct { + state domain.WebKeyState + } + tests := []struct { + name string + args args + want webkey.State + }{ + { + name: "unspecified", + args: args{domain.WebKeyStateUnspecified}, + want: webkey.State_STATE_UNSPECIFIED, + }, + { + name: "initial", + args: args{domain.WebKeyStateInitial}, + want: webkey.State_STATE_INITIAL, + }, + { + name: "active", + args: args{domain.WebKeyStateActive}, + want: webkey.State_STATE_ACTIVE, + }, + { + name: "inactive", + args: args{domain.WebKeyStateInactive}, + want: webkey.State_STATE_INACTIVE, + }, + { + name: "removed", + args: args{domain.WebKeyStateRemoved}, + want: webkey.State_STATE_REMOVED, + }, + { + name: "invalid", + args: args{99}, + want: webkey.State_STATE_UNSPECIFIED, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := webKeyStateToPb(tt.args.state) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_webKeyRSAConfigToPb(t *testing.T) { + type args struct { + config *crypto.WebKeyRSAConfig + } + tests := []struct { + name string + args args + want *webkey.RSA + }{ + { + name: "2048, RSA256", + args: args{&crypto.WebKeyRSAConfig{ + Bits: crypto.RSABits2048, + Hasher: crypto.RSAHasherSHA256, + }}, + want: &webkey.RSA{ + Bits: webkey.RSABits_RSA_BITS_2048, + Hasher: webkey.RSAHasher_RSA_HASHER_SHA256, + }, + }, + { + name: "3072, RSA384", + args: args{&crypto.WebKeyRSAConfig{ + Bits: crypto.RSABits3072, + Hasher: crypto.RSAHasherSHA384, + }}, + want: &webkey.RSA{ + Bits: webkey.RSABits_RSA_BITS_3072, + Hasher: webkey.RSAHasher_RSA_HASHER_SHA384, + }, + }, + { + name: "4096, RSA512", + args: args{&crypto.WebKeyRSAConfig{ + Bits: crypto.RSABits4096, + Hasher: crypto.RSAHasherSHA512, + }}, + want: &webkey.RSA{ + Bits: webkey.RSABits_RSA_BITS_4096, + Hasher: webkey.RSAHasher_RSA_HASHER_SHA512, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := webKeyRSAConfigToPb(tt.args.config) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_webKeyECDSAConfigToPb(t *testing.T) { + type args struct { + config *crypto.WebKeyECDSAConfig + } + tests := []struct { + name string + args args + want *webkey.ECDSA + }{ + { + name: "P256", + args: args{&crypto.WebKeyECDSAConfig{ + Curve: crypto.EllipticCurveP256, + }}, + want: &webkey.ECDSA{ + Curve: webkey.ECDSACurve_ECDSA_CURVE_P256, + }, + }, + { + name: "P384", + args: args{&crypto.WebKeyECDSAConfig{ + Curve: crypto.EllipticCurveP384, + }}, + want: &webkey.ECDSA{ + Curve: webkey.ECDSACurve_ECDSA_CURVE_P384, + }, + }, + { + name: "P512", + args: args{&crypto.WebKeyECDSAConfig{ + Curve: crypto.EllipticCurveP512, + }}, + want: &webkey.ECDSA{ + Curve: webkey.ECDSACurve_ECDSA_CURVE_P512, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := webKeyECDSAConfigToPb(tt.args.config) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/internal/api/grpc/webkey/v2beta/integration_test/webkey_integration_test.go b/internal/api/grpc/webkey/v2beta/integration_test/webkey_integration_test.go index 002669c233..0cbf629b43 100644 --- a/internal/api/grpc/webkey/v2beta/integration_test/webkey_integration_test.go +++ b/internal/api/grpc/webkey/v2beta/integration_test/webkey_integration_test.go @@ -12,11 +12,9 @@ import ( "github.com/stretchr/testify/require" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/timestamppb" "github.com/zitadel/zitadel/internal/integration" - "github.com/zitadel/zitadel/pkg/grpc/feature/v2" webkey "github.com/zitadel/zitadel/pkg/grpc/webkey/v2beta" ) @@ -33,34 +31,8 @@ func TestMain(m *testing.M) { }()) } -func TestServer_Feature_Disabled(t *testing.T) { - instance, iamCtx, _ := createInstance(t, false) - client := instance.Client.WebKeyV2Beta - - t.Run("CreateWebKey", func(t *testing.T) { - _, err := client.CreateWebKey(iamCtx, &webkey.CreateWebKeyRequest{}) - assertFeatureDisabledError(t, err) - }) - t.Run("ActivateWebKey", func(t *testing.T) { - _, err := client.ActivateWebKey(iamCtx, &webkey.ActivateWebKeyRequest{ - Id: "1", - }) - assertFeatureDisabledError(t, err) - }) - t.Run("DeleteWebKey", func(t *testing.T) { - _, err := client.DeleteWebKey(iamCtx, &webkey.DeleteWebKeyRequest{ - Id: "1", - }) - assertFeatureDisabledError(t, err) - }) - t.Run("ListWebKeys", func(t *testing.T) { - _, err := client.ListWebKeys(iamCtx, &webkey.ListWebKeysRequest{}) - assertFeatureDisabledError(t, err) - }) -} - func TestServer_ListWebKeys(t *testing.T) { - instance, iamCtx, creationDate := createInstance(t, true) + instance, iamCtx, creationDate := createInstance(t) // After the feature is first enabled, we can expect 2 generated keys with the default config. checkWebKeyListState(iamCtx, t, instance, 2, "", &webkey.WebKey_Rsa{ Rsa: &webkey.RSA{ @@ -71,7 +43,7 @@ func TestServer_ListWebKeys(t *testing.T) { } func TestServer_CreateWebKey(t *testing.T) { - instance, iamCtx, creationDate := createInstance(t, true) + instance, iamCtx, creationDate := createInstance(t) client := instance.Client.WebKeyV2Beta _, err := client.CreateWebKey(iamCtx, &webkey.CreateWebKeyRequest{ @@ -93,7 +65,7 @@ func TestServer_CreateWebKey(t *testing.T) { } func TestServer_ActivateWebKey(t *testing.T) { - instance, iamCtx, creationDate := createInstance(t, true) + instance, iamCtx, creationDate := createInstance(t) client := instance.Client.WebKeyV2Beta resp, err := client.CreateWebKey(iamCtx, &webkey.CreateWebKeyRequest{ @@ -120,7 +92,7 @@ func TestServer_ActivateWebKey(t *testing.T) { } func TestServer_DeleteWebKey(t *testing.T) { - instance, iamCtx, creationDate := createInstance(t, true) + instance, iamCtx, creationDate := createInstance(t) client := instance.Client.WebKeyV2Beta keyIDs := make([]string, 2) @@ -197,40 +169,22 @@ func TestServer_DeleteWebKey(t *testing.T) { }, creationDate) } -func createInstance(t *testing.T, enableFeature bool) (*integration.Instance, context.Context, *timestamppb.Timestamp) { +func createInstance(t *testing.T) (*integration.Instance, context.Context, *timestamppb.Timestamp) { instance := integration.NewInstance(CTX) creationDate := timestamppb.Now() iamCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) - if enableFeature { - _, err := instance.Client.FeatureV2.SetInstanceFeatures(iamCTX, &feature.SetInstanceFeaturesRequest{ - WebKey: proto.Bool(true), - }) - require.NoError(t, err) - } - retryDuration, tick := integration.WaitForAndTickWithMaxDuration(iamCTX, time.Minute) assert.EventuallyWithT(t, func(collect *assert.CollectT) { resp, err := instance.Client.WebKeyV2Beta.ListWebKeys(iamCTX, &webkey.ListWebKeysRequest{}) - if enableFeature { - assert.NoError(collect, err) - assert.Len(collect, resp.GetWebKeys(), 2) - } else { - assert.Error(collect, err) - } + assert.NoError(collect, err) + assert.Len(collect, resp.GetWebKeys(), 2) + }, retryDuration, tick) return instance, iamCTX, creationDate } -func assertFeatureDisabledError(t *testing.T, err error) { - t.Helper() - require.Error(t, err) - s := status.Convert(err) - assert.Equal(t, codes.FailedPrecondition, s.Code()) - assert.Contains(t, s.Message(), "WEBKEY-Ohx6E") -} - func checkWebKeyListState(ctx context.Context, t *testing.T, instance *integration.Instance, nKeys int, expectActiveKeyID string, config any, creationDate *timestamppb.Timestamp) { t.Helper() diff --git a/internal/api/grpc/webkey/v2beta/server.go b/internal/api/grpc/webkey/v2beta/server.go index 0d4ddb19c8..b000e98104 100644 --- a/internal/api/grpc/webkey/v2beta/server.go +++ b/internal/api/grpc/webkey/v2beta/server.go @@ -1,17 +1,22 @@ package webkey import ( - "google.golang.org/grpc" + "net/http" + + "connectrpc.com/connect" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/grpc/server" "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/query" webkey "github.com/zitadel/zitadel/pkg/grpc/webkey/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/webkey/v2beta/webkeyconnect" ) +var _ webkeyconnect.WebKeyServiceHandler = (*Server)(nil) + type Server struct { - webkey.UnimplementedWebKeyServiceServer command *command.Commands query *query.Queries } @@ -26,10 +31,6 @@ func CreateServer( } } -func (s *Server) RegisterServer(grpcServer *grpc.Server) { - webkey.RegisterWebKeyServiceServer(grpcServer, s) -} - func (s *Server) AppName() string { return webkey.WebKeyService_ServiceDesc.ServiceName } @@ -45,3 +46,11 @@ func (s *Server) AuthMethods() authz.MethodMapping { func (s *Server) RegisterGateway() server.RegisterGatewayFunc { return webkey.RegisterWebKeyServiceHandler } + +func (s *Server) RegisterConnectServer(interceptors ...connect.Interceptor) (string, http.Handler) { + return webkeyconnect.NewWebKeyServiceHandler(s, connect.WithInterceptors(interceptors...)) +} + +func (s *Server) FileDescriptor() protoreflect.FileDescriptor { + return webkey.File_zitadel_webkey_v2beta_webkey_service_proto +} diff --git a/internal/api/grpc/webkey/v2beta/webkey.go b/internal/api/grpc/webkey/v2beta/webkey.go index d45288dff2..fa37cc32e3 100644 --- a/internal/api/grpc/webkey/v2beta/webkey.go +++ b/internal/api/grpc/webkey/v2beta/webkey.go @@ -3,57 +3,47 @@ package webkey import ( "context" + "connectrpc.com/connect" "google.golang.org/protobuf/types/known/timestamppb" - "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/telemetry/tracing" - "github.com/zitadel/zitadel/internal/zerrors" webkey "github.com/zitadel/zitadel/pkg/grpc/webkey/v2beta" ) -func (s *Server) CreateWebKey(ctx context.Context, req *webkey.CreateWebKeyRequest) (_ *webkey.CreateWebKeyResponse, err error) { +func (s *Server) CreateWebKey(ctx context.Context, req *connect.Request[webkey.CreateWebKeyRequest]) (_ *connect.Response[webkey.CreateWebKeyResponse], err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - if err = checkWebKeyFeature(ctx); err != nil { - return nil, err - } - webKey, err := s.command.CreateWebKey(ctx, createWebKeyRequestToConfig(req)) + webKey, err := s.command.CreateWebKey(ctx, createWebKeyRequestToConfig(req.Msg)) if err != nil { return nil, err } - return &webkey.CreateWebKeyResponse{ + return connect.NewResponse(&webkey.CreateWebKeyResponse{ Id: webKey.KeyID, CreationDate: timestamppb.New(webKey.ObjectDetails.EventDate), - }, nil + }), nil } -func (s *Server) ActivateWebKey(ctx context.Context, req *webkey.ActivateWebKeyRequest) (_ *webkey.ActivateWebKeyResponse, err error) { +func (s *Server) ActivateWebKey(ctx context.Context, req *connect.Request[webkey.ActivateWebKeyRequest]) (_ *connect.Response[webkey.ActivateWebKeyResponse], err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - if err = checkWebKeyFeature(ctx); err != nil { - return nil, err - } - details, err := s.command.ActivateWebKey(ctx, req.GetId()) + details, err := s.command.ActivateWebKey(ctx, req.Msg.GetId()) if err != nil { return nil, err } - return &webkey.ActivateWebKeyResponse{ + return connect.NewResponse(&webkey.ActivateWebKeyResponse{ ChangeDate: timestamppb.New(details.EventDate), - }, nil + }), nil } -func (s *Server) DeleteWebKey(ctx context.Context, req *webkey.DeleteWebKeyRequest) (_ *webkey.DeleteWebKeyResponse, err error) { +func (s *Server) DeleteWebKey(ctx context.Context, req *connect.Request[webkey.DeleteWebKeyRequest]) (_ *connect.Response[webkey.DeleteWebKeyResponse], err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - if err = checkWebKeyFeature(ctx); err != nil { - return nil, err - } - deletedAt, err := s.command.DeleteWebKey(ctx, req.GetId()) + deletedAt, err := s.command.DeleteWebKey(ctx, req.Msg.GetId()) if err != nil { return nil, err } @@ -62,31 +52,21 @@ func (s *Server) DeleteWebKey(ctx context.Context, req *webkey.DeleteWebKeyReque if !deletedAt.IsZero() { deletionDate = timestamppb.New(deletedAt) } - return &webkey.DeleteWebKeyResponse{ + return connect.NewResponse(&webkey.DeleteWebKeyResponse{ DeletionDate: deletionDate, - }, nil + }), nil } -func (s *Server) ListWebKeys(ctx context.Context, _ *webkey.ListWebKeysRequest) (_ *webkey.ListWebKeysResponse, err error) { +func (s *Server) ListWebKeys(ctx context.Context, _ *connect.Request[webkey.ListWebKeysRequest]) (_ *connect.Response[webkey.ListWebKeysResponse], err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - if err = checkWebKeyFeature(ctx); err != nil { - return nil, err - } list, err := s.query.ListWebKeys(ctx) if err != nil { return nil, err } - return &webkey.ListWebKeysResponse{ + return connect.NewResponse(&webkey.ListWebKeysResponse{ WebKeys: webKeyDetailsListToPb(list), - }, nil -} - -func checkWebKeyFeature(ctx context.Context) error { - if !authz.GetFeatures(ctx).WebKey { - return zerrors.ThrowPreconditionFailed(nil, "WEBKEY-Ohx6E", "Errors.WebKey.FeatureDisabled") - } - return nil + }), nil } diff --git a/internal/api/http/header.go b/internal/api/http/header.go index a6c2818728..fce5330df8 100644 --- a/internal/api/http/header.go +++ b/internal/api/http/header.go @@ -10,30 +10,36 @@ import ( ) const ( - Authorization = "authorization" - Accept = "accept" - AcceptLanguage = "accept-language" - CacheControl = "cache-control" - ContentType = "content-type" - ContentLength = "content-length" - ContentLocation = "content-location" - Expires = "expires" - Location = "location" - Origin = "origin" - Pragma = "pragma" - UserAgentHeader = "user-agent" - ForwardedFor = "x-forwarded-for" - ForwardedHost = "x-forwarded-host" - ForwardedProto = "x-forwarded-proto" - Forwarded = "forwarded" - ZitadelForwarded = "x-zitadel-forwarded" - XUserAgent = "x-user-agent" - XGrpcWeb = "x-grpc-web" - XRequestedWith = "x-requested-with" - XRobotsTag = "x-robots-tag" - IfNoneMatch = "If-None-Match" - LastModified = "Last-Modified" - Etag = "Etag" + Authorization = "authorization" + Accept = "accept" + AcceptLanguage = "accept-language" + CacheControl = "cache-control" + ContentType = "content-type" + ContentLength = "content-length" + ContentLocation = "content-location" + Expires = "expires" + Location = "location" + Origin = "origin" + Pragma = "pragma" + UserAgentHeader = "user-agent" + ForwardedFor = "x-forwarded-for" + ForwardedHost = "x-forwarded-host" + ForwardedProto = "x-forwarded-proto" + Forwarded = "forwarded" + ZitadelForwarded = "x-zitadel-forwarded" + XUserAgent = "x-user-agent" + XGrpcWeb = "x-grpc-web" + XRequestedWith = "x-requested-with" + XRobotsTag = "x-robots-tag" + IfNoneMatch = "if-none-match" + LastModified = "last-modified" + Etag = "etag" + GRPCTimeout = "grpc-timeout" + ConnectProtocolVersion = "connect-protocol-version" + ConnectTimeoutMS = "connect-timeout-ms" + GrpcStatus = "grpc-status" + GrpcMessage = "grpc-message" + GrpcStatusDetailsBin = "grpc-status-details-bin" ContentSecurityPolicy = "content-security-policy" XXSSProtection = "x-xss-protection" diff --git a/internal/api/http/middleware/cors_interceptor.go b/internal/api/http/middleware/cors_interceptor.go index 02f1924956..fb5503909d 100644 --- a/internal/api/http/middleware/cors_interceptor.go +++ b/internal/api/http/middleware/cors_interceptor.go @@ -21,6 +21,9 @@ var ( http_utils.XUserAgent, http_utils.XGrpcWeb, http_utils.XRequestedWith, + http_utils.ConnectProtocolVersion, + http_utils.ConnectTimeoutMS, + http_utils.GRPCTimeout, }, AllowedMethods: []string{ http.MethodOptions, @@ -34,6 +37,9 @@ var ( ExposedHeaders: []string{ http_utils.Location, http_utils.ContentLength, + http_utils.GrpcStatus, + http_utils.GrpcMessage, + http_utils.GrpcStatusDetailsBin, }, AllowOriginFunc: func(_ string) bool { return true diff --git a/internal/api/oidc/access_token.go b/internal/api/oidc/access_token.go index 66da6e3ccf..08337bb5af 100644 --- a/internal/api/oidc/access_token.go +++ b/internal/api/oidc/access_token.go @@ -11,7 +11,6 @@ import ( "github.com/zitadel/oidc/v3/pkg/op" "golang.org/x/text/language" - "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query" @@ -53,7 +52,7 @@ func (s *Server) verifyAccessToken(ctx context.Context, tkn string) (_ *accessTo tokenID, subject = split[0], split[1] } else { verifier := op.NewAccessTokenVerifier(op.IssuerFromContext(ctx), s.accessTokenKeySet, - op.WithSupportedAccessTokenSigningAlgorithms(supportedSigningAlgs(ctx)...), + op.WithSupportedAccessTokenSigningAlgorithms(supportedSigningAlgs()...), ) claims, err := op.VerifyAccessToken[*oidc.AccessTokenClaims](ctx, tkn, verifier) if err != nil { @@ -122,7 +121,7 @@ func (s *Server) assertClientScopesForPAT(ctx context.Context, token *accessToke if err != nil { return zerrors.ThrowInternal(err, "OIDC-Cyc78", "Errors.Internal") } - roles, err := s.query.SearchProjectRoles(ctx, authz.GetFeatures(ctx).TriggerIntrospectionProjections, &query.ProjectRoleSearchQueries{Queries: []query.SearchQuery{projectIDQuery}}) + roles, err := s.query.SearchProjectRoles(ctx, false, &query.ProjectRoleSearchQueries{Queries: []query.SearchQuery{projectIDQuery}}, nil) if err != nil { return err } diff --git a/internal/api/oidc/auth_request.go b/internal/api/oidc/auth_request.go index f9ea092627..152176a59c 100644 --- a/internal/api/oidc/auth_request.go +++ b/internal/api/oidc/auth_request.go @@ -13,6 +13,7 @@ import ( "github.com/zitadel/logging" "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/oidc/v3/pkg/op" + "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/api/authz" http_utils "github.com/zitadel/zitadel/internal/api/http" @@ -24,13 +25,14 @@ import ( "github.com/zitadel/zitadel/internal/domain/federatedlogout" "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/telemetry/tracing" - "github.com/zitadel/zitadel/internal/user/model" "github.com/zitadel/zitadel/internal/zerrors" ) const ( LoginClientHeader = "x-zitadel-login-client" LoginPostLogoutRedirectParam = "post_logout_redirect" + LoginLogoutHintParam = "logout_hint" + LoginUILocalesParam = "ui_locales" LoginPath = "/login" LogoutPath = "/logout" LogoutDonePath = "/logout/done" @@ -216,11 +218,11 @@ func (o *OPStorage) SaveAuthCode(ctx context.Context, id, code string) (err erro return o.repo.SaveAuthCode(ctx, id, code, userAgentID) } -func (o *OPStorage) DeleteAuthRequest(ctx context.Context, id string) (err error) { +func (o *OPStorage) DeleteAuthRequest(context.Context, string) error { panic(o.panicErr("DeleteAuthRequest")) } -func (o *OPStorage) CreateAccessToken(ctx context.Context, req op.TokenRequest) (string, time.Time, error) { +func (o *OPStorage) CreateAccessToken(context.Context, op.TokenRequest) (string, time.Time, error) { panic(o.panicErr("CreateAccessToken")) } @@ -284,14 +286,19 @@ func (o *OPStorage) TerminateSessionFromRequest(ctx context.Context, endSessionR // we'll redirect to the UI (V2) and let it decide which session to terminate // // If there's no id_token_hint and for v1 logins, we handle them separately - if endSessionRequest.IDTokenHintClaims == nil && - (authz.GetFeatures(ctx).LoginV2.Required || headers.Get(LoginClientHeader) != "") { + if endSessionRequest.IDTokenHintClaims == nil && (authz.GetFeatures(ctx).LoginV2.Required || headers.Get(LoginClientHeader) != "") { redirectURI := v2PostLogoutRedirectURI(endSessionRequest.RedirectURI) - // if no base uri is set, fallback to the default configured in the runtime config - if authz.GetFeatures(ctx).LoginV2.BaseURI == nil || authz.GetFeatures(ctx).LoginV2.BaseURI.String() == "" { - return o.defaultLogoutURLV2 + redirectURI, nil + logoutURI := authz.GetFeatures(ctx).LoginV2.BaseURI + // if no logout uri is set, fallback to the default configured in the runtime config + if logoutURI == nil || logoutURI.String() == "" { + logoutURI, err = url.Parse(o.defaultLogoutURLV2) + if err != nil { + return "", err + } + } else { + logoutURI = logoutURI.JoinPath(LogoutPath) } - return buildLoginV2LogoutURL(authz.GetFeatures(ctx).LoginV2.BaseURI, redirectURI), nil + return buildLoginV2LogoutURL(logoutURI, redirectURI, endSessionRequest.LogoutHint, endSessionRequest.UILocales), nil } // V1: @@ -368,12 +375,25 @@ func (o *OPStorage) federatedLogout(ctx context.Context, sessionID string, postL return login.ExternalLogoutPath(sessionID) } -func buildLoginV2LogoutURL(baseURI *url.URL, redirectURI string) string { - baseURI.JoinPath(LogoutPath) - q := baseURI.Query() +func buildLoginV2LogoutURL(logoutURI *url.URL, redirectURI, logoutHint string, uiLocales []language.Tag) string { + if strings.HasSuffix(logoutURI.Path, "/") && len(logoutURI.Path) > 1 { + logoutURI.Path = strings.TrimSuffix(logoutURI.Path, "/") + } + + q := logoutURI.Query() q.Set(LoginPostLogoutRedirectParam, redirectURI) - baseURI.RawQuery = q.Encode() - return baseURI.String() + if logoutHint != "" { + q.Set(LoginLogoutHintParam, logoutHint) + } + if len(uiLocales) > 0 { + locales := make([]string, len(uiLocales)) + for i, locale := range uiLocales { + locales[i] = locale.String() + } + q.Set(LoginUILocalesParam, strings.Join(locales, " ")) + } + logoutURI.RawQuery = q.Encode() + return logoutURI.String() } // v2PostLogoutRedirectURI will take care that the post_logout_redirect_uri is correctly set for v2 logins. @@ -492,34 +512,6 @@ func (o *OPStorage) GetRefreshTokenInfo(ctx context.Context, clientID string, to return refreshToken.UserID, refreshToken.ID, nil } -func (o *OPStorage) assertProjectRoleScopes(ctx context.Context, clientID string, scopes []string) ([]string, error) { - for _, scope := range scopes { - if strings.HasPrefix(scope, ScopeProjectRolePrefix) { - return scopes, nil - } - } - - project, err := o.query.ProjectByOIDCClientID(ctx, clientID) - if err != nil { - return nil, zerrors.ThrowPreconditionFailed(nil, "OIDC-w4wIn", "Errors.Internal") - } - if !project.ProjectRoleAssertion { - return scopes, nil - } - projectIDQuery, err := query.NewProjectRoleProjectIDSearchQuery(project.ID) - if err != nil { - return nil, zerrors.ThrowInternal(err, "OIDC-Cyc78", "Errors.Internal") - } - roles, err := o.query.SearchProjectRoles(ctx, true, &query.ProjectRoleSearchQueries{Queries: []query.SearchQuery{projectIDQuery}}) - if err != nil { - return nil, err - } - for _, role := range roles.ProjectRoles { - scopes = append(scopes, ScopeProjectRolePrefix+role.Key) - } - return scopes, nil -} - func (o *OPStorage) assertProjectRoleScopesByProject(ctx context.Context, project *query.Project, scopes []string) ([]string, error) { for _, scope := range scopes { if strings.HasPrefix(scope, ScopeProjectRolePrefix) { @@ -533,7 +525,7 @@ func (o *OPStorage) assertProjectRoleScopesByProject(ctx context.Context, projec if err != nil { return nil, zerrors.ThrowInternal(err, "OIDC-Cyc78", "Errors.Internal") } - roles, err := o.query.SearchProjectRoles(ctx, true, &query.ProjectRoleSearchQueries{Queries: []query.SearchQuery{projectIDQuery}}) + roles, err := o.query.SearchProjectRoles(ctx, true, &query.ProjectRoleSearchQueries{Queries: []query.SearchQuery{projectIDQuery}}, nil) if err != nil { return nil, err } @@ -543,22 +535,6 @@ func (o *OPStorage) assertProjectRoleScopesByProject(ctx context.Context, projec return scopes, nil } -func (o *OPStorage) assertClientScopesForPAT(ctx context.Context, token *model.TokenView, clientID, projectID string) error { - token.Audience = append(token.Audience, clientID) - projectIDQuery, err := query.NewProjectRoleProjectIDSearchQuery(projectID) - if err != nil { - return zerrors.ThrowInternal(err, "OIDC-Cyc78", "Errors.Internal") - } - roles, err := o.query.SearchProjectRoles(ctx, true, &query.ProjectRoleSearchQueries{Queries: []query.SearchQuery{projectIDQuery}}) - if err != nil { - return err - } - for _, role := range roles.ProjectRoles { - token.Scopes = append(token.Scopes, ScopeProjectRolePrefix+role.Key) - } - return nil -} - func setContextUserSystem(ctx context.Context) context.Context { data := authz.CtxData{ UserID: "SYSTEM", diff --git a/internal/api/oidc/auth_request_converter.go b/internal/api/oidc/auth_request_converter.go index 2144ca8ba1..064af20de0 100644 --- a/internal/api/oidc/auth_request_converter.go +++ b/internal/api/oidc/auth_request_converter.go @@ -140,13 +140,8 @@ func HttpHeadersFromContext(ctx context.Context) (userAgent, acceptLang string) if !ok { return } - if agents, ok := ctxHeaders[http_utils.UserAgentHeader]; ok { - userAgent = agents[0] - } - if langs, ok := ctxHeaders[http_utils.AcceptLanguage]; ok { - acceptLang = langs[0] - } - return userAgent, acceptLang + return ctxHeaders.Get(http_utils.UserAgentHeader), + ctxHeaders.Get(http_utils.AcceptLanguage) } func IpFromContext(ctx context.Context) net.IP { diff --git a/internal/api/oidc/auth_request_test.go b/internal/api/oidc/auth_request_test.go new file mode 100644 index 0000000000..0210ead49e --- /dev/null +++ b/internal/api/oidc/auth_request_test.go @@ -0,0 +1,98 @@ +package oidc + +import ( + "net/url" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/text/language" +) + +func TestBuildLoginV2LogoutURL(t *testing.T) { + t.Parallel() + + tt := []struct { + testName string + logoutURIStr string + redirectURI string + logoutHint string + uiLocales []language.Tag + expectedParams map[string]string + }{ + { + testName: "basic with only redirectURI", + logoutURIStr: "https://example.com/logout", + redirectURI: "https://client/cb", + expectedParams: map[string]string{ + "post_logout_redirect": "https://client/cb", + }, + }, + { + testName: "with logout hint", + logoutURIStr: "https://example.com/logout", + redirectURI: "https://client/cb", + logoutHint: "user@example.com", + expectedParams: map[string]string{ + "post_logout_redirect": "https://client/cb", + "logout_hint": "user@example.com", + }, + }, + { + testName: "with ui_locales", + logoutURIStr: "https://example.com/logout", + redirectURI: "https://client/cb", + uiLocales: []language.Tag{language.English, language.Italian}, + expectedParams: map[string]string{ + "post_logout_redirect": "https://client/cb", + "ui_locales": "en it", + }, + }, + { + testName: "with all params", + logoutURIStr: "https://example.com/logout", + redirectURI: "https://client/cb", + logoutHint: "logoutme", + uiLocales: []language.Tag{language.Make("de-CH"), language.Make("fr")}, + expectedParams: map[string]string{ + "post_logout_redirect": "https://client/cb", + "logout_hint": "logoutme", + "ui_locales": "de-CH fr", + }, + }, + { + testName: "base with trailing slash", + logoutURIStr: "https://example.com/logout/", + redirectURI: "https://client/cb", + expectedParams: map[string]string{ + "post_logout_redirect": "https://client/cb", + }, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + // t.Parallel() + + // Given + logoutURI, err := url.Parse(tc.logoutURIStr) + require.NoError(t, err) + + // When + got := buildLoginV2LogoutURL(logoutURI, tc.redirectURI, tc.logoutHint, tc.uiLocales) + + // Then + gotURL, err := url.Parse(got) + require.NoError(t, err) + require.NotContains(t, gotURL.String(), "/logout/") + + q := gotURL.Query() + // Ensure no unexpected params + require.Len(t, q, len(tc.expectedParams)) + + for k, v := range tc.expectedParams { + assert.Equal(t, v, q.Get(k)) + } + }) + } +} diff --git a/internal/api/oidc/client.go b/internal/api/oidc/client.go index 08ed8c31b9..6a2639f213 100644 --- a/internal/api/oidc/client.go +++ b/internal/api/oidc/client.go @@ -2,25 +2,17 @@ package oidc import ( "context" - "encoding/base64" "encoding/json" - "fmt" "slices" "strings" "time" - "github.com/dop251/goja" "github.com/go-jose/go-jose/v4" - "github.com/zitadel/logging" "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/oidc/v3/pkg/op" "github.com/zitadel/zitadel/internal/actions" - "github.com/zitadel/zitadel/internal/actions/object" "github.com/zitadel/zitadel/internal/api/authz" - api_http "github.com/zitadel/zitadel/internal/api/http" - "github.com/zitadel/zitadel/internal/command" - "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -44,6 +36,9 @@ const ( oidcCtx = "oidc" ) +// GetClientByClientID implements the op.Storage interface to retrieve an OIDC client by its ID. +// +// TODO: Still used for Auth request creation for v1 login. func (o *OPStorage) GetClientByClientID(ctx context.Context, id string) (_ op.Client, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { @@ -57,819 +52,44 @@ func (o *OPStorage) GetClientByClientID(ctx context.Context, id string) (_ op.Cl return ClientFromBusiness(client, o.defaultLoginURL, o.defaultLoginURLV2), nil } -func (o *OPStorage) GetKeyByIDAndClientID(ctx context.Context, keyID, userID string) (_ *jose.JSONWebKey, err error) { - return o.GetKeyByIDAndIssuer(ctx, keyID, userID) +func (o *OPStorage) GetKeyByIDAndClientID(context.Context, string, string) (*jose.JSONWebKey, error) { + panic(o.panicErr("GetKeyByIDAndClientID")) } -func (o *OPStorage) GetKeyByIDAndIssuer(ctx context.Context, keyID, issuer string) (_ *jose.JSONWebKey, err error) { - ctx, span := tracing.NewSpan(ctx) - defer func() { - err = oidcError(err) - span.EndWithError(err) - }() - publicKeyData, err := o.query.GetAuthNKeyPublicKeyByIDAndIdentifier(ctx, keyID, issuer) - if err != nil { - return nil, err - } - publicKey, err := crypto.BytesToPublicKey(publicKeyData) - if err != nil { - return nil, err - } - return &jose.JSONWebKey{ - KeyID: keyID, - Use: "sig", - Key: publicKey, - }, nil +func (o *OPStorage) ValidateJWTProfileScopes(context.Context, string, []string) ([]string, error) { + panic(o.panicErr("ValidateJWTProfileScopes")) } -func (o *OPStorage) ValidateJWTProfileScopes(ctx context.Context, subject string, scopes []string) (_ []string, err error) { - ctx, span := tracing.NewSpan(ctx) - defer func() { - err = oidcError(err) - span.EndWithError(err) - }() - user, err := o.query.GetUserByID(ctx, true, subject) - if err != nil { - return nil, err - } - return o.checkOrgScopes(ctx, user, scopes) +func (o *OPStorage) AuthorizeClientIDSecret(context.Context, string, string) error { + panic(o.panicErr("AuthorizeClientIDSecret")) } -func (o *OPStorage) AuthorizeClientIDSecret(ctx context.Context, id string, secret string) (err error) { - ctx, span := tracing.NewSpan(ctx) - defer func() { - err = oidcError(err) - span.EndWithError(err) - }() - ctx = authz.SetCtxData(ctx, authz.CtxData{ - UserID: oidcCtx, - OrgID: oidcCtx, - }) - app, err := o.query.AppByClientID(ctx, id) - if err != nil { - return err - } - if app.OIDCConfig != nil { - return o.command.VerifyOIDCClientSecret(ctx, app.ProjectID, app.ID, secret) - } - return o.command.VerifyAPIClientSecret(ctx, app.ProjectID, app.ID, secret) +func (o *OPStorage) SetUserinfoFromToken(context.Context, *oidc.UserInfo, string, string, string) error { + panic(o.panicErr("SetUserinfoFromToken")) } -func (o *OPStorage) SetUserinfoFromToken(ctx context.Context, userInfo *oidc.UserInfo, tokenID, subject, origin string) (err error) { - ctx, span := tracing.NewSpan(ctx) - defer func() { - err = oidcError(err) - span.EndWithError(err) - }() - - if strings.HasPrefix(tokenID, command.IDPrefixV2) { - token, err := o.query.ActiveAccessTokenByToken(ctx, tokenID) - if err != nil { - return err - } - if err = o.isOriginAllowed(ctx, token.ClientID, origin); err != nil { - return err - } - return o.setUserinfo(ctx, userInfo, token.UserID, token.ClientID, token.Scope, nil) - } - - token, err := o.repo.TokenByIDs(ctx, subject, tokenID) - if err != nil { - return zerrors.ThrowPermissionDenied(nil, "OIDC-Dsfb2", "token is not valid or has expired") - } - if token.ApplicationID != "" { - if err = o.isOriginAllowed(ctx, token.ApplicationID, origin); err != nil { - return err - } - } - return o.setUserinfo(ctx, userInfo, token.UserID, token.ApplicationID, token.Scopes, nil) +func (o *OPStorage) SetUserinfoFromScopes(context.Context, *oidc.UserInfo, string, string, []string) error { + panic(o.panicErr("SetUserinfoFromScopes")) } -func (o *OPStorage) SetUserinfoFromScopes(ctx context.Context, userInfo *oidc.UserInfo, userID, applicationID string, scopes []string) (err error) { - ctx, span := tracing.NewSpan(ctx) - defer func() { - err = oidcError(err) - span.EndWithError(err) - }() - if applicationID != "" { - app, err := o.query.AppByOIDCClientID(ctx, applicationID) - if err != nil { - return err - } - if app.OIDCConfig.AssertIDTokenRole { - scopes, err = o.assertProjectRoleScopes(ctx, applicationID, scopes) - if err != nil { - return zerrors.ThrowPreconditionFailed(err, "OIDC-Dfe2s", "Errors.Internal") - } - } - } - return o.setUserinfo(ctx, userInfo, userID, applicationID, scopes, nil) +func (o *OPStorage) SetUserinfoFromRequest(context.Context, *oidc.UserInfo, op.IDTokenRequest, []string) error { + panic(o.panicErr("SetUserinfoFromRequest")) } -// SetUserinfoFromRequest extends the SetUserinfoFromScopes during the id_token generation. -// This is required for V2 tokens to be able to set the sessionID (`sid`) claim. -func (o *OPStorage) SetUserinfoFromRequest(ctx context.Context, userinfo *oidc.UserInfo, request op.IDTokenRequest, _ []string) error { - switch t := request.(type) { - case *AuthRequestV2: - userinfo.AppendClaims("sid", t.SessionID) - case *RefreshTokenRequestV2: - userinfo.AppendClaims("sid", t.SessionID) - } - return nil +func (o *OPStorage) SetIntrospectionFromToken(context.Context, *oidc.IntrospectionResponse, string, string, string) error { + panic(o.panicErr("SetIntrospectionFromToken")) } -func (o *OPStorage) SetIntrospectionFromToken(ctx context.Context, introspection *oidc.IntrospectionResponse, tokenID, subject, clientID string) (err error) { - ctx, span := tracing.NewSpan(ctx) - defer func() { - err = oidcError(err) - span.EndWithError(err) - }() - - if strings.HasPrefix(tokenID, command.IDPrefixV2) { - token, err := o.query.ActiveAccessTokenByToken(ctx, tokenID) - if err != nil { - return err - } - projectID, err := o.query.ProjectIDFromClientID(ctx, clientID) - if err != nil { - return zerrors.ThrowPermissionDenied(nil, "OIDC-Adfg5", "client not found") - } - return o.introspect(ctx, introspection, - tokenID, token.UserID, token.ClientID, clientID, projectID, - token.Audience, token.Scope, - token.AccessTokenCreation, token.AccessTokenExpiration) - } - - token, err := o.repo.TokenByIDs(ctx, subject, tokenID) - if err != nil { - return zerrors.ThrowPermissionDenied(nil, "OIDC-Dsfb2", "token is not valid or has expired") - } - projectID, err := o.query.ProjectIDFromClientID(ctx, clientID) - if err != nil { - return zerrors.ThrowPermissionDenied(nil, "OIDC-Adfg5", "client not found") - } - if token.IsPAT { - err = o.assertClientScopesForPAT(ctx, token, clientID, projectID) - if err != nil { - return zerrors.ThrowPreconditionFailed(err, "OIDC-AGefw", "Errors.Internal") - } - } - return o.introspect(ctx, introspection, - token.ID, token.UserID, token.ApplicationID, clientID, projectID, - token.Audience, token.Scopes, - token.CreationDate, token.Expiration) +func (o *OPStorage) ClientCredentialsTokenRequest(context.Context, string, []string) (op.TokenRequest, error) { + panic(o.panicErr("ClientCredentialsTokenRequest")) } -func (o *OPStorage) ClientCredentialsTokenRequest(ctx context.Context, clientID string, scope []string) (_ op.TokenRequest, err error) { - ctx, span := tracing.NewSpan(ctx) - defer func() { - err = oidcError(err) - span.EndWithError(err) - }() - user, err := o.query.GetUserByLoginName(ctx, false, clientID) - if err != nil { - return nil, err - } - scope, err = o.checkOrgScopes(ctx, user, scope) - if err != nil { - return nil, err - } - audience := domain.AddAudScopeToAudience(ctx, nil, scope) - return &clientCredentialsRequest{ - sub: user.ID, - scopes: scope, - audience: audience, - }, nil -} - -// ClientCredentials method is kept to keep the storage interface implemented. -// However, it should never be called as the VerifyClient method on the Server is overridden. func (o *OPStorage) ClientCredentials(context.Context, string, string) (op.Client, error) { - return nil, zerrors.ThrowInternal(nil, "OIDC-Su8So", "Errors.Internal") + panic(o.panicErr("ClientCredentials")) } -// isOriginAllowed checks whether a call by the client to the endpoint is allowed from the provided origin -// if no origin is provided, no error will be returned -func (o *OPStorage) isOriginAllowed(ctx context.Context, clientID, origin string) error { - if origin == "" { - return nil - } - app, err := o.query.AppByOIDCClientID(ctx, clientID) - if err != nil { - return err - } - if api_http.IsOriginAllowed(app.OIDCConfig.AllowedOrigins, origin) { - return nil - } - return zerrors.ThrowPermissionDenied(nil, "OIDC-da1f3", "origin is not allowed") -} - -func (o *OPStorage) introspect( - ctx context.Context, - introspection *oidc.IntrospectionResponse, - tokenID, subject, tokenClientID, introspectionClientID, introspectionProjectID string, - audience, scope []string, - tokenCreation, tokenExpiration time.Time, -) (err error) { - ctx, span := tracing.NewSpan(ctx) - defer func() { span.EndWithError(err) }() - - for _, aud := range audience { - if aud == introspectionClientID || aud == introspectionProjectID { - userInfo := new(oidc.UserInfo) - err = o.setUserinfo(ctx, userInfo, subject, introspectionClientID, scope, []string{introspectionProjectID}) - if err != nil { - return err - } - introspection.SetUserInfo(userInfo) - introspection.Scope = scope - introspection.ClientID = tokenClientID - introspection.TokenType = oidc.BearerToken - introspection.Expiration = oidc.FromTime(tokenExpiration) - introspection.IssuedAt = oidc.FromTime(tokenCreation) - introspection.NotBefore = oidc.FromTime(tokenCreation) - introspection.Audience = audience - introspection.Issuer = op.IssuerFromContext(ctx) - introspection.JWTID = tokenID - return nil - } - } - return zerrors.ThrowPermissionDenied(nil, "OIDC-sdg3G", "token is not valid for this client") -} - -func (o *OPStorage) checkOrgScopes(ctx context.Context, user *query.User, scopes []string) ([]string, error) { - for i := len(scopes) - 1; i >= 0; i-- { - scope := scopes[i] - if strings.HasPrefix(scope, domain.OrgDomainPrimaryScope) { - var orgID string - org, err := o.query.OrgByPrimaryDomain(ctx, strings.TrimPrefix(scope, domain.OrgDomainPrimaryScope)) - if err == nil { - orgID = org.ID - } - if orgID != user.ResourceOwner { - scopes[i] = scopes[len(scopes)-1] - scopes[len(scopes)-1] = "" - scopes = scopes[:len(scopes)-1] - } - } - if strings.HasPrefix(scope, domain.OrgIDScope) { - if strings.TrimPrefix(scope, domain.OrgIDScope) != user.ResourceOwner { - scopes[i] = scopes[len(scopes)-1] - scopes[len(scopes)-1] = "" - scopes = scopes[:len(scopes)-1] - } - } - } - return scopes, nil -} - -func (o *OPStorage) setUserinfo(ctx context.Context, userInfo *oidc.UserInfo, userID, applicationID string, scopes []string, roleAudience []string) (err error) { - ctx, span := tracing.NewSpan(ctx) - defer func() { span.EndWithError(err) }() - user, err := o.query.GetUserByID(ctx, true, userID) - if err != nil { - return err - } - if user.State != domain.UserStateActive { - return zerrors.ThrowUnauthenticated(nil, "OIDC-S3tha", "Errors.Users.NotActive") - } - var allRoles bool - roles := make([]string, 0) - for _, scope := range scopes { - switch scope { - case oidc.ScopeOpenID: - userInfo.Subject = user.ID - case oidc.ScopeEmail: - setUserInfoEmail(userInfo, user) - case oidc.ScopeProfile: - o.setUserInfoProfile(ctx, userInfo, user) - case oidc.ScopePhone: - setUserInfoPhone(userInfo, user) - case oidc.ScopeAddress: - //TODO: handle address for human users as soon as implemented - case ScopeUserMetaData: - if err := o.setUserInfoMetadata(ctx, userInfo, userID); err != nil { - return err - } - case ScopeResourceOwner: - if err := o.setUserInfoResourceOwner(ctx, userInfo, userID); err != nil { - return err - } - case ScopeProjectsRoles: - allRoles = true - default: - if strings.HasPrefix(scope, ScopeProjectRolePrefix) { - roles = append(roles, strings.TrimPrefix(scope, ScopeProjectRolePrefix)) - } - if strings.HasPrefix(scope, domain.OrgDomainPrimaryScope) { - userInfo.AppendClaims(domain.OrgDomainPrimaryClaim, strings.TrimPrefix(scope, domain.OrgDomainPrimaryScope)) - } - if strings.HasPrefix(scope, domain.OrgIDScope) { - userInfo.AppendClaims(domain.OrgIDClaim, strings.TrimPrefix(scope, domain.OrgIDScope)) - if err := o.setUserInfoResourceOwner(ctx, userInfo, userID); err != nil { - return err - } - } - } - } - - // if all roles are requested take the audience for those from the scopes - if allRoles && len(roleAudience) == 0 { - roleAudience = domain.AddAudScopeToAudience(ctx, roleAudience, scopes) - } - - userGrants, projectRoles, err := o.assertRoles(ctx, userID, applicationID, roles, roleAudience) - if err != nil { - return err - } - o.setUserInfoRoleClaims(userInfo, projectRoles) - - return o.userinfoFlows(ctx, user, userGrants, userInfo) -} - -func (o *OPStorage) setUserInfoProfile(ctx context.Context, userInfo *oidc.UserInfo, user *query.User) { - userInfo.PreferredUsername = user.PreferredLoginName - userInfo.UpdatedAt = oidc.FromTime(user.ChangeDate) - if user.Machine != nil { - userInfo.Name = user.Machine.Name - return - } - userInfo.Name = user.Human.DisplayName - userInfo.FamilyName = user.Human.LastName - userInfo.GivenName = user.Human.FirstName - userInfo.Nickname = user.Human.NickName - userInfo.Gender = getGender(user.Human.Gender) - userInfo.Locale = oidc.NewLocale(user.Human.PreferredLanguage) - userInfo.Picture = domain.AvatarURL(o.assetAPIPrefix(ctx), user.ResourceOwner, user.Human.AvatarKey) -} - -func setUserInfoEmail(userInfo *oidc.UserInfo, user *query.User) { - if user.Human == nil { - return - } - userInfo.UserInfoEmail = oidc.UserInfoEmail{ - Email: string(user.Human.Email), - EmailVerified: oidc.Bool(user.Human.IsEmailVerified)} -} - -func setUserInfoPhone(userInfo *oidc.UserInfo, user *query.User) { - if user.Human == nil { - return - } - userInfo.UserInfoPhone = oidc.UserInfoPhone{ - PhoneNumber: string(user.Human.Phone), - PhoneNumberVerified: user.Human.IsPhoneVerified, - } -} - -func (o *OPStorage) setUserInfoMetadata(ctx context.Context, userInfo *oidc.UserInfo, userID string) error { - userMetaData, err := o.assertUserMetaData(ctx, userID) - if err != nil { - return err - } - if len(userMetaData) > 0 { - userInfo.AppendClaims(ClaimUserMetaData, userMetaData) - } - return nil -} - -func (o *OPStorage) setUserInfoResourceOwner(ctx context.Context, userInfo *oidc.UserInfo, userID string) error { - resourceOwnerClaims, err := o.assertUserResourceOwner(ctx, userID) - if err != nil { - return err - } - for claim, value := range resourceOwnerClaims { - userInfo.AppendClaims(claim, value) - } - return nil -} - -func (o *OPStorage) setUserInfoRoleClaims(userInfo *oidc.UserInfo, roles *projectsRoles) { - if roles != nil && len(roles.projects) > 0 { - if roles, ok := roles.projects[roles.requestProjectID]; ok { - userInfo.AppendClaims(ClaimProjectRoles, roles) - } - for projectID, roles := range roles.projects { - userInfo.AppendClaims(fmt.Sprintf(ClaimProjectRolesFormat, projectID), roles) - } - } -} - -func (o *OPStorage) userinfoFlows(ctx context.Context, user *query.User, userGrants *query.UserGrants, userInfo *oidc.UserInfo) error { - queriedActions, err := o.query.GetActiveActionsByFlowAndTriggerType(ctx, domain.FlowTypeCustomiseToken, domain.TriggerTypePreUserinfoCreation, user.ResourceOwner) - if err != nil { - return err - } - - ctxFields := actions.SetContextFields( - actions.SetFields("v1", - actions.SetFields("claims", userinfoClaims(userInfo)), - actions.SetFields("getUser", func(c *actions.FieldConfig) interface{} { - return func(call goja.FunctionCall) goja.Value { - return object.UserFromQuery(c, user) - } - }), - actions.SetFields("user", - actions.SetFields("getMetadata", func(c *actions.FieldConfig) interface{} { - return func(goja.FunctionCall) goja.Value { - resourceOwnerQuery, err := query.NewUserMetadataResourceOwnerSearchQuery(user.ResourceOwner) - if err != nil { - logging.WithError(err).Debug("unable to create search query") - panic(err) - } - metadata, err := o.query.SearchUserMetadata( - ctx, - true, - userInfo.Subject, - &query.UserMetadataSearchQueries{Queries: []query.SearchQuery{resourceOwnerQuery}}, - false, - ) - if err != nil { - logging.WithError(err).Info("unable to get md in action") - panic(err) - } - return object.UserMetadataListFromQuery(c, metadata) - } - }), - actions.SetFields("grants", - func(c *actions.FieldConfig) interface{} { - return object.UserGrantsFromQuery(ctx, o.query, c, userGrants) - }, - ), - ), - actions.SetFields("org", - actions.SetFields("getMetadata", func(c *actions.FieldConfig) interface{} { - return func(goja.FunctionCall) goja.Value { - return object.GetOrganizationMetadata(ctx, o.query, c, user.ResourceOwner) - } - }), - ), - ), - ) - - for _, action := range queriedActions { - actionCtx, cancel := context.WithTimeout(ctx, action.Timeout()) - claimLogs := []string{} - - apiFields := actions.WithAPIFields( - actions.SetFields("v1", - actions.SetFields("userinfo", - actions.SetFields("setClaim", func(key string, value interface{}) { - if strings.HasPrefix(key, ClaimPrefix) { - return - } - if userInfo.Claims[key] == nil { - userInfo.AppendClaims(key, value) - return - } - claimLogs = append(claimLogs, fmt.Sprintf("key %q already exists", key)) - }), - actions.SetFields("appendLogIntoClaims", func(entry string) { - claimLogs = append(claimLogs, entry) - }), - ), - actions.SetFields("claims", - actions.SetFields("setClaim", func(key string, value interface{}) { - if strings.HasPrefix(key, ClaimPrefix) { - return - } - if userInfo.Claims[key] == nil { - userInfo.AppendClaims(key, value) - return - } - claimLogs = append(claimLogs, fmt.Sprintf("key %q already exists", key)) - }), - actions.SetFields("appendLogIntoClaims", func(entry string) { - claimLogs = append(claimLogs, entry) - }), - ), - actions.SetFields("user", - actions.SetFields("setMetadata", func(call goja.FunctionCall) goja.Value { - if len(call.Arguments) != 2 { - panic("exactly 2 (key, value) arguments expected") - } - key := call.Arguments[0].Export().(string) - val := call.Arguments[1].Export() - - value, err := json.Marshal(val) - if err != nil { - logging.WithError(err).Debug("unable to marshal") - panic(err) - } - - metadata := &domain.Metadata{ - Key: key, - Value: value, - } - if _, err = o.command.SetUserMetadata(ctx, metadata, userInfo.Subject, user.ResourceOwner); err != nil { - logging.WithError(err).Info("unable to set md in action") - panic(err) - } - return nil - }), - ), - ), - ) - - err = actions.Run( - actionCtx, - ctxFields, - apiFields, - action.Script, - action.Name, - append(actions.ActionToOptions(action), actions.WithHTTP(actionCtx), actions.WithUUID(actionCtx))..., - ) - cancel() - if err != nil { - return err - } - if len(claimLogs) > 0 { - userInfo.AppendClaims(fmt.Sprintf(ClaimActionLogFormat, action.Name), claimLogs) - } - } - - return nil -} - -func (o *OPStorage) GetPrivateClaimsFromScopes(ctx context.Context, userID, clientID string, scopes []string) (claims map[string]interface{}, err error) { - ctx, span := tracing.NewSpan(ctx) - defer func() { - err = oidcError(err) - span.EndWithError(err) - }() - - roles := make([]string, 0) - var allRoles bool - for _, scope := range scopes { - switch scope { - case ScopeUserMetaData: - userMetaData, err := o.assertUserMetaData(ctx, userID) - if err != nil { - return nil, err - } - if len(userMetaData) > 0 { - claims = appendClaim(claims, ClaimUserMetaData, userMetaData) - } - case ScopeResourceOwner: - resourceOwnerClaims, err := o.assertUserResourceOwner(ctx, userID) - if err != nil { - return nil, err - } - for claim, value := range resourceOwnerClaims { - claims = appendClaim(claims, claim, value) - } - case ScopeProjectsRoles: - allRoles = true - } - if strings.HasPrefix(scope, ScopeProjectRolePrefix) { - roles = append(roles, strings.TrimPrefix(scope, ScopeProjectRolePrefix)) - } - if strings.HasPrefix(scope, domain.OrgDomainPrimaryScope) { - claims = appendClaim(claims, domain.OrgDomainPrimaryClaim, strings.TrimPrefix(scope, domain.OrgDomainPrimaryScope)) - } - if strings.HasPrefix(scope, domain.OrgIDScope) { - claims = appendClaim(claims, domain.OrgIDClaim, strings.TrimPrefix(scope, domain.OrgIDScope)) - resourceOwnerClaims, err := o.assertUserResourceOwner(ctx, userID) - if err != nil { - return nil, err - } - for claim, value := range resourceOwnerClaims { - claims = appendClaim(claims, claim, value) - } - } - } - - // If requested, use the audience as context for the roles, - // otherwise the project itself will be used - var roleAudience []string - if allRoles { - roleAudience = domain.AddAudScopeToAudience(ctx, roleAudience, scopes) - } - - userGrants, projectRoles, err := o.assertRoles(ctx, userID, clientID, roles, roleAudience) - if err != nil { - return nil, err - } - - if projectRoles != nil && len(projectRoles.projects) > 0 { - if roles, ok := projectRoles.projects[projectRoles.requestProjectID]; ok { - claims = appendClaim(claims, ClaimProjectRoles, roles) - } - for projectID, roles := range projectRoles.projects { - claims = appendClaim(claims, fmt.Sprintf(ClaimProjectRolesFormat, projectID), roles) - } - } - - return o.privateClaimsFlows(ctx, userID, userGrants, claims) -} - -func (o *OPStorage) privateClaimsFlows(ctx context.Context, userID string, userGrants *query.UserGrants, claims map[string]interface{}) (map[string]interface{}, error) { - user, err := o.query.GetUserByID(ctx, true, userID) - if err != nil { - return nil, err - } - queriedActions, err := o.query.GetActiveActionsByFlowAndTriggerType(ctx, domain.FlowTypeCustomiseToken, domain.TriggerTypePreAccessTokenCreation, user.ResourceOwner) - if err != nil { - return nil, err - } - - ctxFields := actions.SetContextFields( - actions.SetFields("v1", - actions.SetFields("claims", func(c *actions.FieldConfig) interface{} { - return c.Runtime.ToValue(claims) - }), - actions.SetFields("getUser", func(c *actions.FieldConfig) interface{} { - return func(call goja.FunctionCall) goja.Value { - return object.UserFromQuery(c, user) - } - }), - actions.SetFields("user", - actions.SetFields("getMetadata", func(c *actions.FieldConfig) interface{} { - return func(goja.FunctionCall) goja.Value { - resourceOwnerQuery, err := query.NewUserMetadataResourceOwnerSearchQuery(user.ResourceOwner) - if err != nil { - logging.WithError(err).Debug("unable to create search query") - panic(err) - } - metadata, err := o.query.SearchUserMetadata( - ctx, - true, - userID, - &query.UserMetadataSearchQueries{Queries: []query.SearchQuery{resourceOwnerQuery}}, - false, - ) - if err != nil { - logging.WithError(err).Info("unable to get md in action") - panic(err) - } - return object.UserMetadataListFromQuery(c, metadata) - } - }), - actions.SetFields("grants", func(c *actions.FieldConfig) interface{} { - return object.UserGrantsFromQuery(ctx, o.query, c, userGrants) - }), - ), - actions.SetFields("org", - actions.SetFields("getMetadata", func(c *actions.FieldConfig) interface{} { - return func(goja.FunctionCall) goja.Value { - return object.GetOrganizationMetadata(ctx, o.query, c, user.ResourceOwner) - } - }), - ), - ), - ) - - for _, action := range queriedActions { - claimLogs := []string{} - actionCtx, cancel := context.WithTimeout(ctx, action.Timeout()) - - apiFields := actions.WithAPIFields( - actions.SetFields("v1", - actions.SetFields("claims", - actions.SetFields("setClaim", func(key string, value interface{}) { - if strings.HasPrefix(key, ClaimPrefix) { - return - } - if _, ok := claims[key]; !ok { - claims = appendClaim(claims, key, value) - return - } - claimLogs = append(claimLogs, fmt.Sprintf("key %q already exists", key)) - }), - actions.SetFields("appendLogIntoClaims", func(entry string) { - claimLogs = append(claimLogs, entry) - }), - ), - actions.SetFields("user", - actions.SetFields("setMetadata", func(call goja.FunctionCall) goja.Value { - if len(call.Arguments) != 2 { - panic("exactly 2 (key, value) arguments expected") - } - key := call.Arguments[0].Export().(string) - val := call.Arguments[1].Export() - - value, err := json.Marshal(val) - if err != nil { - logging.WithError(err).Debug("unable to marshal") - panic(err) - } - - metadata := &domain.Metadata{ - Key: key, - Value: value, - } - if _, err = o.command.SetUserMetadata(ctx, metadata, userID, user.ResourceOwner); err != nil { - logging.WithError(err).Info("unable to set md in action") - panic(err) - } - return nil - }), - ), - ), - ) - - err = actions.Run( - actionCtx, - ctxFields, - apiFields, - action.Script, - action.Name, - append(actions.ActionToOptions(action), actions.WithHTTP(actionCtx), actions.WithUUID(actionCtx))..., - ) - cancel() - if err != nil { - return nil, err - } - if len(claimLogs) > 0 { - claims = appendClaim(claims, fmt.Sprintf(ClaimActionLogFormat, action.Name), claimLogs) - claimLogs = nil - } - } - - return claims, nil -} - -func (o *OPStorage) assertRoles(ctx context.Context, userID, applicationID string, requestedRoles, roleAudience []string) (*query.UserGrants, *projectsRoles, error) { - if (applicationID == "" || len(requestedRoles) == 0) && len(roleAudience) == 0 { - return nil, nil, nil - } - projectID, err := o.query.ProjectIDFromClientID(ctx, applicationID) - // applicationID might contain a username (e.g. client credentials) -> ignore the not found - if err != nil && !zerrors.IsNotFound(err) { - return nil, nil, err - } - // ensure the projectID of the requesting is part of the roleAudience - if projectID != "" { - roleAudience = append(roleAudience, projectID) - } - projectQuery, err := query.NewUserGrantProjectIDsSearchQuery(roleAudience) - if err != nil { - return nil, nil, err - } - userIDQuery, err := query.NewUserGrantUserIDSearchQuery(userID) - if err != nil { - return nil, nil, err - } - activeQuery, err := query.NewUserGrantStateQuery(domain.UserGrantStateActive) - if err != nil { - return nil, nil, err - } - grants, err := o.query.UserGrants(ctx, &query.UserGrantsQueries{ - Queries: []query.SearchQuery{ - projectQuery, - userIDQuery, - activeQuery, - }, - }, true) - if err != nil { - return nil, nil, err - } - roles := new(projectsRoles) - // if specific roles where requested, check if they are granted and append them in the roles list - if len(requestedRoles) > 0 { - for _, requestedRole := range requestedRoles { - for _, grant := range grants.UserGrants { - checkGrantedRoles(roles, *grant, requestedRole, grant.ProjectID == projectID) - } - } - return grants, roles, nil - } - // no specific roles were requested, so convert any grants into roles - for _, grant := range grants.UserGrants { - for _, role := range grant.Roles { - roles.Add(grant.ProjectID, role, grant.ResourceOwner, grant.OrgPrimaryDomain, grant.ProjectID == projectID) - } - } - return grants, roles, nil -} - -func (o *OPStorage) assertUserMetaData(ctx context.Context, userID string) (map[string]string, error) { - metaData, err := o.query.SearchUserMetadata(ctx, true, userID, &query.UserMetadataSearchQueries{}, false) - if err != nil { - return nil, err - } - - userMetaData := make(map[string]string) - for _, md := range metaData.Metadata { - userMetaData[md.Key] = base64.RawURLEncoding.EncodeToString(md.Value) - } - return userMetaData, nil -} - -func (o *OPStorage) assertUserResourceOwner(ctx context.Context, userID string) (map[string]string, error) { - user, err := o.query.GetUserByID(ctx, true, userID) - if err != nil { - return nil, err - } - resourceOwner, err := o.query.OrgByID(ctx, true, user.ResourceOwner) - if err != nil { - return nil, err - } - return map[string]string{ - ClaimResourceOwnerID: resourceOwner.ID, - ClaimResourceOwnerName: resourceOwner.Name, - ClaimResourceOwnerPrimaryDomain: resourceOwner.Domain, - }, nil +func (o *OPStorage) GetPrivateClaimsFromScopes(context.Context, string, string, []string) (map[string]interface{}, error) { + panic(o.panicErr("GetPrivateClaimsFromScopes")) } func checkGrantedRoles(roles *projectsRoles, grant query.UserGrant, requestedRole string, isRequested bool) { @@ -946,14 +166,6 @@ func getGender(gender domain.Gender) oidc.Gender { return "" } -func appendClaim(claims map[string]interface{}, claim string, value interface{}) map[string]interface{} { - if claims == nil { - claims = make(map[string]interface{}) - } - claims[claim] = value - return claims -} - func userinfoClaims(userInfo *oidc.UserInfo) func(c *actions.FieldConfig) interface{} { return func(c *actions.FieldConfig) interface{} { marshalled, err := json.Marshal(userInfo) diff --git a/internal/api/oidc/client_credentials.go b/internal/api/oidc/client_credentials.go index 9087360452..c86f5da9ae 100644 --- a/internal/api/oidc/client_credentials.go +++ b/internal/api/oidc/client_credentials.go @@ -14,27 +14,6 @@ import ( "github.com/zitadel/zitadel/internal/zerrors" ) -type clientCredentialsRequest struct { - sub string - audience []string - scopes []string -} - -// GetSubject returns the subject for token to be created because of the client credentials request -// the subject will be the id of the service user -func (c *clientCredentialsRequest) GetSubject() string { - return c.sub -} - -// GetAudience returns the audience for token to be created because of the client credentials request -func (c *clientCredentialsRequest) GetAudience() []string { - return c.audience -} - -func (c *clientCredentialsRequest) GetScopes() []string { - return c.scopes -} - func (s *Server) clientCredentialsAuth(ctx context.Context, clientID, clientSecret string) (op.Client, error) { user, err := s.query.GetUserByLoginName(ctx, false, clientID) if zerrors.IsNotFound(err) { diff --git a/internal/api/oidc/device_auth.go b/internal/api/oidc/device_auth.go index a10cba499d..8912ad1736 100644 --- a/internal/api/oidc/device_auth.go +++ b/internal/api/oidc/device_auth.go @@ -88,6 +88,6 @@ func (o *OPStorage) StoreDeviceAuthorization(ctx context.Context, clientID, devi return err } -func (o *OPStorage) GetDeviceAuthorizatonState(ctx context.Context, _, deviceCode string) (state *op.DeviceAuthorizationState, err error) { - return nil, nil +func (o *OPStorage) GetDeviceAuthorizatonState(context.Context, string, string) (*op.DeviceAuthorizationState, error) { + panic(o.panicErr("GetDeviceAuthorizatonState")) } diff --git a/internal/api/oidc/integration_test/auth_request_test.go b/internal/api/oidc/integration_test/auth_request_test.go index ad78184a04..77e389f7be 100644 --- a/internal/api/oidc/integration_test/auth_request_test.go +++ b/internal/api/oidc/integration_test/auth_request_test.go @@ -498,7 +498,7 @@ func TestOPStorage_TerminateSession(t *testing.T) { _, err = rp.Userinfo[*oidc.UserInfo](CTX, tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider) require.NoError(t, err) - postLogoutRedirect, err := rp.EndSession(CTX, provider, tokens.IDToken, logoutRedirectURI, "state") + postLogoutRedirect, err := rp.EndSession(CTX, provider, tokens.IDToken, logoutRedirectURI, "state", "", nil) require.NoError(t, err) assert.Equal(t, logoutRedirectURI+"?state=state", postLogoutRedirect.String()) @@ -535,7 +535,7 @@ func TestOPStorage_TerminateSession_refresh_grant(t *testing.T) { _, err = rp.Userinfo[*oidc.UserInfo](CTX, tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider) require.NoError(t, err) - postLogoutRedirect, err := rp.EndSession(CTX, provider, tokens.IDToken, logoutRedirectURI, "state") + postLogoutRedirect, err := rp.EndSession(CTX, provider, tokens.IDToken, logoutRedirectURI, "state", "", nil) require.NoError(t, err) assert.Equal(t, logoutRedirectURI+"?state=state", postLogoutRedirect.String()) @@ -551,6 +551,17 @@ func TestOPStorage_TerminateSession_refresh_grant(t *testing.T) { require.NoError(t, err) } +func buildLogoutURL(origin, logoutURLV2 string, redirectURI string, extraParams map[string]string) string { + u, _ := url.Parse(origin + logoutURLV2 + redirectURI) + q := u.Query() + for k, v := range extraParams { + q.Set(k, v) + } + u.RawQuery = q.Encode() + // Append the redirect URI as a URL-escaped string + return u.String() +} + func TestOPStorage_TerminateSession_empty_id_token_hint(t *testing.T) { tests := []struct { name string @@ -565,7 +576,7 @@ func TestOPStorage_TerminateSession_empty_id_token_hint(t *testing.T) { return clientID }(), authRequestID: createAuthRequest, - logoutURL: http_utils.BuildOrigin(Instance.Host(), Instance.Config.Secure) + Instance.Config.LogoutURLV2 + logoutRedirectURI + "?state=state", + logoutURL: buildLogoutURL(http_utils.BuildOrigin(Instance.Host(), Instance.Config.Secure), Instance.Config.LogoutURLV2, logoutRedirectURI+"?state=state", map[string]string{"logout_hint": "hint", "ui_locales": "it-IT en-US"}), }, { name: "login v2 config", @@ -574,7 +585,7 @@ func TestOPStorage_TerminateSession_empty_id_token_hint(t *testing.T) { return clientID }(), authRequestID: createAuthRequestNoLoginClientHeader, - logoutURL: http_utils.BuildOrigin(Instance.Host(), Instance.Config.Secure) + Instance.Config.LogoutURLV2 + logoutRedirectURI + "?state=state", + logoutURL: buildLogoutURL(http_utils.BuildOrigin(Instance.Host(), Instance.Config.Secure), Instance.Config.LogoutURLV2, logoutRedirectURI+"?state=state", map[string]string{"logout_hint": "hint", "ui_locales": "it-IT en-US"}), }, } for _, tt := range tests { @@ -601,7 +612,7 @@ func TestOPStorage_TerminateSession_empty_id_token_hint(t *testing.T) { assertTokens(t, tokens, false) assertIDTokenClaims(t, tokens.IDTokenClaims, User.GetUserId(), armPasskey, startTime, changeTime, sessionID) - postLogoutRedirect, err := rp.EndSession(CTX, provider, "", logoutRedirectURI, "state") + postLogoutRedirect, err := rp.EndSession(CTX, provider, "", logoutRedirectURI, "state", "hint", oidc.ParseLocales([]string{"it-IT", "en-US"})) require.NoError(t, err) assert.Equal(t, tt.logoutURL, postLogoutRedirect.String()) diff --git a/internal/api/oidc/integration_test/client_test.go b/internal/api/oidc/integration_test/client_test.go index 43b000108c..08c17f69cb 100644 --- a/internal/api/oidc/integration_test/client_test.go +++ b/internal/api/oidc/integration_test/client_test.go @@ -7,6 +7,7 @@ import ( "testing" "time" + "github.com/brianvoe/gofakeit/v6" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/zitadel/oidc/v3/pkg/client" @@ -24,8 +25,7 @@ import ( ) func TestServer_Introspect(t *testing.T) { - project, err := Instance.CreateProject(CTX) - require.NoError(t, err) + project := Instance.CreateProject(CTX, t, "", gofakeit.AppName(), false, false) app, err := Instance.CreateOIDCNativeClient(CTX, redirectURI, logoutRedirectURI, project.GetId(), false) require.NoError(t, err) @@ -188,10 +188,8 @@ func assertIntrospection( // with clients that have different authentication methods. func TestServer_VerifyClient(t *testing.T) { sessionID, sessionToken, startTime, changeTime := Instance.CreateVerifiedWebAuthNSession(t, CTXLOGIN, User.GetUserId()) - project, err := Instance.CreateProject(CTX) - require.NoError(t, err) - projectInactive, err := Instance.CreateProject(CTX) - require.NoError(t, err) + project := Instance.CreateProject(CTX, t, "", gofakeit.AppName(), false, false) + projectInactive := Instance.CreateProject(CTX, t, "", gofakeit.AppName(), false, false) inactiveClient, err := Instance.CreateOIDCInactivateClient(CTX, redirectURI, logoutRedirectURI, project.GetId()) require.NoError(t, err) diff --git a/internal/api/oidc/integration_test/keys_test.go b/internal/api/oidc/integration_test/keys_test.go index 8b66e980d0..a6223cf1ee 100644 --- a/internal/api/oidc/integration_test/keys_test.go +++ b/internal/api/oidc/integration_test/keys_test.go @@ -14,12 +14,10 @@ import ( "github.com/stretchr/testify/require" "github.com/zitadel/oidc/v3/pkg/client" "github.com/zitadel/oidc/v3/pkg/oidc" - "google.golang.org/protobuf/proto" http_util "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/integration" - "github.com/zitadel/zitadel/pkg/grpc/feature/v2" oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2" ) @@ -53,25 +51,16 @@ func TestServer_Keys(t *testing.T) { require.NoError(t, err) tests := []struct { - name string - webKeyFeature bool - wantLen int + name string + wantLen int }{ { - name: "legacy only", - webKeyFeature: false, - wantLen: 1, - }, - { - name: "webkeys with legacy", - webKeyFeature: true, - wantLen: 3, // 1 legacy + 2 created by enabling feature flag + name: "webkeys", + wantLen: 2, // 2 from instance creation. }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - ensureWebKeyFeature(t, instance, tt.webKeyFeature) - assert.EventuallyWithT(t, func(ttt *assert.CollectT) { resp, err := http.Get(discovery.JwksURI) require.NoError(ttt, err) @@ -92,30 +81,10 @@ func TestServer_Keys(t *testing.T) { } cacheControl := resp.Header.Get("cache-control") - if tt.webKeyFeature { - require.Equal(ttt, "max-age=300, must-revalidate", cacheControl) - return - } - require.Equal(ttt, "no-store", cacheControl) + require.Equal(ttt, "max-age=300, must-revalidate", cacheControl) }, time.Minute, time.Second/10) }) } } - -func ensureWebKeyFeature(t *testing.T, instance *integration.Instance, set bool) { - ctxIam := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) - - _, err := instance.Client.FeatureV2.SetInstanceFeatures(ctxIam, &feature.SetInstanceFeaturesRequest{ - WebKey: proto.Bool(set), - }) - require.NoError(t, err) - - t.Cleanup(func() { - _, err := instance.Client.FeatureV2.SetInstanceFeatures(ctxIam, &feature.SetInstanceFeaturesRequest{ - WebKey: proto.Bool(false), - }) - require.NoError(t, err) - }) -} diff --git a/internal/api/oidc/integration_test/oidc_test.go b/internal/api/oidc/integration_test/oidc_test.go index c68cb67736..d3d80b0557 100644 --- a/internal/api/oidc/integration_test/oidc_test.go +++ b/internal/api/oidc/integration_test/oidc_test.go @@ -311,7 +311,7 @@ func Test_ZITADEL_API_terminated_session(t *testing.T) { require.Equal(t, User.GetUserId(), myUserResp.GetUser().GetId()) // end session - postLogoutRedirect, err := rp.EndSession(CTX, provider, tokens.IDToken, logoutRedirectURI, "state") + postLogoutRedirect, err := rp.EndSession(CTX, provider, tokens.IDToken, logoutRedirectURI, "state", "", nil) require.NoError(t, err) assert.Equal(t, logoutRedirectURI+"?state=state", postLogoutRedirect.String()) @@ -421,21 +421,20 @@ type clientOpts struct { func createClientWithOpts(t testing.TB, instance *integration.Instance, opts clientOpts) (clientID, projectID string) { ctx := instance.WithAuthorization(CTX, integration.UserTypeOrgOwner) - project, err := instance.CreateProject(ctx) - require.NoError(t, err) + project := instance.CreateProject(ctx, t.(*testing.T), "", gofakeit.AppName(), false, false) app, err := instance.CreateOIDCClientLoginVersion(ctx, opts.redirectURI, opts.logoutURI, project.GetId(), app.OIDCAppType_OIDC_APP_TYPE_NATIVE, app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE, opts.devMode, opts.LoginVersion) require.NoError(t, err) return app.GetClientId(), project.GetId() } func createImplicitClient(t testing.TB) string { - app, err := Instance.CreateOIDCImplicitFlowClient(CTX, redirectURIImplicit, nil) + app, err := Instance.CreateOIDCImplicitFlowClient(CTX, t.(*testing.T), redirectURIImplicit, nil) require.NoError(t, err) return app.GetClientId() } func createImplicitClientNoLoginClientHeader(t testing.TB) string { - app, err := Instance.CreateOIDCImplicitFlowClient(CTX, redirectURIImplicit, &app.LoginVersion{Version: &app.LoginVersion_LoginV2{LoginV2: &app.LoginV2{BaseUri: nil}}}) + app, err := Instance.CreateOIDCImplicitFlowClient(CTX, t.(*testing.T), redirectURIImplicit, &app.LoginVersion{Version: &app.LoginVersion_LoginV2{LoginV2: &app.LoginV2{BaseUri: nil}}}) require.NoError(t, err) return app.GetClientId() } diff --git a/internal/api/oidc/integration_test/token_device_test.go b/internal/api/oidc/integration_test/token_device_test.go index 0c6a65e8a2..9692909205 100644 --- a/internal/api/oidc/integration_test/token_device_test.go +++ b/internal/api/oidc/integration_test/token_device_test.go @@ -8,6 +8,7 @@ import ( "testing" "time" + "github.com/brianvoe/gofakeit/v6" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/zitadel/oidc/v3/pkg/client/rp" @@ -21,8 +22,7 @@ import ( ) func TestServer_DeviceAuth(t *testing.T) { - project, err := Instance.CreateProject(CTX) - require.NoError(t, err) + project := Instance.CreateProject(CTX, t, "", gofakeit.AppName(), false, false) client, err := Instance.CreateOIDCClient(CTX, redirectURI, logoutRedirectURI, project.GetId(), app.OIDCAppType_OIDC_APP_TYPE_NATIVE, app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_NONE, false, app.OIDCGrantType_OIDC_GRANT_TYPE_DEVICE_CODE) require.NoError(t, err) diff --git a/internal/api/oidc/integration_test/token_exchange_test.go b/internal/api/oidc/integration_test/token_exchange_test.go index 0844898a2f..56a50a7c96 100644 --- a/internal/api/oidc/integration_test/token_exchange_test.go +++ b/internal/api/oidc/integration_test/token_exchange_test.go @@ -52,13 +52,6 @@ func setTokenExchangeFeature(t *testing.T, instance *integration.Instance, value time.Sleep(time.Second) } -func resetFeatures(t *testing.T, instance *integration.Instance) { - iamCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) - _, err := instance.Client.FeatureV2.ResetInstanceFeatures(iamCTX, &feature.ResetInstanceFeaturesRequest{}) - require.NoError(t, err) - time.Sleep(time.Second) -} - func setImpersonationPolicy(t *testing.T, instance *integration.Instance, value bool) { iamCTX := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) @@ -147,7 +140,7 @@ func TestServer_TokenExchange(t *testing.T) { ctx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) userResp := instance.CreateHumanUser(ctx) - client, keyData, err := instance.CreateOIDCTokenExchangeClient(ctx) + client, keyData, err := instance.CreateOIDCTokenExchangeClient(ctx, t) require.NoError(t, err) signer, err := rp.SignerFromKeyFile(keyData)() require.NoError(t, err) @@ -371,7 +364,7 @@ func TestServer_TokenExchangeImpersonation(t *testing.T) { setTokenExchangeFeature(t, instance, true) setImpersonationPolicy(t, instance, true) - client, keyData, err := instance.CreateOIDCTokenExchangeClient(ctx) + client, keyData, err := instance.CreateOIDCTokenExchangeClient(ctx, t) require.NoError(t, err) signer, err := rp.SignerFromKeyFile(keyData)() require.NoError(t, err) @@ -591,7 +584,7 @@ func TestImpersonation_API_Call(t *testing.T) { instance := integration.NewInstance(CTX) ctx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) - client, keyData, err := instance.CreateOIDCTokenExchangeClient(ctx) + client, keyData, err := instance.CreateOIDCTokenExchangeClient(ctx, t) require.NoError(t, err) signer, err := rp.SignerFromKeyFile(keyData)() require.NoError(t, err) diff --git a/internal/api/oidc/integration_test/userinfo_test.go b/internal/api/oidc/integration_test/userinfo_test.go index da1dc6b1e3..2a31dd964b 100644 --- a/internal/api/oidc/integration_test/userinfo_test.go +++ b/internal/api/oidc/integration_test/userinfo_test.go @@ -18,69 +18,11 @@ import ( oidc_api "github.com/zitadel/zitadel/internal/api/oidc" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/integration" - "github.com/zitadel/zitadel/pkg/grpc/feature/v2" "github.com/zitadel/zitadel/pkg/grpc/management" oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2" ) -// TestServer_UserInfo is a top-level test which re-executes the actual -// userinfo integration test against a matrix of different feature flags. -// This ensure that the response of the different implementations remains the same. func TestServer_UserInfo(t *testing.T) { - iamOwnerCTX := Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) - t.Cleanup(func() { - _, err := Instance.Client.FeatureV2.ResetInstanceFeatures(iamOwnerCTX, &feature.ResetInstanceFeaturesRequest{}) - require.NoError(t, err) - }) - tests := []struct { - name string - legacy bool - trigger bool - webKey bool - }{ - { - name: "legacy enabled", - legacy: true, - }, - { - name: "legacy disabled, trigger disabled", - legacy: false, - trigger: false, - }, - { - name: "legacy disabled, trigger enabled", - legacy: false, - trigger: true, - }, - - // This is the only functional test we need to cover web keys. - // - By creating tokens the signer is tested - // - When obtaining the tokens, the RP verifies the ID Token using the key set from the jwks endpoint. - // - By calling userinfo with the access token as JWT, the Token Verifier with the public key cache is tested. - { - name: "web keys", - legacy: false, - trigger: false, - webKey: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - _, err := Instance.Client.FeatureV2.SetInstanceFeatures(iamOwnerCTX, &feature.SetInstanceFeaturesRequest{ - OidcLegacyIntrospection: &tt.legacy, - OidcTriggerIntrospectionProjections: &tt.trigger, - WebKey: &tt.webKey, - }) - require.NoError(t, err) - testServer_UserInfo(t) - }) - } -} - -// testServer_UserInfo is the actual userinfo integration test, -// which calls the userinfo endpoint with different client configurations, roles and token scopes. -func testServer_UserInfo(t *testing.T) { const ( roleFoo = "foo" roleBar = "bar" @@ -309,9 +251,7 @@ func TestServer_UserInfo_Issue6662(t *testing.T) { roleBar = "bar" ) - project, err := Instance.CreateProject(CTX) - projectID := project.GetId() - require.NoError(t, err) + projectID := Instance.CreateProject(CTX, t, "", gofakeit.AppName(), false, false).GetId() user, _, clientID, clientSecret, err := Instance.CreateOIDCCredentialsClient(CTX) require.NoError(t, err) addProjectRolesGrants(t, user.GetUserId(), projectID, roleFoo, roleBar) diff --git a/internal/api/oidc/introspect.go b/internal/api/oidc/introspect.go index c028013d6a..6ce5d72e24 100644 --- a/internal/api/oidc/introspect.go +++ b/internal/api/oidc/introspect.go @@ -23,15 +23,6 @@ func (s *Server) Introspect(ctx context.Context, r *op.Request[op.IntrospectionR err = oidcError(err) span.EndWithError(err) }() - - features := authz.GetFeatures(ctx) - if features.LegacyIntrospection { - return s.LegacyServer.Introspect(ctx, r) - } - if features.TriggerIntrospectionProjections { - query.TriggerIntrospectionProjections(ctx) - } - ctx, cancel := context.WithCancel(ctx) defer cancel() @@ -109,6 +100,7 @@ func (s *Server) Introspect(ctx context.Context, r *op.Request[op.IntrospectionR token.userID, token.scope, client.projectID, + client.clientID, client.projectRoleAssertion, true, true, diff --git a/internal/api/oidc/jwt-profile.go b/internal/api/oidc/jwt-profile.go index fe668b5a8a..d230f58b5b 100644 --- a/internal/api/oidc/jwt-profile.go +++ b/internal/api/oidc/jwt-profile.go @@ -3,38 +3,9 @@ package oidc import ( "context" - "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/oidc/v3/pkg/op" - - "github.com/zitadel/zitadel/internal/domain" - "github.com/zitadel/zitadel/internal/telemetry/tracing" - "github.com/zitadel/zitadel/internal/zerrors" ) -func (o *OPStorage) JWTProfileTokenType(ctx context.Context, request op.TokenRequest) (_ op.AccessTokenType, err error) { - ctx, span := tracing.NewSpan(ctx) - defer func() { - err = oidcError(err) - span.EndWithError(err) - }() - - mapJWTProfileScopesToAudience(ctx, request) - user, err := o.query.GetUserByID(ctx, false, request.GetSubject()) - if err != nil { - return 0, err - } - // the user should always be a machine, but let's just be sure - if user.Machine == nil { - return 0, zerrors.ThrowInvalidArgument(nil, "OIDC-jk26S", "invalid client type") - } - return accessTokenTypeToOIDC(user.Machine.AccessTokenType), nil -} - -func mapJWTProfileScopesToAudience(ctx context.Context, request op.TokenRequest) { - // the request should always be a JWTTokenRequest, but let's make sure - jwt, ok := request.(*oidc.JWTTokenRequest) - if !ok { - return - } - jwt.Audience = domain.AddAudScopeToAudience(ctx, jwt.Audience, jwt.Scopes) +func (o *OPStorage) JWTProfileTokenType(context.Context, op.TokenRequest) (op.AccessTokenType, error) { + panic(o.panicErr("JWTProfileTokenType")) } diff --git a/internal/api/oidc/key.go b/internal/api/oidc/key.go index 852bbc7db8..61f874664f 100644 --- a/internal/api/oidc/key.go +++ b/internal/api/oidc/key.go @@ -10,18 +10,12 @@ import ( "github.com/go-jose/go-jose/v4" "github.com/jonboulle/clockwork" - "github.com/muhlemmer/gu" - "github.com/shopspring/decimal" - "github.com/zitadel/logging" "github.com/zitadel/oidc/v3/pkg/op" "github.com/zitadel/zitadel/internal/api/authz" http_util "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/crypto" - "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/query" - "github.com/zitadel/zitadel/internal/repository/instance" - "github.com/zitadel/zitadel/internal/repository/keypair" "github.com/zitadel/zitadel/internal/telemetry/tracing" "github.com/zitadel/zitadel/internal/zerrors" ) @@ -36,11 +30,8 @@ var supportedWebKeyAlgs = []string{ string(jose.ES512), } -func supportedSigningAlgs(ctx context.Context) []string { - if authz.GetFeatures(ctx).WebKey { - return supportedWebKeyAlgs - } - return []string{string(jose.RS256)} +func supportedSigningAlgs() []string { + return supportedWebKeyAlgs } type cachedPublicKey struct { @@ -211,15 +202,6 @@ func withKeyExpiryCheck(check bool) keySetOption { } } -func jsonWebkey(key query.PublicKey) *jose.JSONWebKey { - return &jose.JSONWebKey{ - KeyID: key.ID(), - Algorithm: key.Algorithm(), - Use: key.Use().String(), - Key: key.Key(), - } -} - // keySetMap is a mapping of key IDs to public key data. type keySetMap map[string][]byte @@ -250,7 +232,6 @@ func (k keySetMap) VerifySignature(ctx context.Context, jws *jose.JSONWebSignatu } const ( - locksTable = "projections.locks" signingKey = "signing_key" oidcUser = "OIDC" @@ -279,203 +260,36 @@ func (s *SigningKey) ID() string { return s.id } -// PublicKey wraps the query.PublicKey to implement the op.Key interface -type PublicKey struct { - key query.PublicKey -} - -func (s *PublicKey) Algorithm() jose.SignatureAlgorithm { - return jose.SignatureAlgorithm(s.key.Algorithm()) -} - -func (s *PublicKey) Use() string { - return s.key.Use().String() -} - -func (s *PublicKey) Key() interface{} { - return s.key.Key() -} - -func (s *PublicKey) ID() string { - return s.key.ID() -} - // KeySet implements the op.Storage interface func (o *OPStorage) KeySet(ctx context.Context) (keys []op.Key, err error) { - ctx, span := tracing.NewSpan(ctx) - defer func() { span.EndWithError(err) }() - err = retry(func() error { - publicKeys, err := o.query.ActivePublicKeys(ctx, time.Now()) - if err != nil { - return err - } - keys = make([]op.Key, len(publicKeys.Keys)) - for i, key := range publicKeys.Keys { - keys[i] = &PublicKey{key} - } - return nil - }) - return keys, err + panic(o.panicErr("KeySet")) } // SignatureAlgorithms implements the op.Storage interface func (o *OPStorage) SignatureAlgorithms(ctx context.Context) ([]jose.SignatureAlgorithm, error) { - key, err := o.SigningKey(ctx) - if err != nil { - logging.WithError(err).Warn("unable to fetch signing key") - return nil, err - } - return []jose.SignatureAlgorithm{key.SignatureAlgorithm()}, nil + panic(o.panicErr("SignatureAlgorithms")) } // SigningKey implements the op.Storage interface func (o *OPStorage) SigningKey(ctx context.Context) (key op.SigningKey, err error) { - err = retry(func() error { - key, err = o.getSigningKey(ctx) - if err != nil { - return err - } - if key == nil { - return zerrors.ThrowNotFound(nil, "OIDC-ve4Qu", "Errors.Internal") - } - return nil - }) - return key, err -} - -func (o *OPStorage) getSigningKey(ctx context.Context) (op.SigningKey, error) { - keys, err := o.query.ActivePrivateSigningKey(ctx, time.Now().Add(gracefulPeriod)) - if err != nil { - return nil, err - } - if len(keys.Keys) > 0 { - return PrivateKeyToSigningKey(SelectSigningKey(keys.Keys), o.encAlg) - } - var position decimal.Decimal - if keys.State != nil { - position = keys.State.Position - } - return nil, o.refreshSigningKey(ctx, position) -} - -func (o *OPStorage) refreshSigningKey(ctx context.Context, position decimal.Decimal) error { - ok, err := o.ensureIsLatestKey(ctx, position) - if err != nil || !ok { - return zerrors.ThrowInternal(err, "OIDC-ASfh3", "cannot ensure that projection is up to date") - } - err = o.lockAndGenerateSigningKeyPair(ctx) - if err != nil { - return zerrors.ThrowInternal(err, "OIDC-ADh31", "could not create signing key") - } - return zerrors.ThrowInternal(nil, "OIDC-Df1bh", "") -} - -func (o *OPStorage) ensureIsLatestKey(ctx context.Context, position decimal.Decimal) (bool, error) { - maxSequence, err := o.getMaxKeyPosition(ctx) - if err != nil { - return false, fmt.Errorf("error retrieving new events: %w", err) - } - return position.GreaterThanOrEqual(maxSequence), nil -} - -func PrivateKeyToSigningKey(key query.PrivateKey, algorithm crypto.EncryptionAlgorithm) (_ op.SigningKey, err error) { - keyData, err := crypto.Decrypt(key.Key(), algorithm) - if err != nil { - return nil, err - } - privateKey, err := crypto.BytesToPrivateKey(keyData) - if err != nil { - return nil, err - } - return &SigningKey{ - algorithm: jose.SignatureAlgorithm(key.Algorithm()), - key: privateKey, - id: key.ID(), - }, nil -} - -func (o *OPStorage) lockAndGenerateSigningKeyPair(ctx context.Context) error { - logging.Info("lock and generate signing key pair") - - ctx, cancel := context.WithCancel(ctx) - defer cancel() - - errs := o.locker.Lock(ctx, lockDuration, authz.GetInstance(ctx).InstanceID()) - err, ok := <-errs - if err != nil || !ok { - if zerrors.IsErrorAlreadyExists(err) { - return nil - } - logging.OnError(err).Debug("initial lock failed") - return err - } - - return o.command.GenerateSigningKeyPair(setOIDCCtx(ctx), "RS256") -} - -func (o *OPStorage) getMaxKeyPosition(ctx context.Context) (decimal.Decimal, error) { - return o.eventstore.LatestPosition(ctx, - eventstore.NewSearchQueryBuilder(eventstore.ColumnsMaxPosition). - ResourceOwner(authz.GetInstance(ctx).InstanceID()). - AwaitOpenTransactions(). - AddQuery(). - AggregateTypes( - keypair.AggregateType, - instance.AggregateType, - ). - EventTypes( - keypair.AddedEventType, - instance.InstanceRemovedEventType, - ). - Builder(), - ) -} - -func SelectSigningKey(keys []query.PrivateKey) query.PrivateKey { - return keys[len(keys)-1] -} - -func setOIDCCtx(ctx context.Context) context.Context { - return authz.SetCtxData(ctx, authz.CtxData{UserID: oidcUser, OrgID: authz.GetInstance(ctx).InstanceID()}) -} - -func retry(retryable func() error) (err error) { - for i := 0; i < retryCount; i++ { - err = retryable() - if err == nil { - return nil - } - time.Sleep(retryBackoff) - } - return err + panic(o.panicErr("SigningKey")) } func (s *Server) Keys(ctx context.Context, r *op.Request[struct{}]) (_ *op.Response, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - if !authz.GetFeatures(ctx).WebKey { - return s.LegacyServer.Keys(ctx, r) - } - keyset, err := s.query.GetWebKeySet(ctx) if err != nil { return nil, err } - // Return legacy keys, so we do not invalidate all tokens - // once the feature flag is enabled. - legacyKeys, err := s.query.ActivePublicKeys(ctx, time.Now()) - logging.OnError(err).Error("oidc server: active public keys (legacy)") - appendPublicKeysToWebKeySet(keyset, legacyKeys) - resp := op.NewResponse(keyset) if s.jwksCacheControlMaxAge != 0 { resp.Header.Set(http_util.CacheControl, fmt.Sprintf("max-age=%d, must-revalidate", int(s.jwksCacheControlMaxAge/time.Second)), ) } - return resp, nil } @@ -497,20 +311,10 @@ func appendPublicKeysToWebKeySet(keyset *jose.JSONWebKeySet, pubkeys *query.Publ func queryKeyFunc(q *query.Queries) func(ctx context.Context, keyID string) (*jose.JSONWebKey, *time.Time, error) { return func(ctx context.Context, keyID string) (*jose.JSONWebKey, *time.Time, error) { - if authz.GetFeatures(ctx).WebKey { - webKey, err := q.GetPublicWebKeyByID(ctx, keyID) - if err == nil { - return webKey, nil, nil - } - if !zerrors.IsNotFound(err) { - return nil, nil, err - } - } - - pubKey, err := q.GetPublicKeyByID(ctx, keyID) + webKey, err := q.GetPublicWebKeyByID(ctx, keyID) if err != nil { return nil, nil, err } - return jsonWebkey(pubKey), gu.Ptr(pubKey.Expiry()), nil + return webKey, nil, nil } } diff --git a/internal/api/oidc/op.go b/internal/api/oidc/op.go index d7171b957b..6f59ce3525 100644 --- a/internal/api/oidc/op.go +++ b/internal/api/oidc/op.go @@ -18,10 +18,8 @@ import ( "github.com/zitadel/zitadel/internal/cache" "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/crypto" - "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/domain/federatedlogout" "github.com/zitadel/zitadel/internal/eventstore" - "github.com/zitadel/zitadel/internal/eventstore/handler/crdb" "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/telemetry/metrics" "github.com/zitadel/zitadel/internal/zerrors" @@ -75,7 +73,6 @@ type OPStorage struct { defaultRefreshTokenIdleExpiration time.Duration defaultRefreshTokenExpiration time.Duration encAlg crypto.EncryptionAlgorithm - locker crdb.Locker assetAPIPrefix func(ctx context.Context) string contextToIssuer func(context.Context) string federateLogoutCache cache.Cache[federatedlogout.Index, string, *federatedlogout.FederatedLogout] @@ -91,14 +88,14 @@ type Provider struct { // IDTokenHintVerifier configures a Verifier and supported signing algorithms based on the Web Key feature in the context. func (o *Provider) IDTokenHintVerifier(ctx context.Context) *op.IDTokenHintVerifier { return op.NewIDTokenHintVerifier(op.IssuerFromContext(ctx), o.idTokenHintKeySet, op.WithSupportedIDTokenHintSigningAlgorithms( - supportedSigningAlgs(ctx)..., + supportedSigningAlgs()..., )) } // AccessTokenVerifier configures a Verifier and supported signing algorithms based on the Web Key feature in the context. func (o *Provider) AccessTokenVerifier(ctx context.Context) *op.AccessTokenVerifier { return op.NewAccessTokenVerifier(op.IssuerFromContext(ctx), o.accessTokenKeySet, op.WithSupportedAccessTokenSigningAlgorithms( - supportedSigningAlgs(ctx)..., + supportedSigningAlgs()..., )) } @@ -113,7 +110,6 @@ func NewServer( encryptionAlg crypto.EncryptionAlgorithm, cryptoKey []byte, es *eventstore.Eventstore, - projections *database.DB, userAgentCookie, instanceHandler func(http.Handler) http.Handler, accessHandler *middleware.AccessInterceptor, fallbackLogger *slog.Logger, @@ -124,7 +120,7 @@ func NewServer( if err != nil { return nil, zerrors.ThrowInternal(err, "OIDC-EGrqd", "cannot create op config: %w") } - storage := newStorage(config, command, query, repo, encryptionAlg, es, projections, ContextToIssuer, federatedLogoutCache) + storage := newStorage(config, command, query, repo, encryptionAlg, es, ContextToIssuer, federatedLogoutCache) keyCache := newPublicKeyCache(ctx, config.PublicKeyCacheMaxAge, queryKeyFunc(query)) accessTokenKeySet := newOidcKeySet(keyCache, withKeyExpiryCheck(true)) idTokenHintKeySet := newOidcKeySet(keyCache) @@ -236,7 +232,6 @@ func newStorage( repo repository.Repository, encAlg crypto.EncryptionAlgorithm, es *eventstore.Eventstore, - db *database.DB, contextToIssuer func(context.Context) string, federateLogoutCache cache.Cache[federatedlogout.Index, string, *federatedlogout.FederatedLogout], ) *OPStorage { @@ -253,7 +248,6 @@ func newStorage( defaultRefreshTokenIdleExpiration: config.DefaultRefreshTokenIdleExpiration, defaultRefreshTokenExpiration: config.DefaultRefreshTokenExpiration, encAlg: encAlg, - locker: crdb.NewLocker(db.DB, locksTable, signingKey), assetAPIPrefix: assets.AssetAPI(), contextToIssuer: contextToIssuer, federateLogoutCache: federateLogoutCache, diff --git a/internal/api/oidc/server.go b/internal/api/oidc/server.go index 1a0854e2a6..df7127443f 100644 --- a/internal/api/oidc/server.go +++ b/internal/api/oidc/server.go @@ -188,7 +188,7 @@ func (s *Server) createDiscoveryConfig(ctx context.Context, supportedUILocales o }, GrantTypesSupported: op.GrantTypes(s.Provider()), SubjectTypesSupported: op.SubjectTypes(s.Provider()), - IDTokenSigningAlgValuesSupported: supportedSigningAlgs(ctx), + IDTokenSigningAlgValuesSupported: supportedSigningAlgs(), RequestObjectSigningAlgValuesSupported: op.RequestObjectSigAlgorithms(s.Provider()), TokenEndpointAuthMethodsSupported: op.AuthMethodsTokenEndpoint(s.Provider()), TokenEndpointAuthSigningAlgValuesSupported: op.TokenSigAlgorithms(s.Provider()), diff --git a/internal/api/oidc/server_test.go b/internal/api/oidc/server_test.go index 76d073151a..9bf22fd210 100644 --- a/internal/api/oidc/server_test.go +++ b/internal/api/oidc/server_test.go @@ -8,9 +8,6 @@ import ( "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/oidc/v3/pkg/op" "golang.org/x/text/language" - - "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/feature" ) func TestServer_createDiscoveryConfig(t *testing.T) { @@ -63,92 +60,6 @@ func TestServer_createDiscoveryConfig(t *testing.T) { ctx: op.ContextWithIssuer(context.Background(), "https://issuer.com"), supportedUILocales: []language.Tag{language.English, language.German}, }, - &oidc.DiscoveryConfiguration{ - Issuer: "https://issuer.com", - AuthorizationEndpoint: "https://issuer.com/auth", - TokenEndpoint: "https://issuer.com/token", - IntrospectionEndpoint: "https://issuer.com/introspect", - UserinfoEndpoint: "https://issuer.com/userinfo", - RevocationEndpoint: "https://issuer.com/revoke", - EndSessionEndpoint: "https://issuer.com/logout", - DeviceAuthorizationEndpoint: "https://issuer.com/device", - CheckSessionIframe: "", - JwksURI: "https://issuer.com/keys", - RegistrationEndpoint: "", - ScopesSupported: []string{oidc.ScopeOpenID, oidc.ScopeProfile, oidc.ScopeEmail, oidc.ScopePhone, oidc.ScopeAddress, oidc.ScopeOfflineAccess}, - ResponseTypesSupported: []string{string(oidc.ResponseTypeCode), string(oidc.ResponseTypeIDTokenOnly), string(oidc.ResponseTypeIDToken)}, - ResponseModesSupported: []string{string(oidc.ResponseModeQuery), string(oidc.ResponseModeFragment), string(oidc.ResponseModeFormPost)}, - GrantTypesSupported: []oidc.GrantType{oidc.GrantTypeCode, oidc.GrantTypeImplicit, oidc.GrantTypeRefreshToken, oidc.GrantTypeBearer}, - ACRValuesSupported: nil, - SubjectTypesSupported: []string{"public"}, - IDTokenSigningAlgValuesSupported: []string{"RS256"}, - IDTokenEncryptionAlgValuesSupported: nil, - IDTokenEncryptionEncValuesSupported: nil, - UserinfoSigningAlgValuesSupported: nil, - UserinfoEncryptionAlgValuesSupported: nil, - UserinfoEncryptionEncValuesSupported: nil, - RequestObjectSigningAlgValuesSupported: []string{"RS256"}, - RequestObjectEncryptionAlgValuesSupported: nil, - RequestObjectEncryptionEncValuesSupported: nil, - TokenEndpointAuthMethodsSupported: []oidc.AuthMethod{oidc.AuthMethodNone, oidc.AuthMethodBasic, oidc.AuthMethodPost, oidc.AuthMethodPrivateKeyJWT}, - TokenEndpointAuthSigningAlgValuesSupported: []string{"RS256"}, - RevocationEndpointAuthMethodsSupported: []oidc.AuthMethod{oidc.AuthMethodNone, oidc.AuthMethodBasic, oidc.AuthMethodPost, oidc.AuthMethodPrivateKeyJWT}, - RevocationEndpointAuthSigningAlgValuesSupported: []string{"RS256"}, - IntrospectionEndpointAuthMethodsSupported: []oidc.AuthMethod{oidc.AuthMethodBasic, oidc.AuthMethodPrivateKeyJWT}, - IntrospectionEndpointAuthSigningAlgValuesSupported: []string{"RS256"}, - DisplayValuesSupported: nil, - ClaimTypesSupported: nil, - ClaimsSupported: []string{"sub", "aud", "exp", "iat", "iss", "auth_time", "nonce", "acr", "amr", "c_hash", "at_hash", "act", "scopes", "client_id", "azp", "preferred_username", "name", "family_name", "given_name", "locale", "email", "email_verified", "phone_number", "phone_number_verified"}, - ClaimsParameterSupported: false, - CodeChallengeMethodsSupported: []oidc.CodeChallengeMethod{"S256"}, - ServiceDocumentation: "", - ClaimsLocalesSupported: nil, - UILocalesSupported: []language.Tag{language.English, language.German}, - RequestParameterSupported: true, - RequestURIParameterSupported: false, - RequireRequestURIRegistration: false, - OPPolicyURI: "", - OPTermsOfServiceURI: "", - }, - }, - { - "web keys feature enabled", - fields{ - LegacyServer: op.NewLegacyServer( - func() *op.Provider { - //nolint:staticcheck - provider, _ := op.NewForwardedOpenIDProvider("path", - &op.Config{ - CodeMethodS256: true, - AuthMethodPost: true, - AuthMethodPrivateKeyJWT: true, - GrantTypeRefreshToken: true, - RequestObjectSupported: true, - }, - nil, - ) - return provider - }(), - op.Endpoints{ - Authorization: op.NewEndpoint("auth"), - Token: op.NewEndpoint("token"), - Introspection: op.NewEndpoint("introspect"), - Userinfo: op.NewEndpoint("userinfo"), - Revocation: op.NewEndpoint("revoke"), - EndSession: op.NewEndpoint("logout"), - JwksURI: op.NewEndpoint("keys"), - DeviceAuthorization: op.NewEndpoint("device"), - }, - ), - signingKeyAlgorithm: "RS256", - }, - args{ - ctx: authz.WithFeatures( - op.ContextWithIssuer(context.Background(), "https://issuer.com"), - feature.Features{WebKey: true}, - ), - supportedUILocales: []language.Tag{language.English, language.German}, - }, &oidc.DiscoveryConfiguration{ Issuer: "https://issuer.com", AuthorizationEndpoint: "https://issuer.com/auth", diff --git a/internal/api/oidc/token.go b/internal/api/oidc/token.go index 485f455784..d7a258259a 100644 --- a/internal/api/oidc/token.go +++ b/internal/api/oidc/token.go @@ -12,7 +12,6 @@ import ( "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/oidc/v3/pkg/op" - "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -32,7 +31,7 @@ for example the v2 code exchange and refresh token. */ func (s *Server) accessTokenResponseFromSession(ctx context.Context, client op.Client, session *command.OIDCSession, state, projectID string, projectRoleAssertion, accessTokenRoleAssertion, idTokenRoleAssertion, userInfoAssertion bool) (_ *oidc.AccessTokenResponse, err error) { - getUserInfo := s.getUserInfo(session.UserID, projectID, projectRoleAssertion, userInfoAssertion, session.Scope) + getUserInfo := s.getUserInfo(session.UserID, projectID, client.GetID(), projectRoleAssertion, userInfoAssertion, session.Scope) getSigner := s.getSignerOnce() resp := &oidc.AccessTokenResponse{ @@ -64,14 +63,13 @@ func (s *Server) accessTokenResponseFromSession(ctx context.Context, client op.C type SignerFunc func(ctx context.Context) (jose.Signer, jose.SignatureAlgorithm, error) func (s *Server) getSignerOnce() SignerFunc { - return GetSignerOnce(s.query.GetActiveSigningWebKey, s.Provider().Storage().SigningKey) + return GetSignerOnce(s.query.GetActiveSigningWebKey) } // GetSignerOnce returns a function which retrieves the instance's signer from the database once. // Repeated calls of the returned function return the same results. func GetSignerOnce( getActiveSigningWebKey func(ctx context.Context) (*jose.JSONWebKey, error), - getSigningKey func(ctx context.Context) (op.SigningKey, error), ) SignerFunc { var ( once sync.Once @@ -84,23 +82,12 @@ func GetSignerOnce( ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - if authz.GetFeatures(ctx).WebKey { - var webKey *jose.JSONWebKey - webKey, err = getActiveSigningWebKey(ctx) - if err != nil { - return - } - signer, signAlg, err = signerFromWebKey(webKey) - return - } - - var signingKey op.SigningKey - signingKey, err = getSigningKey(ctx) + var webKey *jose.JSONWebKey + webKey, err = getActiveSigningWebKey(ctx) if err != nil { return } - signAlg = signingKey.SignatureAlgorithm() - signer, err = op.SignerFromKey(signingKey) + signer, signAlg, err = signerFromWebKey(webKey) }) return signer, signAlg, err } @@ -126,8 +113,8 @@ type userInfoFunc func(ctx context.Context, roleAssertion bool, triggerType doma // getUserInfo returns a function which retrieves userinfo from the database once. // However, each time, role claims are asserted and also action flows will trigger. -func (s *Server) getUserInfo(userID, projectID string, projectRoleAssertion, userInfoAssertion bool, scope []string) userInfoFunc { - userInfo := s.userInfo(userID, scope, projectID, projectRoleAssertion, userInfoAssertion, false) +func (s *Server) getUserInfo(userID, projectID, clientID string, projectRoleAssertion, userInfoAssertion bool, scope []string) userInfoFunc { + userInfo := s.userInfo(userID, scope, projectID, clientID, projectRoleAssertion, userInfoAssertion, false) return func(ctx context.Context, roleAssertion bool, triggerType domain.TriggerType) (*oidc.UserInfo, error) { return userInfo(ctx, roleAssertion, triggerType) } diff --git a/internal/api/oidc/token_exchange.go b/internal/api/oidc/token_exchange.go index 030066ea1c..8cb087e760 100644 --- a/internal/api/oidc/token_exchange.go +++ b/internal/api/oidc/token_exchange.go @@ -218,7 +218,7 @@ func validateTokenExchangeAudience(requestedAudience, subjectAudience, actorAudi // Both tokens may point to the same object (subjectToken) in case of a regular Token Exchange. // When the subject and actor Tokens point to different objects, the new tokens will be for impersonation / delegation. func (s *Server) createExchangeTokens(ctx context.Context, tokenType oidc.TokenType, client *Client, subjectToken, actorToken *exchangeToken, audience, scopes []string) (_ *oidc.TokenExchangeResponse, err error) { - getUserInfo := s.getUserInfo(subjectToken.userID, client.client.ProjectID, client.client.ProjectRoleAssertion, client.IDTokenUserinfoClaimsAssertion(), scopes) + getUserInfo := s.getUserInfo(subjectToken.userID, client.client.ProjectID, client.GetID(), client.client.ProjectRoleAssertion, client.IDTokenUserinfoClaimsAssertion(), scopes) getSigner := s.getSignerOnce() resp := &oidc.TokenExchangeResponse{ diff --git a/internal/api/oidc/userinfo.go b/internal/api/oidc/userinfo.go index 61f03b6d0f..833c7a6ee4 100644 --- a/internal/api/oidc/userinfo.go +++ b/internal/api/oidc/userinfo.go @@ -33,15 +33,6 @@ func (s *Server) UserInfo(ctx context.Context, r *op.Request[oidc.UserInfoReques err = oidcError(err) span.EndWithError(err) }() - - features := authz.GetFeatures(ctx) - if features.LegacyIntrospection { - return s.LegacyServer.UserInfo(ctx, r) - } - if features.TriggerIntrospectionProjections { - query.TriggerOIDCUserInfoProjections(ctx) - } - token, err := s.verifyAccessToken(ctx, r.Data.AccessToken) if err != nil { return nil, op.NewStatusError(oidc.ErrAccessDenied().WithDescription("access token invalid").WithParent(err).WithReturnParentToClient(authz.GetFeatures(ctx).DebugOIDCParentError), http.StatusUnauthorized) @@ -63,6 +54,7 @@ func (s *Server) UserInfo(ctx context.Context, r *op.Request[oidc.UserInfoReques token.userID, token.scope, projectID, + token.clientID, assertion, true, false, @@ -95,6 +87,7 @@ func (s *Server) userInfo( userID string, scope []string, projectID string, + clientID string, projectRoleAssertion, userInfoAssertion, currentProjectOnly bool, ) func(ctx context.Context, roleAssertion bool, triggerType domain.TriggerType) (_ *oidc.UserInfo, err error) { var ( @@ -129,7 +122,7 @@ func (s *Server) userInfo( Claims: maps.Clone(rawUserInfo.Claims), } assertRoles(projectID, qu, roleAudience, requestedRoles, roleAssertion, userInfo) - return userInfo, s.userinfoFlows(ctx, qu, userInfo, triggerType) + return userInfo, s.userinfoFlows(ctx, qu, userInfo, triggerType, clientID) } } @@ -294,7 +287,8 @@ func setUserInfoRoleClaims(userInfo *oidc.UserInfo, roles *projectsRoles) { } } -func (s *Server) userinfoFlows(ctx context.Context, qu *query.OIDCUserInfo, userInfo *oidc.UserInfo, triggerType domain.TriggerType) (err error) { +//nolint:gocognit +func (s *Server) userinfoFlows(ctx context.Context, qu *query.OIDCUserInfo, userInfo *oidc.UserInfo, triggerType domain.TriggerType, clientID string) (err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -328,6 +322,13 @@ func (s *Server) userinfoFlows(ctx context.Context, qu *query.OIDCUserInfo, user } }), ), + actions.SetFields("application", + actions.SetFields("getClientId", func(c *actions.FieldConfig) interface{} { + return func(goja.FunctionCall) goja.Value { + return c.Runtime.ToValue(clientID) + } + }), + ), ), ) @@ -436,6 +437,7 @@ func (s *Server) userinfoFlows(ctx context.Context, qu *query.OIDCUserInfo, user User: qu.User, UserMetadata: qu.Metadata, Org: qu.Org, + Application: &ContextInfoApplication{ClientID: clientID}, UserGrants: qu.UserGrants, } @@ -472,13 +474,17 @@ func (s *Server) userinfoFlows(ctx context.Context, qu *query.OIDCUserInfo, user } type ContextInfo struct { - Function string `json:"function,omitempty"` - UserInfo *oidc.UserInfo `json:"userinfo,omitempty"` - User *query.User `json:"user,omitempty"` - UserMetadata []query.UserMetadata `json:"user_metadata,omitempty"` - Org *query.UserInfoOrg `json:"org,omitempty"` - UserGrants []query.UserGrant `json:"user_grants,omitempty"` - Response *ContextInfoResponse `json:"response,omitempty"` + Function string `json:"function,omitempty"` + UserInfo *oidc.UserInfo `json:"userinfo,omitempty"` + User *query.User `json:"user,omitempty"` + UserMetadata []query.UserMetadata `json:"user_metadata,omitempty"` + Org *query.UserInfoOrg `json:"org,omitempty"` + UserGrants []query.UserGrant `json:"user_grants,omitempty"` + Application *ContextInfoApplication `json:"application,omitempty"` + Response *ContextInfoResponse `json:"response,omitempty"` +} +type ContextInfoApplication struct { + ClientID string `json:"client_id,omitempty"` } type ContextInfoResponse struct { diff --git a/internal/api/saml/certificate.go b/internal/api/saml/certificate.go index 14752cd5cd..e0eb31255e 100644 --- a/internal/api/saml/certificate.go +++ b/internal/api/saml/certificate.go @@ -14,13 +14,14 @@ import ( "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/repository/instance" "github.com/zitadel/zitadel/internal/repository/keypair" "github.com/zitadel/zitadel/internal/zerrors" ) const ( - locksTable = "projections.locks" + locksTable = projection.LocksTable signingKey = "signing_key" samlUser = "SAML" diff --git a/internal/api/saml/storage.go b/internal/api/saml/storage.go index 935e986c72..5a7f6cb576 100644 --- a/internal/api/saml/storage.go +++ b/internal/api/saml/storage.go @@ -309,7 +309,7 @@ func (p *Storage) getCustomAttributes(ctx context.Context, user *query.User, use true, user.ID, &query.UserMetadataSearchQueries{Queries: []query.SearchQuery{resourceOwnerQuery}}, - false, + nil, ) if err != nil { logging.WithError(err).Info("unable to get md in action") @@ -490,7 +490,7 @@ func (p *Storage) getGrants(ctx context.Context, userID, applicationID string) ( userIDQuery, activeQuery, }, - }, true) + }, true, nil) } type customAttribute struct { diff --git a/internal/api/scim/integration_test/bulk_test.go b/internal/api/scim/integration_test/bulk_test.go index 660b10f4fd..c2f430c1b7 100644 --- a/internal/api/scim/integration_test/bulk_test.go +++ b/internal/api/scim/integration_test/bulk_test.go @@ -17,6 +17,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/text/language" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" "github.com/zitadel/zitadel/internal/api/scim/resources" "github.com/zitadel/zitadel/internal/api/scim/schemas" @@ -478,7 +480,7 @@ func TestBulk(t *testing.T) { }, { name: "fail on errors", - body: bulkFailOnErrorsJson, + body: withUsername(bulkFailOnErrorsJson, gofakeit.Username()), want: &scim.BulkResponse{ Schemas: []schemas.ScimSchemaType{schemas.IdBulkResponse}, Operations: []*scim.BulkResponseOperation{ @@ -579,7 +581,6 @@ func TestBulk(t *testing.T) { response, err := Instance.Client.SCIM.Bulk(ctx, orgID, tt.body) createdUserIDs := buildCreatedIDs(response) - defer deleteUsers(t, createdUserIDs) if tt.wantErr != nil { statusCode := tt.wantErr.status @@ -656,17 +657,6 @@ func buildCreatedIDs(response *scim.BulkResponse) []string { return createdIds } -func deleteUsers(t require.TestingT, ids []string) { - for _, id := range ids { - err := Instance.Client.SCIM.Users.Delete(CTX, Instance.DefaultOrg.Id, id) - - // only not found errors are ok (if the user is deleted in a later on bulk request) - if err != nil { - scim.RequireScimError(t, http.StatusNotFound, err) - } - } -} - func buildMinimalUpdateRequest(userID string) *scim.BulkRequest { return &scim.BulkRequest{ Schemas: []schemas.ScimSchemaType{schemas.IdBulkRequest}, @@ -690,7 +680,7 @@ func buildTooManyOperationsRequest() *scim.BulkRequest { req.Operations[i] = &scim.BulkRequestOperation{ Method: http.MethodPost, Path: "/Users", - Data: minimalUserJson, + Data: withUsername(minimalUserJson, gofakeit.Username()), } } @@ -720,8 +710,11 @@ func ensureMetadataProjected(t require.TestingT, userID, key, value string) { Id: userID, Key: key, }) - require.NoError(tt, err) - require.Equal(tt, value, string(md.Metadata.Value)) + if !assert.NoError(tt, err) { + require.Equal(tt, status.Code(err), codes.NotFound) + return + } + assert.Equal(tt, value, string(md.Metadata.Value)) }, retryDuration, tick) } diff --git a/internal/api/scim/integration_test/testdata/bulk_test_fail_on_errors.json b/internal/api/scim/integration_test/testdata/bulk_test_fail_on_errors.json index bc9ed1346a..d1d0c88fe6 100644 --- a/internal/api/scim/integration_test/testdata/bulk_test_fail_on_errors.json +++ b/internal/api/scim/integration_test/testdata/bulk_test_fail_on_errors.json @@ -32,14 +32,14 @@ "urn:ietf:params:scim:schemas:core:2.0:User" ], "externalId": "scim-bulk-created-user-0", - "userName": "scim-bulk-created-user-0", + "userName": "{{ .Username }}", "name": { "familyName": "scim-bulk-created-user-0-family-name", "givenName": "scim-bulk-created-user-0-given-name" }, "emails": [ { - "value": "scim-bulk-created-user-0@example.com", + "value": "{{ .Username }}@example.com", "primary": true } ], diff --git a/internal/api/scim/integration_test/testdata/users_create_test_full.json b/internal/api/scim/integration_test/testdata/users_create_test_full.json index 7879ecf160..cfc786a7e3 100644 --- a/internal/api/scim/integration_test/testdata/users_create_test_full.json +++ b/internal/api/scim/integration_test/testdata/users_create_test_full.json @@ -1,7 +1,7 @@ { "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"], "externalId": "701984", - "userName": "bjensen@example.com", + "userName": "{{ .Username }}@example.com", "name": { "formatted": "Ms. Barbara J Jensen, III", "familyName": "Jensen", @@ -15,12 +15,12 @@ "profileUrl": "http://login.example.com/bjensen", "emails": [ { - "value": "bjensen@example.com", + "value": "{{ .Username }}@example.com", "type": "work", "primary": true }, { - "value": "babs@jensen.org", + "value": "{{ .Username }}+1@example.com", "type": "home" } ], diff --git a/internal/api/scim/integration_test/testdata/users_create_test_minimal.json b/internal/api/scim/integration_test/testdata/users_create_test_minimal.json index c51f416bc7..bdceb3e45f 100644 --- a/internal/api/scim/integration_test/testdata/users_create_test_minimal.json +++ b/internal/api/scim/integration_test/testdata/users_create_test_minimal.json @@ -2,14 +2,14 @@ "schemas": [ "urn:ietf:params:scim:schemas:core:2.0:User" ], - "userName": "acmeUser1", + "userName": "{{ .Username }}", "name": { "familyName": "Ross", "givenName": "Bethany" }, "emails": [ { - "value": "user1@example.com", + "value": "{{ .Username }}@example.com", "primary": true } ] diff --git a/internal/api/scim/integration_test/testdata/users_create_test_minimal_inactive.json b/internal/api/scim/integration_test/testdata/users_create_test_minimal_inactive.json index 11650674a6..95ca1246d5 100644 --- a/internal/api/scim/integration_test/testdata/users_create_test_minimal_inactive.json +++ b/internal/api/scim/integration_test/testdata/users_create_test_minimal_inactive.json @@ -2,14 +2,14 @@ "schemas": [ "urn:ietf:params:scim:schemas:core:2.0:User" ], - "userName": "acmeUser1", + "userName": "acmeUser1-inactive", "name": { "familyName": "Ross", "givenName": "Bethany" }, "emails": [ { - "value": "user1@example.com", + "value": "user1-inactive@example.com", "primary": true } ], diff --git a/internal/api/scim/integration_test/testdata/users_create_test_no_primary_email_phone.json b/internal/api/scim/integration_test/testdata/users_create_test_no_primary_email_phone.json index 20c67c4715..f2b4bf2e4c 100644 --- a/internal/api/scim/integration_test/testdata/users_create_test_no_primary_email_phone.json +++ b/internal/api/scim/integration_test/testdata/users_create_test_no_primary_email_phone.json @@ -2,14 +2,14 @@ "schemas": [ "urn:ietf:params:scim:schemas:core:2.0:User" ], - "userName": "acmeUser1", + "userName": "acmeUser1-no-primary-email-phone", "name": { "familyName": "Ross", "givenName": "Bethany" }, "emails": [ { - "value": "user1@example.com" + "value": "user1-no-primary-email-phone@example.com" } ], "phoneNumbers": [ diff --git a/internal/api/scim/integration_test/testdata/users_replace_test_minimal_with_email_type.json b/internal/api/scim/integration_test/testdata/users_replace_test_minimal_with_email_type.json index b7e8d87590..33d78a2e3a 100644 --- a/internal/api/scim/integration_test/testdata/users_replace_test_minimal_with_email_type.json +++ b/internal/api/scim/integration_test/testdata/users_replace_test_minimal_with_email_type.json @@ -2,14 +2,14 @@ "schemas": [ "urn:ietf:params:scim:schemas:core:2.0:User" ], - "userName": "acmeUser1-minimal-replaced", + "userName": "{{ .Username }}", "name": { "familyName": "Ross-replaced", "givenName": "Bethany-replaced" }, "emails": [ { - "value": "user1-minimal-replaced@example.com", + "value": "{{ .Username }}@example.com", "primary": true, "type": "work" } diff --git a/internal/api/scim/integration_test/testdata/users_update_test_full.json b/internal/api/scim/integration_test/testdata/users_update_test_full.json index 23403f3e5b..f79a0b5332 100644 --- a/internal/api/scim/integration_test/testdata/users_update_test_full.json +++ b/internal/api/scim/integration_test/testdata/users_update_test_full.json @@ -7,7 +7,7 @@ "value": { "emails":[ { - "value":"babs@example.com", + "value":"{{ .Username }}+2@example.com", "type":"home", "primary": true } diff --git a/internal/api/scim/integration_test/users_create_test.go b/internal/api/scim/integration_test/users_create_test.go index 35d5297878..71356c7b43 100644 --- a/internal/api/scim/integration_test/users_create_test.go +++ b/internal/api/scim/integration_test/users_create_test.go @@ -3,17 +3,22 @@ package integration_test import ( + "bytes" "context" _ "embed" + "fmt" "net/http" "path" "testing" + "text/template" "time" + "github.com/brianvoe/gofakeit/v6" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/text/language" "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" "github.com/zitadel/zitadel/internal/api/scim/resources" "github.com/zitadel/zitadel/internal/api/scim/schemas" @@ -157,7 +162,18 @@ var ( } ) +func withUsername(fixture []byte, username string) []byte { + buf := new(bytes.Buffer) + template.Must(template.New("").Parse(string(fixture))).Execute(buf, &struct { + Username string + }{ + Username: username, + }) + return buf.Bytes() +} + func TestCreateUser(t *testing.T) { + minimalUsername := gofakeit.Username() tests := []struct { name string body []byte @@ -171,16 +187,16 @@ func TestCreateUser(t *testing.T) { }{ { name: "minimal user", - body: minimalUserJson, + body: withUsername(minimalUserJson, minimalUsername), want: &resources.ScimUser{ - UserName: "acmeUser1", + UserName: minimalUsername, Name: &resources.ScimUserName{ FamilyName: "Ross", GivenName: "Bethany", }, Emails: []*resources.ScimEmail{ { - Value: "user1@example.com", + Value: minimalUsername + "@example.com", Primary: true, }, }, @@ -195,7 +211,7 @@ func TestCreateUser(t *testing.T) { }, { name: "full user", - body: fullUserJson, + body: withUsername(fullUserJson, "bjensen"), want: fullUser, }, { @@ -204,7 +220,7 @@ func TestCreateUser(t *testing.T) { want: &resources.ScimUser{ Emails: []*resources.ScimEmail{ { - Value: "user1@example.com", + Value: "user1-no-primary-email-phone@example.com", Primary: true, }, }, @@ -262,21 +278,21 @@ func TestCreateUser(t *testing.T) { }, { name: "not authenticated", - body: minimalUserJson, + body: withUsername(minimalUserJson, gofakeit.Username()), ctx: context.Background(), wantErr: true, errorStatus: http.StatusUnauthorized, }, { name: "no permissions", - body: minimalUserJson, + body: withUsername(minimalUserJson, gofakeit.Username()), ctx: Instance.WithAuthorization(CTX, integration.UserTypeNoPermission), wantErr: true, errorStatus: http.StatusNotFound, }, { name: "another org", - body: minimalUserJson, + body: withUsername(minimalUserJson, gofakeit.Username()), orgID: SecondaryOrganization.OrganizationId, wantErr: true, errorStatus: http.StatusNotFound, @@ -315,11 +331,6 @@ func TestCreateUser(t *testing.T) { } assert.NotEmpty(t, createdUser.ID) - defer func() { - _, err = Instance.Client.UserV2.DeleteUser(CTX, &user.DeleteUserRequest{UserId: createdUser.ID}) - assert.NoError(t, err) - }() - assert.EqualValues(t, []schemas.ScimSchemaType{"urn:ietf:params:scim:schemas:core:2.0:User"}, createdUser.Resource.Schemas) assert.Equal(t, schemas.ScimResourceTypeSingular("User"), createdUser.Resource.Meta.ResourceType) assert.Equal(t, "http://"+Instance.Host()+path.Join(schemas.HandlerPrefix, orgID, "Users", createdUser.ID), createdUser.Resource.Meta.Location) @@ -345,10 +356,11 @@ func TestCreateUser(t *testing.T) { } func TestCreateUser_duplicate(t *testing.T) { - createdUser, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, minimalUserJson) + parsedMinimalUserJson := withUsername(minimalUserJson, gofakeit.Username()) + createdUser, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, parsedMinimalUserJson) require.NoError(t, err) - _, err = Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, minimalUserJson) + _, err = Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, parsedMinimalUserJson) scimErr := scim.RequireScimError(t, http.StatusConflict, err) assert.Equal(t, "User already exists", scimErr.Error.Detail) assert.Equal(t, "uniqueness", scimErr.Error.ScimType) @@ -358,14 +370,10 @@ func TestCreateUser_duplicate(t *testing.T) { } func TestCreateUser_metadata(t *testing.T) { - createdUser, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, fullUserJson) + username := gofakeit.Username() + createdUser, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, withUsername(fullUserJson, username)) require.NoError(t, err) - defer func() { - _, err = Instance.Client.UserV2.DeleteUser(CTX, &user.DeleteUserRequest{UserId: createdUser.ID}) - require.NoError(t, err) - }() - retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute) require.EventuallyWithT(t, func(tt *assert.CollectT) { md, err := Instance.Client.Mgmt.ListUserMetadata(CTX, &management.ListUserMetadataRequest{ @@ -391,38 +399,111 @@ func TestCreateUser_metadata(t *testing.T) { test.AssertMapContains(tt, mdMap, "urn:zitadel:scim:locale", "en-US") test.AssertMapContains(tt, mdMap, "urn:zitadel:scim:ims", `[{"value":"someaimhandle","type":"aim"},{"value":"twitterhandle","type":"X"}]`) test.AssertMapContains(tt, mdMap, "urn:zitadel:scim:roles", `[{"value":"my-role-1","display":"Rolle 1","type":"main-role","primary":true},{"value":"my-role-2","display":"Rolle 2","type":"secondary-role"}]`) - test.AssertMapContains(tt, mdMap, "urn:zitadel:scim:emails", `[{"value":"bjensen@example.com","primary":true,"type":"work"},{"value":"babs@jensen.org","primary":false,"type":"home"}]`) + test.AssertMapContains(tt, mdMap, "urn:zitadel:scim:emails", fmt.Sprintf(`[{"value":"%s@example.com","primary":true,"type":"work"},{"value":"%s+1@example.com","primary":false,"type":"home"}]`, username, username)) }, retryDuration, tick) } func TestCreateUser_scopedExternalID(t *testing.T) { - setProvisioningDomain(t, Instance.Users.Get(integration.UserTypeOrgOwner).ID, "fooBar") - - createdUser, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, fullUserJson) + callingUserId, callingUserPat, err := Instance.CreateMachineUserPATWithMembership(CTX, "ORG_OWNER") require.NoError(t, err) - - defer func() { - _, err = Instance.Client.UserV2.DeleteUser(CTX, &user.DeleteUserRequest{UserId: createdUser.ID}) - require.NoError(t, err) - - removeProvisioningDomain(t, Instance.Users.Get(integration.UserTypeOrgOwner).ID) - }() - - retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute) + ctx := integration.WithAuthorizationToken(CTX, callingUserPat) + setProvisioningDomain(t, callingUserId, "fooBar") + createdUser, err := Instance.Client.SCIM.Users.Create(ctx, Instance.DefaultOrg.Id, withUsername(fullUserJson, gofakeit.Username())) + require.NoError(t, err) + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, time.Minute) require.EventuallyWithT(t, func(tt *assert.CollectT) { // unscoped externalID should not exist - _, err = Instance.Client.Mgmt.GetUserMetadata(CTX, &management.GetUserMetadataRequest{ + unscoped, err := Instance.Client.Mgmt.GetUserMetadata(ctx, &management.GetUserMetadataRequest{ Id: createdUser.ID, Key: "urn:zitadel:scim:externalId", }) integration.AssertGrpcStatus(tt, codes.NotFound, err) + unscoped = unscoped // scoped externalID should exist - md, err := Instance.Client.Mgmt.GetUserMetadata(CTX, &management.GetUserMetadataRequest{ + md, err := Instance.Client.Mgmt.GetUserMetadata(ctx, &management.GetUserMetadataRequest{ Id: createdUser.ID, Key: "urn:zitadel:scim:fooBar:externalId", }) - require.NoError(tt, err) + if !assert.NoError(tt, err) { + require.Equal(tt, status.Code(err), codes.NotFound) + return + } assert.Equal(tt, "701984", string(md.Metadata.Value)) }, retryDuration, tick) } + +func TestCreateUser_ignorePasswordOnCreate(t *testing.T) { + t.Parallel() + tests := []struct { + name string + ignorePassword string + scimErrorType string + scimErrorDetail string + wantUser *resources.ScimUser + wantErr bool + }{ + { + name: "ignorePasswordOnCreate set to false", + ignorePassword: "false", + wantErr: true, + scimErrorType: "invalidValue", + scimErrorDetail: "Password is too short", + }, + { + name: "ignorePasswordOnCreate set to an invalid value", + ignorePassword: "random", + wantErr: true, + scimErrorType: "invalidValue", + scimErrorDetail: "Invalid value for metadata key urn:zitadel:scim:ignorePasswordOnCreate: random", + }, + { + name: "ignorePasswordOnCreate set to true", + ignorePassword: "true", + wantUser: &resources.ScimUser{ + UserName: "acmeUser1", + Name: &resources.ScimUserName{ + FamilyName: "Ross", + GivenName: "Bethany", + }, + Emails: []*resources.ScimEmail{ + { + Value: "user1@example.com", + Primary: true, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + // create a machine user + callingUserId, callingUserPat, err := Instance.CreateMachineUserPATWithMembership(CTX, "ORG_OWNER") + require.NoError(t, err) + ctx := integration.WithAuthorizationToken(CTX, callingUserPat) + + // set urn:zitadel:scim:ignorePasswordOnCreate metadata for the machine user + setAndEnsureMetadata(t, callingUserId, "urn:zitadel:scim:ignorePasswordOnCreate", tt.ignorePassword) + + // create a user with an invalid password + createdUser, err := Instance.Client.SCIM.Users.Create(ctx, Instance.DefaultOrg.Id, withUsername(invalidPasswordUserJson, "acmeUser1")) + require.Equal(t, tt.wantErr, err != nil) + if err != nil { + scimErr := scim.RequireScimError(t, http.StatusBadRequest, err) + assert.Equal(t, tt.scimErrorType, scimErr.Error.ScimType) + assert.Equal(t, tt.scimErrorDetail, scimErr.Error.Detail) + return + } + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + // ensure the user is really stored and not just returned to the caller + fetchedUser, err := Instance.Client.SCIM.Users.Get(CTX, Instance.DefaultOrg.Id, createdUser.ID) + require.NoError(ttt, err) + assert.True(ttt, test.PartiallyDeepEqual(tt.wantUser, fetchedUser)) + }, retryDuration, tick) + }) + } +} diff --git a/internal/api/scim/integration_test/users_delete_test.go b/internal/api/scim/integration_test/users_delete_test.go index 86e88f46c4..23c335f93c 100644 --- a/internal/api/scim/integration_test/users_delete_test.go +++ b/internal/api/scim/integration_test/users_delete_test.go @@ -8,6 +8,7 @@ import ( "testing" "time" + "github.com/brianvoe/gofakeit/v6" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/grpc/codes" @@ -76,13 +77,12 @@ func TestDeleteUser_errors(t *testing.T) { func TestDeleteUser_ensureReallyDeleted(t *testing.T) { // create user and dependencies createUserResp := Instance.CreateHumanUser(CTX) - proj, err := Instance.CreateProject(CTX) - require.NoError(t, err) + proj := Instance.CreateProject(CTX, t, "", gofakeit.AppName(), false, false) Instance.CreateProjectUserGrant(t, CTX, proj.Id, createUserResp.UserId) // delete user via scim - err = Instance.Client.SCIM.Users.Delete(CTX, Instance.DefaultOrg.Id, createUserResp.UserId) + err := Instance.Client.SCIM.Users.Delete(CTX, Instance.DefaultOrg.Id, createUserResp.UserId) assert.NoError(t, err) // ensure it is really deleted => try to delete again => should 404 diff --git a/internal/api/scim/integration_test/users_get_test.go b/internal/api/scim/integration_test/users_get_test.go index 8a1bab6c93..08e09946fb 100644 --- a/internal/api/scim/integration_test/users_get_test.go +++ b/internal/api/scim/integration_test/users_get_test.go @@ -9,6 +9,7 @@ import ( "testing" "time" + "github.com/brianvoe/gofakeit/v6" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/text/language" @@ -18,256 +19,260 @@ import ( "github.com/zitadel/zitadel/internal/integration" "github.com/zitadel/zitadel/internal/integration/scim" "github.com/zitadel/zitadel/internal/test" - "github.com/zitadel/zitadel/pkg/grpc/user/v2" ) func TestGetUser(t *testing.T) { - tests := []struct { - name string - orgID string - buildUserID func() string - cleanup func(userID string) + type testCase struct { ctx context.Context + orgID string + userID string want *resources.ScimUser wantErr bool errorStatus int + } + tests := []struct { + name string + setup func(t *testing.T) testCase }{ { - name: "not authenticated", - ctx: context.Background(), - errorStatus: http.StatusUnauthorized, - wantErr: true, + name: "not authenticated", + setup: func(t *testing.T) testCase { + return testCase{ + ctx: context.Background(), + errorStatus: http.StatusUnauthorized, + wantErr: true, + } + }, }, { - name: "no permissions", - ctx: Instance.WithAuthorization(CTX, integration.UserTypeNoPermission), - errorStatus: http.StatusNotFound, - wantErr: true, + name: "no permissions", + setup: func(t *testing.T) testCase { + return testCase{ + ctx: Instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + errorStatus: http.StatusNotFound, + wantErr: true, + } + }, }, { - name: "another org", - orgID: SecondaryOrganization.OrganizationId, - errorStatus: http.StatusNotFound, - wantErr: true, + name: "another org", + setup: func(t *testing.T) testCase { + return testCase{ + orgID: SecondaryOrganization.OrganizationId, + errorStatus: http.StatusNotFound, + wantErr: true, + } + }, }, { - name: "another org with permissions", - orgID: SecondaryOrganization.OrganizationId, - ctx: Instance.WithAuthorization(CTX, integration.UserTypeNoPermission), - errorStatus: http.StatusNotFound, - wantErr: true, + name: "another org with permissions", + setup: func(t *testing.T) testCase { + return testCase{ + ctx: Instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + orgID: SecondaryOrganization.OrganizationId, + errorStatus: http.StatusNotFound, + wantErr: true, + } + }, }, { name: "unknown user id", - buildUserID: func() string { - return "unknown" + setup: func(t *testing.T) testCase { + return testCase{ + userID: "unknown", + errorStatus: http.StatusNotFound, + wantErr: true, + } }, - errorStatus: http.StatusNotFound, - wantErr: true, }, { name: "created via grpc", - want: &resources.ScimUser{ - Name: &resources.ScimUserName{ - FamilyName: "Mouse", - GivenName: "Mickey", - }, - PreferredLanguage: language.MustParse("nl"), - PhoneNumbers: []*resources.ScimPhoneNumber{ - { - Value: "+41791234567", - Primary: true, + setup: func(t *testing.T) testCase { + return testCase{ + want: &resources.ScimUser{ + Name: &resources.ScimUserName{ + FamilyName: "Mouse", + GivenName: "Mickey", + }, + PreferredLanguage: language.MustParse("nl"), + PhoneNumbers: []*resources.ScimPhoneNumber{ + { + Value: "+41791234567", + Primary: true, + }, + }, }, - }, + } }, }, { name: "created via scim", - buildUserID: func() string { - createdUser, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, fullUserJson) + setup: func(t *testing.T) testCase { + username := gofakeit.Username() + createdUser, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, withUsername(fullUserJson, username)) require.NoError(t, err) - return createdUser.ID - }, - cleanup: func(userID string) { - _, err := Instance.Client.UserV2.DeleteUser(CTX, &user.DeleteUserRequest{UserId: userID}) - require.NoError(t, err) - }, - want: &resources.ScimUser{ - ExternalID: "701984", - UserName: "bjensen@example.com", - Name: &resources.ScimUserName{ - Formatted: "Babs Jensen", // DisplayName takes precedence - FamilyName: "Jensen", - GivenName: "Barbara", - MiddleName: "Jane", - HonorificPrefix: "Ms.", - HonorificSuffix: "III", - }, - DisplayName: "Babs Jensen", - NickName: "Babs", - ProfileUrl: test.Must(schemas.ParseHTTPURL("http://login.example.com/bjensen")), - Title: "Tour Guide", - PreferredLanguage: language.Make("en-US"), - Locale: "en-US", - Timezone: "America/Los_Angeles", - Active: schemas.NewRelaxedBool(true), - Emails: []*resources.ScimEmail{ - { - Value: "bjensen@example.com", - Primary: true, - Type: "work", + return testCase{ + userID: createdUser.ID, + want: &resources.ScimUser{ + ExternalID: "701984", + UserName: username + "@example.com", + Name: &resources.ScimUserName{ + Formatted: "Babs Jensen", // DisplayName takes precedence + FamilyName: "Jensen", + GivenName: "Barbara", + MiddleName: "Jane", + HonorificPrefix: "Ms.", + HonorificSuffix: "III", + }, + DisplayName: "Babs Jensen", + NickName: "Babs", + ProfileUrl: test.Must(schemas.ParseHTTPURL("http://login.example.com/bjensen")), + Title: "Tour Guide", + PreferredLanguage: language.Make("en-US"), + Locale: "en-US", + Timezone: "America/Los_Angeles", + Active: schemas.NewRelaxedBool(true), + Emails: []*resources.ScimEmail{ + { + Value: username + "@example.com", + Primary: true, + Type: "work", + }, + }, + PhoneNumbers: []*resources.ScimPhoneNumber{ + { + Value: "+415555555555", + Primary: true, + }, + }, + Ims: []*resources.ScimIms{ + { + Value: "someaimhandle", + Type: "aim", + }, + { + Value: "twitterhandle", + Type: "X", + }, + }, + Addresses: []*resources.ScimAddress{ + { + Type: "work", + StreetAddress: "100 Universal City Plaza", + Locality: "Hollywood", + Region: "CA", + PostalCode: "91608", + Country: "USA", + Formatted: "100 Universal City Plaza\nHollywood, CA 91608 USA", + Primary: true, + }, + { + Type: "home", + StreetAddress: "456 Hollywood Blvd", + Locality: "Hollywood", + Region: "CA", + PostalCode: "91608", + Country: "USA", + Formatted: "456 Hollywood Blvd\nHollywood, CA 91608 USA", + }, + }, + Photos: []*resources.ScimPhoto{ + { + Value: *test.Must(schemas.ParseHTTPURL("https://photos.example.com/profilephoto/72930000000Ccne/F")), + Type: "photo", + }, + { + Value: *test.Must(schemas.ParseHTTPURL("https://photos.example.com/profilephoto/72930000000Ccne/T")), + Type: "thumbnail", + }, + }, + Roles: []*resources.ScimRole{ + { + Value: "my-role-1", + Display: "Rolle 1", + Type: "main-role", + Primary: true, + }, + { + Value: "my-role-2", + Display: "Rolle 2", + Type: "secondary-role", + Primary: false, + }, + }, + Entitlements: []*resources.ScimEntitlement{ + { + Value: "my-entitlement-1", + Display: "Entitlement 1", + Type: "main-entitlement", + Primary: true, + }, + { + Value: "my-entitlement-2", + Display: "Entitlement 2", + Type: "secondary-entitlement", + Primary: false, + }, + }, }, - }, - PhoneNumbers: []*resources.ScimPhoneNumber{ - { - Value: "+415555555555", - Primary: true, - }, - }, - Ims: []*resources.ScimIms{ - { - Value: "someaimhandle", - Type: "aim", - }, - { - Value: "twitterhandle", - Type: "X", - }, - }, - Addresses: []*resources.ScimAddress{ - { - Type: "work", - StreetAddress: "100 Universal City Plaza", - Locality: "Hollywood", - Region: "CA", - PostalCode: "91608", - Country: "USA", - Formatted: "100 Universal City Plaza\nHollywood, CA 91608 USA", - Primary: true, - }, - { - Type: "home", - StreetAddress: "456 Hollywood Blvd", - Locality: "Hollywood", - Region: "CA", - PostalCode: "91608", - Country: "USA", - Formatted: "456 Hollywood Blvd\nHollywood, CA 91608 USA", - }, - }, - Photos: []*resources.ScimPhoto{ - { - Value: *test.Must(schemas.ParseHTTPURL("https://photos.example.com/profilephoto/72930000000Ccne/F")), - Type: "photo", - }, - { - Value: *test.Must(schemas.ParseHTTPURL("https://photos.example.com/profilephoto/72930000000Ccne/T")), - Type: "thumbnail", - }, - }, - Roles: []*resources.ScimRole{ - { - Value: "my-role-1", - Display: "Rolle 1", - Type: "main-role", - Primary: true, - }, - { - Value: "my-role-2", - Display: "Rolle 2", - Type: "secondary-role", - Primary: false, - }, - }, - Entitlements: []*resources.ScimEntitlement{ - { - Value: "my-entitlement-1", - Display: "Entitlement 1", - Type: "main-entitlement", - Primary: true, - }, - { - Value: "my-entitlement-2", - Display: "Entitlement 2", - Type: "secondary-entitlement", - Primary: false, - }, - }, + } }, }, { name: "scoped externalID", - buildUserID: func() string { - // create user without provisioning domain - createdUser, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, fullUserJson) + setup: func(t *testing.T) testCase { + createdUser, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, withUsername(fullUserJson, gofakeit.Username())) require.NoError(t, err) - - // set provisioning domain of service user - setProvisioningDomain(t, Instance.Users.Get(integration.UserTypeOrgOwner).ID, "fooBar") - - // set externalID for provisioning domain + callingUserId, callingUserPat, err := Instance.CreateMachineUserPATWithMembership(CTX, "ORG_OWNER") + require.NoError(t, err) + setProvisioningDomain(t, callingUserId, "fooBar") setAndEnsureMetadata(t, createdUser.ID, "urn:zitadel:scim:fooBar:externalId", "100-scopedExternalId") - return createdUser.ID - }, - cleanup: func(userID string) { - _, err := Instance.Client.UserV2.DeleteUser(CTX, &user.DeleteUserRequest{UserId: userID}) - require.NoError(t, err) - - removeProvisioningDomain(t, Instance.Users.Get(integration.UserTypeOrgOwner).ID) - }, - want: &resources.ScimUser{ - ExternalID: "100-scopedExternalId", + return testCase{ + ctx: integration.WithAuthorizationToken(CTX, callingUserPat), + userID: createdUser.ID, + want: &resources.ScimUser{ + ExternalID: "100-scopedExternalId", + }, + } }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - ctx := tt.ctx - if ctx == nil { - ctx = CTX + ttt := tt.setup(t) + if ttt.userID == "" { + ttt.userID = Instance.CreateHumanUser(CTX).UserId } - - var userID string - if tt.buildUserID != nil { - userID = tt.buildUserID() - } else { - createUserResp := Instance.CreateHumanUser(CTX) - userID = createUserResp.UserId + if ttt.ctx == nil { + ttt.ctx = CTX } - - orgID := tt.orgID - if orgID == "" { - orgID = Instance.DefaultOrg.Id + if ttt.orgID == "" { + ttt.orgID = Instance.DefaultOrg.Id } - retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute) - var fetchedUser *resources.ScimUser - var err error - require.EventuallyWithT(t, func(ttt *assert.CollectT) { - fetchedUser, err = Instance.Client.SCIM.Users.Get(ctx, orgID, userID) - if tt.wantErr { - statusCode := tt.errorStatus + require.EventuallyWithT(t, func(collect *assert.CollectT) { + fetchedUser, err := Instance.Client.SCIM.Users.Get(ttt.ctx, ttt.orgID, ttt.userID) + if ttt.wantErr { + statusCode := ttt.errorStatus if statusCode == 0 { statusCode = http.StatusBadRequest } - - scim.RequireScimError(ttt, statusCode, err) + scim.RequireScimError(collect, statusCode, err) return } - - assert.Equal(ttt, userID, fetchedUser.ID) - assert.EqualValues(ttt, []schemas.ScimSchemaType{"urn:ietf:params:scim:schemas:core:2.0:User"}, fetchedUser.Schemas) - assert.Equal(ttt, schemas.ScimResourceTypeSingular("User"), fetchedUser.Resource.Meta.ResourceType) - assert.Equal(ttt, "http://"+Instance.Host()+path.Join(schemas.HandlerPrefix, orgID, "Users", fetchedUser.ID), fetchedUser.Resource.Meta.Location) - assert.Nil(ttt, fetchedUser.Password) - if !test.PartiallyDeepEqual(tt.want, fetchedUser) { - ttt.Errorf("GetUser() got = %#v, want %#v", fetchedUser, tt.want) + if !assert.NoError(collect, err) { + scim.RequireScimError(collect, http.StatusNotFound, err) + return + } + assert.Equal(collect, ttt.userID, fetchedUser.ID) + assert.EqualValues(collect, []schemas.ScimSchemaType{"urn:ietf:params:scim:schemas:core:2.0:User"}, fetchedUser.Schemas) + assert.Equal(collect, schemas.ScimResourceTypeSingular("User"), fetchedUser.Resource.Meta.ResourceType) + assert.Equal(collect, "http://"+Instance.Host()+path.Join(schemas.HandlerPrefix, ttt.orgID, "Users", fetchedUser.ID), fetchedUser.Resource.Meta.Location) + assert.Nil(collect, fetchedUser.Password) + if !test.PartiallyDeepEqual(ttt.want, fetchedUser) { + collect.Errorf("GetUser() got = %#v, want %#v", fetchedUser, ttt.want) } }, retryDuration, tick) - - if tt.cleanup != nil { - tt.cleanup(fetchedUser.ID) - } }) } } diff --git a/internal/api/scim/integration_test/users_list_test.go b/internal/api/scim/integration_test/users_list_test.go index 8c6ccb80ef..81fbf1a5bc 100644 --- a/internal/api/scim/integration_test/users_list_test.go +++ b/internal/api/scim/integration_test/users_list_test.go @@ -23,15 +23,6 @@ var totalCountOfHumanUsers = 13 /* func TestListUser(t *testing.T) { createdUserIDs := createUsers(t, CTX, Instance.DefaultOrg.Id) - defer func() { - // only the full user needs to be deleted, all others have random identification data - // fullUser is always the first one. - _, err := Instance.Client.UserV2.DeleteUser(CTX, &user_v2.DeleteUserRequest{ - UserId: createdUserIDs[0], - }) - require.NoError(t, err) - }() - // secondary organization with same set of users, // these should never be modified. // This allows testing list requests without filters. @@ -451,7 +442,7 @@ func createUsers(t *testing.T, ctx context.Context, orgID string) []string { // create the full scim user if on primary org if orgID == Instance.DefaultOrg.Id { - fullUserCreatedResp, err := Instance.Client.SCIM.Users.Create(ctx, orgID, fullUserJson) + fullUserCreatedResp, err := Instance.Client.SCIM.Users.Create(ctx, orgID, withUsername(fullUserJson, gofakeit.Username())) require.NoError(t, err) createdUserIDs = append(createdUserIDs, fullUserCreatedResp.ID) count-- diff --git a/internal/api/scim/integration_test/users_replace_test.go b/internal/api/scim/integration_test/users_replace_test.go index 1c99592b01..7896ed4e00 100644 --- a/internal/api/scim/integration_test/users_replace_test.go +++ b/internal/api/scim/integration_test/users_replace_test.go @@ -5,11 +5,13 @@ package integration_test import ( "context" _ "embed" + "fmt" "net/http" "path" "testing" "time" + "github.com/brianvoe/gofakeit/v6" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/text/language" @@ -20,7 +22,6 @@ import ( "github.com/zitadel/zitadel/internal/integration/scim" "github.com/zitadel/zitadel/internal/test" "github.com/zitadel/zitadel/pkg/grpc/management" - "github.com/zitadel/zitadel/pkg/grpc/user/v2" ) var ( @@ -199,28 +200,28 @@ func TestReplaceUser(t *testing.T) { }, { name: "not authenticated", - body: minimalUserJson, + body: withUsername(minimalUserJson, gofakeit.Username()), ctx: context.Background(), wantErr: true, errorStatus: http.StatusUnauthorized, }, { name: "no permissions", - body: minimalUserJson, + body: withUsername(minimalUserJson, gofakeit.Username()), ctx: Instance.WithAuthorization(CTX, integration.UserTypeNoPermission), wantErr: true, errorStatus: http.StatusNotFound, }, { name: "another org", - body: minimalUserJson, + body: withUsername(minimalUserJson, gofakeit.Username()), replaceUserOrgID: SecondaryOrganization.OrganizationId, wantErr: true, errorStatus: http.StatusNotFound, }, { name: "another org with permissions", - body: minimalUserJson, + body: withUsername(minimalUserJson, gofakeit.Username()), replaceUserOrgID: SecondaryOrganization.OrganizationId, ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), wantErr: true, @@ -230,14 +231,9 @@ func TestReplaceUser(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // use iam owner => we don't want to test permissions of the create endpoint. - createdUser, err := Instance.Client.SCIM.Users.Create(Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), Instance.DefaultOrg.Id, fullUserJson) + createdUser, err := Instance.Client.SCIM.Users.Create(Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), Instance.DefaultOrg.Id, withUsername(fullUserJson, gofakeit.Username())) require.NoError(t, err) - defer func() { - _, err = Instance.Client.UserV2.DeleteUser(CTX, &user.DeleteUserRequest{UserId: createdUser.ID}) - assert.NoError(t, err) - }() - ctx := tt.ctx if ctx == nil { ctx = CTX @@ -294,10 +290,11 @@ func TestReplaceUser(t *testing.T) { func TestReplaceUser_removeOldMetadata(t *testing.T) { // ensure old metadata is removed correctly - createdUser, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, fullUserJson) + username := gofakeit.Username() + createdUser, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, withUsername(fullUserJson, username)) require.NoError(t, err) - _, err = Instance.Client.SCIM.Users.Replace(CTX, Instance.DefaultOrg.Id, createdUser.ID, minimalUserJson) + _, err = Instance.Client.SCIM.Users.Replace(CTX, Instance.DefaultOrg.Id, createdUser.ID, withUsername(minimalUserJson, username)) require.NoError(t, err) retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute) @@ -312,20 +309,17 @@ func TestReplaceUser_removeOldMetadata(t *testing.T) { for i := range md.Result { mdMap[md.Result[i].Key] = string(md.Result[i].Value) } - - test.AssertMapContains(tt, mdMap, "urn:zitadel:scim:emails", "[{\"value\":\"user1@example.com\",\"primary\":true}]") + test.AssertMapContains(tt, mdMap, "urn:zitadel:scim:emails", fmt.Sprintf("[{\"value\":\"%s@example.com\",\"primary\":true}]", username)) }, retryDuration, tick) - - _, err = Instance.Client.UserV2.DeleteUser(CTX, &user.DeleteUserRequest{UserId: createdUser.ID}) - require.NoError(t, err) } func TestReplaceUser_emailType(t *testing.T) { // ensure old metadata is removed correctly - createdUser, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, fullUserJson) + createdUser, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, withUsername(fullUserJson, gofakeit.Username())) require.NoError(t, err) - _, err = Instance.Client.SCIM.Users.Replace(CTX, Instance.DefaultOrg.Id, createdUser.ID, minimalUserWithEmailTypeReplaceJson) + replacedUsername := gofakeit.Username() + _, err = Instance.Client.SCIM.Users.Replace(CTX, Instance.DefaultOrg.Id, createdUser.ID, withUsername(minimalUserWithEmailTypeReplaceJson, replacedUsername)) require.NoError(t, err) retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute) @@ -341,28 +335,26 @@ func TestReplaceUser_emailType(t *testing.T) { mdMap[md.Result[i].Key] = string(md.Result[i].Value) } - test.AssertMapContains(tt, mdMap, "urn:zitadel:scim:emails", "[{\"value\":\"user1-minimal-replaced@example.com\",\"primary\":true,\"type\":\"work\"}]") + test.AssertMapContains(tt, mdMap, "urn:zitadel:scim:emails", fmt.Sprintf("[{\"value\":\"%s@example.com\",\"primary\":true,\"type\":\"work\"}]", replacedUsername)) }, retryDuration, tick) - - _, err = Instance.Client.UserV2.DeleteUser(CTX, &user.DeleteUserRequest{UserId: createdUser.ID}) - require.NoError(t, err) } func TestReplaceUser_scopedExternalID(t *testing.T) { - // create user without provisioning domain set - createdUser, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, fullUserJson) + createdUser, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, withUsername(fullUserJson, gofakeit.Username())) require.NoError(t, err) - + callingUserId, callingUserPat, err := Instance.CreateMachineUserPATWithMembership(CTX, "ORG_OWNER") + require.NoError(t, err) + ctx := integration.WithAuthorizationToken(CTX, callingUserPat) // set provisioning domain of service user - setProvisioningDomain(t, Instance.Users.Get(integration.UserTypeOrgOwner).ID, "fooBazz") + setProvisioningDomain(t, callingUserId, "fooBazz") // replace the user with provisioning domain set - _, err = Instance.Client.SCIM.Users.Replace(CTX, Instance.DefaultOrg.Id, createdUser.ID, minimalUserWithExternalIDJson) + _, err = Instance.Client.SCIM.Users.Replace(ctx, Instance.DefaultOrg.Id, createdUser.ID, minimalUserWithExternalIDJson) require.NoError(t, err) - retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute) + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, time.Minute) require.EventuallyWithT(t, func(tt *assert.CollectT) { - md, err := Instance.Client.Mgmt.ListUserMetadata(CTX, &management.ListUserMetadataRequest{ + md, err := Instance.Client.Mgmt.ListUserMetadata(ctx, &management.ListUserMetadataRequest{ Id: createdUser.ID, }) require.NoError(tt, err) @@ -376,9 +368,4 @@ func TestReplaceUser_scopedExternalID(t *testing.T) { test.AssertMapContains(tt, mdMap, "urn:zitadel:scim:externalId", "701984") test.AssertMapContains(tt, mdMap, "urn:zitadel:scim:fooBazz:externalId", "replaced-external-id") }, retryDuration, tick) - - _, err = Instance.Client.UserV2.DeleteUser(CTX, &user.DeleteUserRequest{UserId: createdUser.ID}) - require.NoError(t, err) - - removeProvisioningDomain(t, Instance.Users.Get(integration.UserTypeOrgOwner).ID) } diff --git a/internal/api/scim/integration_test/users_update_test.go b/internal/api/scim/integration_test/users_update_test.go index 77e55bac60..f8a65a8a69 100644 --- a/internal/api/scim/integration_test/users_update_test.go +++ b/internal/api/scim/integration_test/users_update_test.go @@ -5,11 +5,13 @@ package integration_test import ( "context" _ "embed" + "encoding/json" "fmt" "net/http" "testing" "time" + "github.com/brianvoe/gofakeit/v6" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/text/language" @@ -19,7 +21,6 @@ import ( "github.com/zitadel/zitadel/internal/integration" "github.com/zitadel/zitadel/internal/integration/scim" "github.com/zitadel/zitadel/internal/test" - "github.com/zitadel/zitadel/pkg/grpc/user/v2" ) var ( @@ -34,15 +35,7 @@ func init() { } func TestUpdateUser(t *testing.T) { - fullUserCreated, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, fullUserJson) - require.NoError(t, err) - - defer func() { - _, err = Instance.Client.UserV2.DeleteUser(CTX, &user.DeleteUserRequest{UserId: fullUserCreated.ID}) - require.NoError(t, err) - }() - - tests := []struct { + type testCase struct { name string body []byte ctx context.Context @@ -52,215 +45,297 @@ func TestUpdateUser(t *testing.T) { wantErr bool scimErrorType string errorStatus int + } + tests := []struct { + name string + setup func(t *testing.T) testCase }{ { - name: "not authenticated", - ctx: context.Background(), - body: minimalUserUpdateJson, - wantErr: true, - errorStatus: http.StatusUnauthorized, + name: "not authenticated", + setup: func(t *testing.T) testCase { + username := gofakeit.Username() + created, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, withUsername(fullUserJson, username)) + require.NoError(t, err) + return testCase{ + userID: created.ID, + ctx: context.Background(), + body: minimalUserUpdateJson, + wantErr: true, + errorStatus: http.StatusUnauthorized, + } + }, }, { - name: "no permissions", - ctx: Instance.WithAuthorization(CTX, integration.UserTypeNoPermission), - body: minimalUserUpdateJson, - wantErr: true, - errorStatus: http.StatusNotFound, + name: "no permissions", + setup: func(t *testing.T) testCase { + username := gofakeit.Username() + created, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, withUsername(fullUserJson, username)) + require.NoError(t, err) + return testCase{ + userID: created.ID, + ctx: Instance.WithAuthorization(CTX, integration.UserTypeNoPermission), + body: minimalUserUpdateJson, + wantErr: true, + errorStatus: http.StatusNotFound, + } + }, }, { - name: "other org", - orgID: SecondaryOrganization.OrganizationId, - body: minimalUserUpdateJson, - wantErr: true, - errorStatus: http.StatusNotFound, + name: "other org", + setup: func(t *testing.T) testCase { + username := gofakeit.Username() + created, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, withUsername(fullUserJson, username)) + require.NoError(t, err) + return testCase{ + userID: created.ID, + orgID: SecondaryOrganization.OrganizationId, + body: minimalUserUpdateJson, + wantErr: true, + errorStatus: http.StatusNotFound, + } + }, }, { - name: "other org with permissions", - ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), - orgID: SecondaryOrganization.OrganizationId, - body: minimalUserUpdateJson, - wantErr: true, - errorStatus: http.StatusNotFound, + name: "other org with permissions", + setup: func(t *testing.T) testCase { + username := gofakeit.Username() + created, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, withUsername(fullUserJson, username)) + require.NoError(t, err) + return testCase{ + userID: created.ID, + ctx: Instance.WithAuthorization(CTX, integration.UserTypeIAMOwner), + orgID: SecondaryOrganization.OrganizationId, + body: minimalUserUpdateJson, + wantErr: true, + errorStatus: http.StatusNotFound, + } + }, }, { - name: "invalid patch json", - body: simpleReplacePatchBody("nickname", "10"), - wantErr: true, - scimErrorType: "invalidValue", + name: "invalid patch json", + setup: func(t *testing.T) testCase { + username := gofakeit.Username() + created, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, withUsername(fullUserJson, username)) + require.NoError(t, err) + return testCase{ + userID: created.ID, + body: simpleReplacePatchBody("nickname", "10"), + wantErr: true, + scimErrorType: "invalidValue", + } + }, }, { - name: "password complexity violation", - body: simpleReplacePatchBody("password", `"fooBar"`), - wantErr: true, - scimErrorType: "invalidValue", + name: "password complexity violation", + setup: func(t *testing.T) testCase { + username := gofakeit.Username() + created, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, withUsername(fullUserJson, username)) + require.NoError(t, err) + return testCase{ + userID: created.ID, + body: simpleReplacePatchBody("password", `"fooBar"`), + wantErr: true, + scimErrorType: "invalidValue", + } + }, }, { - name: "invalid profile url", - body: simpleReplacePatchBody("profileUrl", `"ftp://example.com/profiles"`), - wantErr: true, - scimErrorType: "invalidValue", + name: "invalid profile url", + setup: func(t *testing.T) testCase { + username := gofakeit.Username() + created, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, withUsername(fullUserJson, username)) + require.NoError(t, err) + return testCase{ + userID: created.ID, + body: simpleReplacePatchBody("profileUrl", `"ftp://example.com/profiles"`), + wantErr: true, + scimErrorType: "invalidValue", + } + }, }, { - name: "invalid time zone", - body: simpleReplacePatchBody("timezone", `"foobar"`), - wantErr: true, - scimErrorType: "invalidValue", + name: "invalid time zone", + setup: func(t *testing.T) testCase { + username := gofakeit.Username() + created, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, withUsername(fullUserJson, username)) + require.NoError(t, err) + return testCase{ + userID: created.ID, + body: simpleReplacePatchBody("timezone", `"foobar"`), + wantErr: true, + scimErrorType: "invalidValue", + } + }, }, { - name: "invalid locale", - body: simpleReplacePatchBody("locale", `"foobar"`), - wantErr: true, - scimErrorType: "invalidValue", + name: "invalid locale", + setup: func(t *testing.T) testCase { + username := gofakeit.Username() + created, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, withUsername(fullUserJson, username)) + require.NoError(t, err) + return testCase{ + userID: created.ID, + body: simpleReplacePatchBody("locale", `"foobar"`), + wantErr: true, + scimErrorType: "invalidValue", + } + }, }, { - name: "unknown user id", - body: simpleReplacePatchBody("nickname", `"foo"`), - userID: "fooBar", - wantErr: true, - errorStatus: http.StatusNotFound, + name: "unknown user id", + setup: func(t *testing.T) testCase { + return testCase{ + body: simpleReplacePatchBody("nickname", `"foo"`), + userID: "fooBar", + wantErr: true, + errorStatus: http.StatusNotFound, + } + }, }, { name: "full", - body: fullUserUpdateJson, - want: &resources.ScimUser{ - ExternalID: "fooBAR", - UserName: "bjensen@example.com", - Name: &resources.ScimUserName{ - Formatted: "replaced-display-name", - FamilyName: "added-family-name", - GivenName: "added-given-name", - MiddleName: "added-middle-name-2", - HonorificPrefix: "added-honorific-prefix", - HonorificSuffix: "replaced-honorific-suffix", - }, - DisplayName: "replaced-display-name", - NickName: "", - ProfileUrl: test.Must(schemas.ParseHTTPURL("http://login.example.com/bjensen")), - Emails: []*resources.ScimEmail{ - { - Value: "bjensen@example.com", - Type: "work", + setup: func(t *testing.T) testCase { + username := gofakeit.Username() + created, err := Instance.Client.SCIM.Users.Create(CTX, Instance.DefaultOrg.Id, withUsername(fullUserJson, username)) + require.NoError(t, err) + return testCase{ + userID: created.ID, + body: withUsername(fullUserUpdateJson, username), + want: &resources.ScimUser{ + ExternalID: "fooBAR", + UserName: username + "@example.com", + Name: &resources.ScimUserName{ + Formatted: "replaced-display-name", + FamilyName: "added-family-name", + GivenName: "added-given-name", + MiddleName: "added-middle-name-2", + HonorificPrefix: "added-honorific-prefix", + HonorificSuffix: "replaced-honorific-suffix", + }, + DisplayName: "replaced-display-name", + NickName: "", + ProfileUrl: test.Must(schemas.ParseHTTPURL("http://login.example.com/bjensen")), + Emails: []*resources.ScimEmail{ + { + Value: username + "@example.com", + Type: "work", + }, + { + Value: username + "+1@example.com", + Type: "home", + }, + { + Value: username + "+2@example.com", + Primary: true, + Type: "home", + }, + }, + Addresses: []*resources.ScimAddress{ + { + Type: "replaced-work", + StreetAddress: "replaced-100 Universal City Plaza", + Locality: "replaced-Hollywood", + Region: "replaced-CA", + PostalCode: "replaced-91608", + Country: "replaced-USA", + Formatted: "replaced-100 Universal City Plaza\nHollywood, CA 91608 USA", + Primary: true, + }, + }, + PhoneNumbers: []*resources.ScimPhoneNumber{ + { + Value: "+41711234567", + Primary: true, + }, + }, + Ims: []*resources.ScimIms{ + { + Value: "someaimhandle", + Type: "aim", + }, + { + Value: "twitterhandle", + Type: "", + }, + }, + Photos: []*resources.ScimPhoto{ + { + Value: *test.Must(schemas.ParseHTTPURL("https://photos.example.com/profilephoto/72930000000Ccne/F")), + Type: "photo", + }, + }, + Roles: nil, + Entitlements: []*resources.ScimEntitlement{ + { + Value: "my-entitlement-1", + Display: "added-entitlement-1", + Type: "added-entitlement-1", + Primary: false, + }, + { + Value: "my-entitlement-2", + Display: "Entitlement 2", + Type: "secondary-entitlement", + Primary: false, + }, + { + Value: "added-entitlement-1", + Primary: false, + }, + { + Value: "added-entitlement-2", + Primary: false, + }, + { + Value: "added-entitlement-3", + Primary: true, + }, + }, + Title: "Tour Guide", + PreferredLanguage: language.MustParse("en"), + Locale: "en-US", + Timezone: "America/Los_Angeles", + Active: schemas.NewRelaxedBool(true), }, - { - Value: "babs@jensen.org", - Type: "home", - }, - { - Value: "babs@example.com", - Primary: true, - Type: "home", - }, - }, - Addresses: []*resources.ScimAddress{ - { - Type: "replaced-work", - StreetAddress: "replaced-100 Universal City Plaza", - Locality: "replaced-Hollywood", - Region: "replaced-CA", - PostalCode: "replaced-91608", - Country: "replaced-USA", - Formatted: "replaced-100 Universal City Plaza\nHollywood, CA 91608 USA", - Primary: true, - }, - }, - PhoneNumbers: []*resources.ScimPhoneNumber{ - { - Value: "+41711234567", - Primary: true, - }, - }, - Ims: []*resources.ScimIms{ - { - Value: "someaimhandle", - Type: "aim", - }, - { - Value: "twitterhandle", - Type: "", - }, - }, - Photos: []*resources.ScimPhoto{ - { - Value: *test.Must(schemas.ParseHTTPURL("https://photos.example.com/profilephoto/72930000000Ccne/F")), - Type: "photo", - }, - }, - Roles: nil, - Entitlements: []*resources.ScimEntitlement{ - { - Value: "my-entitlement-1", - Display: "added-entitlement-1", - Type: "added-entitlement-1", - Primary: false, - }, - { - Value: "my-entitlement-2", - Display: "Entitlement 2", - Type: "secondary-entitlement", - Primary: false, - }, - { - Value: "added-entitlement-1", - Primary: false, - }, - { - Value: "added-entitlement-2", - Primary: false, - }, - { - Value: "added-entitlement-3", - Primary: true, - }, - }, - Title: "Tour Guide", - PreferredLanguage: language.MustParse("en-US"), - Locale: "en-US", - Timezone: "America/Los_Angeles", - Active: schemas.NewRelaxedBool(true), + } }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if tt.ctx == nil { - tt.ctx = CTX + ttt := tt.setup(t) + if ttt.orgID == "" { + ttt.orgID = Instance.DefaultOrg.Id } - - if tt.orgID == "" { - tt.orgID = Instance.DefaultOrg.Id + if ttt.ctx == nil { + ttt.ctx = CTX } - - if tt.userID == "" { - tt.userID = fullUserCreated.ID - } - - err := Instance.Client.SCIM.Users.Update(tt.ctx, tt.orgID, tt.userID, tt.body) - - if tt.wantErr { + err := Instance.Client.SCIM.Users.Update(ttt.ctx, ttt.orgID, ttt.userID, ttt.body) + if ttt.wantErr { require.Error(t, err) - - statusCode := tt.errorStatus + statusCode := ttt.errorStatus if statusCode == 0 { statusCode = http.StatusBadRequest } - scimErr := scim.RequireScimError(t, statusCode, err) - assert.Equal(t, tt.scimErrorType, scimErr.Error.ScimType) + assert.Equal(t, ttt.scimErrorType, scimErr.Error.ScimType) return } require.NoError(t, err) - retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute) - require.EventuallyWithT(t, func(ttt *assert.CollectT) { - fetchedUser, err := Instance.Client.SCIM.Users.Get(tt.ctx, tt.orgID, fullUserCreated.ID) - require.NoError(ttt, err) - + require.EventuallyWithT(t, func(collect *assert.CollectT) { + fetchedUser, err := Instance.Client.SCIM.Users.Get(ttt.ctx, ttt.orgID, ttt.userID) + if !assert.NoError(collect, err) { + return + } fetchedUser.Resource = nil fetchedUser.ID = "" - if tt.want != nil && !test.PartiallyDeepEqual(tt.want, fetchedUser) { - ttt.Errorf("got = %#v, want = %#v", fetchedUser, tt.want) - } + fetched, err := json.Marshal(fetchedUser) + require.NoError(collect, err) + want, err := json.Marshal(ttt.want) + require.NoError(collect, err) + assert.JSONEq(collect, string(want), string(fetched)) }, retryDuration, tick) }) } diff --git a/internal/api/scim/metadata/context.go b/internal/api/scim/metadata/context.go index dd8ef54d8e..2d0d76b928 100644 --- a/internal/api/scim/metadata/context.go +++ b/internal/api/scim/metadata/context.go @@ -15,6 +15,7 @@ var scimContextKey scimContextKeyType type ScimContextData struct { ProvisioningDomain string + IgnorePasswordOnCreate bool ExternalIDScopedMetadataKey ScopedKey bulkIDMapping map[string]string } diff --git a/internal/api/scim/metadata/metadata.go b/internal/api/scim/metadata/metadata.go index 28e42290d1..eb20284821 100644 --- a/internal/api/scim/metadata/metadata.go +++ b/internal/api/scim/metadata/metadata.go @@ -13,8 +13,9 @@ type ScopedKey string const ( externalIdProvisioningDomainPlaceholder = "{provisioningDomain}" - KeyPrefix = "urn:zitadel:scim:" - KeyProvisioningDomain Key = KeyPrefix + "provisioningDomain" + KeyPrefix = "urn:zitadel:scim:" + KeyProvisioningDomain Key = KeyPrefix + "provisioningDomain" + KeyIgnorePasswordOnCreate Key = KeyPrefix + "ignorePasswordOnCreate" KeyExternalId Key = KeyPrefix + "externalId" keyScopedExternalIdTemplate = KeyPrefix + externalIdProvisioningDomainPlaceholder + ":externalId" diff --git a/internal/api/scim/middleware/scim_context_middleware.go b/internal/api/scim/middleware/scim_context_middleware.go index 1ec917e18b..e9b6f48aa2 100644 --- a/internal/api/scim/middleware/scim_context_middleware.go +++ b/internal/api/scim/middleware/scim_context_middleware.go @@ -3,10 +3,15 @@ package middleware import ( "context" "net/http" + "strconv" + "strings" + + "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" zhttp "github.com/zitadel/zitadel/internal/api/http/middleware" smetadata "github.com/zitadel/zitadel/internal/api/scim/metadata" + sresources "github.com/zitadel/zitadel/internal/api/scim/resources" "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/zerrors" ) @@ -29,22 +34,43 @@ func initScimContext(ctx context.Context, q *query.Queries) (context.Context, er ctx = smetadata.SetScimContextData(ctx, data) userID := authz.GetCtxData(ctx).UserID - metadata, err := q.GetUserMetadataByKey(ctx, false, userID, string(smetadata.KeyProvisioningDomain), false) + + // get the provisioningDomain and ignorePasswordOnCreate metadata keys associated with the service user + metadataKeys := []smetadata.Key{ + smetadata.KeyProvisioningDomain, + smetadata.KeyIgnorePasswordOnCreate, + } + queries := sresources.BuildMetadataQueries(ctx, metadataKeys) + + metadataList, err := q.SearchUserMetadata(ctx, false, userID, queries, nil) if err != nil { if zerrors.IsNotFound(err) { return ctx, nil } - return ctx, err } - if metadata == nil { + if metadataList == nil || len(metadataList.Metadata) == 0 { return ctx, nil } - data.ProvisioningDomain = string(metadata.Value) - if data.ProvisioningDomain != "" { - data.ExternalIDScopedMetadataKey = smetadata.ScopeExternalIdKey(data.ProvisioningDomain) + for _, metadata := range metadataList.Metadata { + switch metadata.Key { + case string(smetadata.KeyProvisioningDomain): + data.ProvisioningDomain = string(metadata.Value) + if data.ProvisioningDomain != "" { + data.ExternalIDScopedMetadataKey = smetadata.ScopeExternalIdKey(data.ProvisioningDomain) + } + case string(smetadata.KeyIgnorePasswordOnCreate): + ignorePasswordOnCreate, parseErr := strconv.ParseBool(strings.TrimSpace(string(metadata.Value))) + if parseErr != nil { + return ctx, + zerrors.ThrowInvalidArgumentf(nil, "SMCM-yvw2rt", "Invalid value for metadata key %s: %s", smetadata.KeyIgnorePasswordOnCreate, metadata.Value) + } + data.IgnorePasswordOnCreate = ignorePasswordOnCreate + default: + logging.WithFields("user_metadata_key", metadata.Key).Warn("unexpected metadata key") + } } return smetadata.SetScimContextData(ctx, data), nil } diff --git a/internal/api/scim/resources/user.go b/internal/api/scim/resources/user.go index 1e2eb1c6d9..dbf97e0eae 100644 --- a/internal/api/scim/resources/user.go +++ b/internal/api/scim/resources/user.go @@ -204,7 +204,6 @@ func (h *UsersHandler) Delete(ctx context.Context, id string) error { if err != nil { return err } - _, err = h.command.RemoveUserV2(ctx, id, authz.GetCtxData(ctx).OrgID, memberships, grants...) return err } @@ -241,7 +240,7 @@ func (h *UsersHandler) List(ctx context.Context, request *ListRequest) (*ListRes return NewListResponse(count, q.SearchRequest, make([]*ScimUser, 0)), nil } - users, err := h.query.SearchUsers(ctx, q, authz.GetCtxData(ctx).OrgID, nil) + users, err := h.query.SearchUsers(ctx, q, nil) if err != nil { return nil, err } @@ -263,7 +262,7 @@ func (h *UsersHandler) queryUserDependencies(ctx context.Context, userID string) grants, err := h.query.UserGrants(ctx, &query.UserGrantsQueries{ Queries: []query.SearchQuery{userGrantUserQuery}, - }, true) + }, true, nil) if err != nil { return nil, nil, err } diff --git a/internal/api/scim/resources/user_mapping.go b/internal/api/scim/resources/user_mapping.go index 260e50846a..0aac888173 100644 --- a/internal/api/scim/resources/user_mapping.go +++ b/internal/api/scim/resources/user_mapping.go @@ -45,7 +45,12 @@ func (h *UsersHandler) mapToAddHuman(ctx context.Context, scimUser *ScimUser) (* } human.Metadata = md - if scimUser.Password != nil { + // Okta sends a random password during SCIM provisioning + // irrespective of whether the Sync Password option is enabled or disabled on Okta. + // This password does not comply with Zitadel's password complexity, and + // the following workaround ignores the random password as it does not add any value. + ignorePasswordOnCreate := metadata.GetScimContextData(ctx).IgnorePasswordOnCreate + if scimUser.Password != nil && !ignorePasswordOnCreate { human.Password = scimUser.Password.String() scimUser.Password = nil } diff --git a/internal/api/scim/resources/user_metadata.go b/internal/api/scim/resources/user_metadata.go index 3e018507fe..8cf8693691 100644 --- a/internal/api/scim/resources/user_metadata.go +++ b/internal/api/scim/resources/user_metadata.go @@ -21,7 +21,7 @@ import ( ) func (h *UsersHandler) queryMetadataForUsers(ctx context.Context, userIds []string) (map[string]map[metadata.ScopedKey][]byte, error) { - queries := h.buildMetadataQueries(ctx) + queries := BuildMetadataQueries(ctx, metadata.ScimUserRelevantMetadataKeys) md, err := h.query.SearchUserMetadataForUsers(ctx, false, userIds, queries) if err != nil { @@ -43,9 +43,9 @@ func (h *UsersHandler) queryMetadataForUsers(ctx context.Context, userIds []stri } func (h *UsersHandler) queryMetadataForUser(ctx context.Context, id string) (map[metadata.ScopedKey][]byte, error) { - queries := h.buildMetadataQueries(ctx) + queries := BuildMetadataQueries(ctx, metadata.ScimUserRelevantMetadataKeys) - md, err := h.query.SearchUserMetadata(ctx, false, id, queries, false) + md, err := h.query.SearchUserMetadata(ctx, false, id, queries, nil) if err != nil { return nil, err } @@ -53,9 +53,9 @@ func (h *UsersHandler) queryMetadataForUser(ctx context.Context, id string) (map return metadata.MapListToScopedKeyMap(md.Metadata), nil } -func (h *UsersHandler) buildMetadataQueries(ctx context.Context) *query.UserMetadataSearchQueries { - keyQueries := make([]query.SearchQuery, len(metadata.ScimUserRelevantMetadataKeys)) - for i, key := range metadata.ScimUserRelevantMetadataKeys { +func BuildMetadataQueries(ctx context.Context, metadataKeys []metadata.Key) *query.UserMetadataSearchQueries { + keyQueries := make([]query.SearchQuery, len(metadataKeys)) + for i, key := range metadataKeys { keyQueries[i] = buildMetadataKeyQuery(ctx, key) } diff --git a/internal/api/ui/login/external_provider_handler.go b/internal/api/ui/login/external_provider_handler.go index a1ca68928c..6f851df562 100644 --- a/internal/api/ui/login/external_provider_handler.go +++ b/internal/api/ui/login/external_provider_handler.go @@ -48,6 +48,18 @@ const ( tmplExternalNotFoundOption = "externalnotfoundoption" ) +var ( + samlFormPost = template.Must(template.New("saml-post-form").Parse(` +
+{{range $key, $value := .Fields}} + +{{end}} + +
+ +`)) +) + type externalIDPData struct { IDPConfigID string `schema:"idpConfigID"` } @@ -201,15 +213,21 @@ func (l *Login) handleIDP(w http.ResponseWriter, r *http.Request, authReq *domai l.externalAuthFailed(w, r, authReq, err) return } - - content, redirect := session.GetAuth(r.Context()) - if redirect { - http.Redirect(w, r, content, http.StatusFound) + auth, err := session.GetAuth(r.Context()) + if err != nil { + l.renderInternalError(w, r, authReq, err) return } - _, err = w.Write([]byte(content)) - if err != nil { - l.renderError(w, r, authReq, err) + switch a := auth.(type) { + case *idp.RedirectAuth: + http.Redirect(w, r, a.RedirectURL, http.StatusFound) + return + case *idp.FormAuth: + err = samlFormPost.Execute(w, a) + if err != nil { + l.renderError(w, r, authReq, err) + return + } return } } @@ -1243,7 +1261,8 @@ func (l *Login) appendUserGrants(ctx context.Context, userGrants []*domain.UserG return nil } for _, grant := range userGrants { - _, err := l.command.AddUserGrant(setContext(ctx, resourceOwner), grant, resourceOwner) + grant.ResourceOwner = resourceOwner + _, err := l.command.AddUserGrant(setContext(ctx, resourceOwner), grant, nil) if err != nil { return err } diff --git a/internal/api/ui/login/static/i18n/bg.yaml b/internal/api/ui/login/static/i18n/bg.yaml index 2a7191edb8..cf79d9a9e7 100644 --- a/internal/api/ui/login/static/i18n/bg.yaml +++ b/internal/api/ui/login/static/i18n/bg.yaml @@ -262,6 +262,7 @@ RegistrationUser: Hungarian: Magyar Korean: 한국어 Romanian: Română + Turkish: Türkçe GenderLabel: Пол Female: Женски пол Male: Мъжки @@ -305,6 +306,7 @@ ExternalRegistrationUserOverview: Hungarian: Magyar Korean: 한국어 Romanian: Română + Turkish: Türkçe TosAndPrivacyLabel: Правила и условия TosConfirm: Приемам TosLinkText: TOS @@ -377,6 +379,7 @@ ExternalNotFound: Hungarian: Magyar Korean: 한국어 Romanian: Română + Turkish: Türkçe DeviceAuth: Title: Упълномощаване на устройството UserCode: diff --git a/internal/api/ui/login/static/i18n/cs.yaml b/internal/api/ui/login/static/i18n/cs.yaml index aa77730dd9..58766fe1c1 100644 --- a/internal/api/ui/login/static/i18n/cs.yaml +++ b/internal/api/ui/login/static/i18n/cs.yaml @@ -266,6 +266,7 @@ RegistrationUser: Hungarian: Magyar Korean: 한국어 Romanian: Română + Turkish: Türkçe GenderLabel: Pohlaví Female: Žena Male: Muž @@ -310,6 +311,7 @@ ExternalRegistrationUserOverview: Hungarian: Magyar Korean: 한국어 Romanian: Română + Turkish: Türkçe TosAndPrivacyLabel: Obchodní podmínky TosConfirm: Souhlasím s TosLinkText: obchodními podmínkami @@ -388,6 +390,7 @@ ExternalNotFound: Hungarian: Magyar Korean: 한국어 Romanian: Română + Turkish: Türkçe DeviceAuth: Title: Autorizace zařízení UserCode: diff --git a/internal/api/ui/login/static/i18n/de.yaml b/internal/api/ui/login/static/i18n/de.yaml index ee2b1b6ad2..f19e8f6778 100644 --- a/internal/api/ui/login/static/i18n/de.yaml +++ b/internal/api/ui/login/static/i18n/de.yaml @@ -265,6 +265,7 @@ RegistrationUser: Hungarian: Magyar Korean: 한국어 Romanian: Română + Turkish: Türkçe GenderLabel: Geschlecht Female: weiblich Male: männlich @@ -309,6 +310,7 @@ ExternalRegistrationUserOverview: Hungarian: Magyar Korean: 한국어 Romanian: Română + Turkish: Türkçe TosAndPrivacyLabel: Allgemeine Geschäftsbedingungen und Datenschutz TosConfirm: Ich akzeptiere die TosLinkText: AGB @@ -387,6 +389,7 @@ ExternalNotFound: Hungarian: Magyar Korean: 한국어 Romanian: Română + Turkish: Türkçe DeviceAuth: Title: Gerät verbinden UserCode: diff --git a/internal/api/ui/login/static/i18n/en.yaml b/internal/api/ui/login/static/i18n/en.yaml index 39be340e2c..7cb55799bf 100644 --- a/internal/api/ui/login/static/i18n/en.yaml +++ b/internal/api/ui/login/static/i18n/en.yaml @@ -266,6 +266,7 @@ RegistrationUser: Hungarian: Magyar Korean: 한국어 Romanian: Română + Turkish: Türkçe GenderLabel: Gender Female: Female Male: Male @@ -310,6 +311,7 @@ ExternalRegistrationUserOverview: Hungarian: Magyar Korean: 한국어 Romanian: Română + Turkish: Türkçe TosAndPrivacyLabel: Terms and conditions TosConfirm: I accept the TosLinkText: TOS @@ -388,6 +390,7 @@ ExternalNotFound: Hungarian: Magyar Korean: 한국어 Romanian: Română + Turkish: Türkçe DeviceAuth: Title: Device Authorization UserCode: diff --git a/internal/api/ui/login/static/i18n/es.yaml b/internal/api/ui/login/static/i18n/es.yaml index 8f86cd12ae..818851ee8b 100644 --- a/internal/api/ui/login/static/i18n/es.yaml +++ b/internal/api/ui/login/static/i18n/es.yaml @@ -266,6 +266,7 @@ RegistrationUser: Hungarian: Magyar Korean: 한국어 Romanian: Română + Turkish: Türkçe GenderLabel: Género Female: Mujer Male: Hombre @@ -310,6 +311,7 @@ ExternalRegistrationUserOverview: Hungarian: Magyar Korean: 한국어 Romanian: Română + Turkish: Türkçe TosAndPrivacyLabel: Términos y condiciones TosConfirm: Acepto los TosLinkText: TDS @@ -388,6 +390,7 @@ ExternalNotFound: Hungarian: Magyar Korean: 한국어 Romanian: Română + Turkish: Türkçe Footer: PoweredBy: Powered By diff --git a/internal/api/ui/login/static/i18n/fr.yaml b/internal/api/ui/login/static/i18n/fr.yaml index 898d35d707..3e306e610e 100644 --- a/internal/api/ui/login/static/i18n/fr.yaml +++ b/internal/api/ui/login/static/i18n/fr.yaml @@ -266,6 +266,7 @@ RegistrationUser: Hungarian: Magyar Korean: 한국어 Romanian: Română + Turkish: Türkçe GenderLabel: Genre Female: Femme Male: Homme @@ -310,6 +311,7 @@ ExternalRegistrationUserOverview: Hungarian: Magyar Korean: 한국어 Romanian: Română + Turkish: Türkçe TosAndPrivacyLabel: Termes et conditions TosConfirm: J'accepte les TosLinkText: TOS @@ -388,6 +390,7 @@ ExternalNotFound: Hungarian: Magyar Korean: 한국어 Romanian: Română + Turkish: Türkçe DeviceAuth: Title: Autorisation de l'appareil diff --git a/internal/api/ui/login/static/i18n/hu.yaml b/internal/api/ui/login/static/i18n/hu.yaml index c5d8416b89..008b242d28 100644 --- a/internal/api/ui/login/static/i18n/hu.yaml +++ b/internal/api/ui/login/static/i18n/hu.yaml @@ -236,6 +236,7 @@ RegistrationUser: Hungarian: Magyar Korean: 한국어 Romanian: Română + Turkish: Türkçe GenderLabel: Nem Female: Nő Male: Férfi @@ -279,6 +280,7 @@ ExternalRegistrationUserOverview: Hungarian: Magyar Korean: 한국어 Romanian: Română + Turkish: Türkçe TosAndPrivacyLabel: Felhasználási feltételek TosConfirm: Elfogadom a TosLinkText: TOS @@ -351,6 +353,7 @@ ExternalNotFound: Hungarian: Magyar Korean: 한국어 Romanian: Română + Turkish: Türkçe DeviceAuth: Title: Eszköz engedélyezése UserCode: diff --git a/internal/api/ui/login/static/i18n/id.yaml b/internal/api/ui/login/static/i18n/id.yaml index dbd15431a7..0e043e31bc 100644 --- a/internal/api/ui/login/static/i18n/id.yaml +++ b/internal/api/ui/login/static/i18n/id.yaml @@ -236,6 +236,7 @@ RegistrationUser: Hungarian: Magyar Korean: 한국어 Romanian: Română + Turkish: Türkçe GenderLabel: Jenis kelamin Female: Perempuan Male: Pria @@ -279,6 +280,7 @@ ExternalRegistrationUserOverview: Hungarian: Magyar Korean: 한국어 Romanian: Română + Turkish: Türkçe TosAndPrivacyLabel: Syarat dan Ketentuan TosConfirm: Saya menerima itu TosLinkText: KL @@ -351,6 +353,7 @@ ExternalNotFound: Hungarian: Magyar Korean: 한국어 Romanian: Română + Turkish: Türkçe DeviceAuth: Title: Otorisasi Perangkat UserCode: diff --git a/internal/api/ui/login/static/i18n/it.yaml b/internal/api/ui/login/static/i18n/it.yaml index dae4d9bc4e..92b16259ac 100644 --- a/internal/api/ui/login/static/i18n/it.yaml +++ b/internal/api/ui/login/static/i18n/it.yaml @@ -266,6 +266,7 @@ RegistrationUser: Hungarian: Magyar Korean: 한국어 Romanian: Română + Turkish: Türkçe GenderLabel: Genere Female: Femminile Male: Maschile @@ -310,6 +311,7 @@ ExternalRegistrationUserOverview: Hungarian: Magyar Korean: 한국어 Romanian: Română + Turkish: Türkçe TosAndPrivacyLabel: Termini di servizio TosConfirm: Accetto i TosLinkText: Termini di servizio @@ -388,6 +390,7 @@ ExternalNotFound: Hungarian: Magyar Korean: 한국어 Romanian: Română + Turkish: Türkçe DeviceAuth: Title: Autorizzazione del dispositivo diff --git a/internal/api/ui/login/static/i18n/ja.yaml b/internal/api/ui/login/static/i18n/ja.yaml index 66c0addfd1..a7db4a0792 100644 --- a/internal/api/ui/login/static/i18n/ja.yaml +++ b/internal/api/ui/login/static/i18n/ja.yaml @@ -266,6 +266,7 @@ RegistrationUser: Hungarian: Magyar Korean: 한국어 Romanian: Română + Turkish: Türkçe GenderLabel: 性別 Female: 女性 Male: 男性 @@ -310,6 +311,7 @@ ExternalRegistrationUserOverview: Hungarian: Magyar Korean: 한국어 Romanian: Română + Turkish: Türkçe TosAndPrivacyLabel: 利用規約 TosConfirm: 私は利用規約を承諾します。 TosLinkText: TOS @@ -388,6 +390,7 @@ ExternalNotFound: Hungarian: Magyar Korean: 한국어 Romanian: Română + Turkish: Türkçe DeviceAuth: Title: デバイス認証 diff --git a/internal/api/ui/login/static/i18n/ko.yaml b/internal/api/ui/login/static/i18n/ko.yaml index b3bc340e2b..bed9581239 100644 --- a/internal/api/ui/login/static/i18n/ko.yaml +++ b/internal/api/ui/login/static/i18n/ko.yaml @@ -266,6 +266,7 @@ RegistrationUser: Hungarian: Magyar Korean: 한국어 Romanian: Română + Turkish: Türkçe GenderLabel: 성별 Female: 여성 Male: 남성 @@ -310,6 +311,7 @@ ExternalRegistrationUserOverview: Hungarian: Magyar Korean: 한국어 Romanian: Română + Turkish: Türkçe TosAndPrivacyLabel: 동의사항 TosConfirm: 이용 약관에 동의합니다. TosLinkText: 이용 약관 @@ -388,6 +390,7 @@ ExternalNotFound: Hungarian: Magyar Korean: 한국어 Romanian: Română + Turkish: Türkçe DeviceAuth: Title: 기기 인증 UserCode: diff --git a/internal/api/ui/login/static/i18n/mk.yaml b/internal/api/ui/login/static/i18n/mk.yaml index 96369c553a..8a6819ead6 100644 --- a/internal/api/ui/login/static/i18n/mk.yaml +++ b/internal/api/ui/login/static/i18n/mk.yaml @@ -266,6 +266,7 @@ RegistrationUser: Hungarian: Magyar Korean: 한국어 Romanian: Română + Turkish: Türkçe GenderLabel: Пол Female: Женски Male: Машки @@ -310,6 +311,7 @@ ExternalRegistrationUserOverview: Hungarian: Magyar Korean: 한국어 Romanian: Română + Turkish: Türkçe TosAndPrivacyLabel: Правила и услови TosConfirm: Се согласувам со TosLinkText: правилата за користење @@ -388,6 +390,7 @@ ExternalNotFound: Hungarian: Magyar Korean: 한국어 Romanian: Română + Turkish: Türkçe DeviceAuth: Title: Овластување преку уред diff --git a/internal/api/ui/login/static/i18n/nl.yaml b/internal/api/ui/login/static/i18n/nl.yaml index bb3a9a414f..4a9de56e50 100644 --- a/internal/api/ui/login/static/i18n/nl.yaml +++ b/internal/api/ui/login/static/i18n/nl.yaml @@ -266,6 +266,7 @@ RegistrationUser: Hungarian: Magyar Korean: 한국어 Romanian: Română + Turkish: Türkçe GenderLabel: Geslacht Female: Vrouw Male: Man @@ -310,6 +311,7 @@ ExternalRegistrationUserOverview: Hungarian: Magyar Korean: 한국어 Romanian: Română + Turkish: Türkçe TosAndPrivacyLabel: Algemene voorwaarden TosConfirm: Ik accepteer de TosLinkText: AV @@ -388,6 +390,7 @@ ExternalNotFound: Hungarian: Magyar Korean: 한국어 Romanian: Română + Turkish: Türkçe DeviceAuth: Title: Apparaat Autorisatie UserCode: diff --git a/internal/api/ui/login/static/i18n/pl.yaml b/internal/api/ui/login/static/i18n/pl.yaml index ef05514ee5..9bb01038d9 100644 --- a/internal/api/ui/login/static/i18n/pl.yaml +++ b/internal/api/ui/login/static/i18n/pl.yaml @@ -266,6 +266,7 @@ RegistrationUser: Hungarian: Magyar Korean: 한국어 Romanian: Română + Turkish: Türkçe GenderLabel: Płeć Female: Kobieta Male: Mężczyzna @@ -310,6 +311,7 @@ ExternalRegistrationUserOverview: Hungarian: Magyar Korean: 한국어 Romanian: Română + Turkish: Türkçe TosAndPrivacyLabel: Warunki i zasady TosConfirm: Akceptuję TosLinkText: Warunki korzystania @@ -388,6 +390,7 @@ ExternalNotFound: Hungarian: Magyar Korean: 한국어 Romanian: Română + Turkish: Türkçe DeviceAuth: Title: Autoryzacja urządzenia diff --git a/internal/api/ui/login/static/i18n/pt.yaml b/internal/api/ui/login/static/i18n/pt.yaml index 6899aed541..51f4c9b59b 100644 --- a/internal/api/ui/login/static/i18n/pt.yaml +++ b/internal/api/ui/login/static/i18n/pt.yaml @@ -262,6 +262,7 @@ RegistrationUser: Hungarian: Magyar Korean: 한국어 Romanian: Română + Turkish: Türkçe GenderLabel: Gênero Female: Feminino Male: Masculino @@ -306,6 +307,7 @@ ExternalRegistrationUserOverview: Hungarian: Magyar Korean: 한국어 Romanian: Română + Turkish: Türkçe TosAndPrivacyLabel: Termos e condições TosConfirm: Eu aceito os TosLinkText: termos de serviço @@ -384,6 +386,7 @@ ExternalNotFound: Hungarian: Magyar Korean: 한국어 Romanian: Română + Turkish: Türkçe DeviceAuth: Title: Autorização de dispositivo diff --git a/internal/api/ui/login/static/i18n/ro.yaml b/internal/api/ui/login/static/i18n/ro.yaml index ceef26f6a4..4fe1ae46c1 100644 --- a/internal/api/ui/login/static/i18n/ro.yaml +++ b/internal/api/ui/login/static/i18n/ro.yaml @@ -266,6 +266,7 @@ RegistrationUser: Hungarian: Magyar Korean: 한국어 Romanian: Română + Turkish: Türkçe GenderLabel: Gen Female: Femeie Male: Bărbat @@ -310,6 +311,7 @@ ExternalRegistrationUserOverview: Hungarian: Magyar Korean: 한국어 Romanian: Română + Turkish: Türkçe TosAndPrivacyLabel: Termeni și condiții TosConfirm: Accept TosLinkText: TOS @@ -388,6 +390,7 @@ ExternalNotFound: Hungarian: Magyar Korean: 한국어 Romanian: Română + Turkish: Türkçe DeviceAuth: Title: Autorizare dispozitiv UserCode: diff --git a/internal/api/ui/login/static/i18n/ru.yaml b/internal/api/ui/login/static/i18n/ru.yaml index 7d5c2b0f98..d6b92278db 100644 --- a/internal/api/ui/login/static/i18n/ru.yaml +++ b/internal/api/ui/login/static/i18n/ru.yaml @@ -266,6 +266,7 @@ RegistrationUser: Hungarian: Magyar Korean: 한국어 Romanian: Română + Turkish: Türkçe GenderLabel: Пол Female: Женский Male: Мужской @@ -310,6 +311,7 @@ ExternalRegistrationUserOverview: Hungarian: Magyar Korean: 한국어 Romanian: Română + Turkish: Türkçe TosAndPrivacyLabel: Условия использования TosConfirm: Я согласен с TosLinkText: Пользовательским соглашением @@ -388,6 +390,7 @@ ExternalNotFound: Hungarian: Magyar Korean: 한국어 Romanian: Română + Turkish: Türkçe DeviceAuth: Title: Авторизация устройства diff --git a/internal/api/ui/login/static/i18n/sv.yaml b/internal/api/ui/login/static/i18n/sv.yaml index f7398465c0..9fc7f98854 100644 --- a/internal/api/ui/login/static/i18n/sv.yaml +++ b/internal/api/ui/login/static/i18n/sv.yaml @@ -266,6 +266,7 @@ RegistrationUser: Hungarian: Magyar Korean: 한국어 Romanian: Română + Turkish: Türkçe GenderLabel: Kön Female: Man Male: Kvinna @@ -310,6 +311,7 @@ ExternalRegistrationUserOverview: Hungarian: Magyar Korean: 한국어 Romanian: Română + Turkish: Türkçe TosAndPrivacyLabel: Användarvillkor TosConfirm: Jag accepterar TosLinkText: Användarvillkoren @@ -388,6 +390,7 @@ ExternalNotFound: Hungarian: Magyar Korean: 한국어 Romanian: Română + Turkish: Türkçe DeviceAuth: Title: Tillgång från hårdvaruenhet UserCode: diff --git a/internal/api/ui/login/static/i18n/tr.yaml b/internal/api/ui/login/static/i18n/tr.yaml new file mode 100644 index 0000000000..623a254be1 --- /dev/null +++ b/internal/api/ui/login/static/i18n/tr.yaml @@ -0,0 +1,531 @@ +Login: + Title: Tekrar Hoş Geldiniz! + Description: Giriş bilgilerinizi girin. + TitleLinking: Kullanıcı bağlama için giriş + DescriptionLinking: Harici kullanıcınızı bağlamak için giriş bilgilerinizi girin. + LoginNameLabel: Giriş Adı + UsernamePlaceHolder: kullanıcıadı + LoginnamePlaceHolder: kullanıcıadı@domain + ExternalUserDescription: Harici kullanıcı ile giriş yapın. + MustBeMemberOfOrg: Kullanıcı {{.OrgName}} organizasyonunun üyesi olmalıdır. + RegisterButtonText: Kayıt Ol + NextButtonText: İleri + +LDAP: + Title: Giriş + Description: Giriş bilgilerinizi girin. + LoginNameLabel: Giriş Adı + PasswordLabel: Şifre + NextButtonText: İleri + +SelectAccount: + Title: Hesap Seç + Description: Hesabınızı kullanın + TitleLinking: Kullanıcı bağlama için hesap seç + DescriptionLinking: Harici kullanıcınızla bağlamak için hesabınızı seçin. + OtherUser: Diğer Kullanıcı + SessionState0: aktif + SessionState1: Çıkış yapıldı + MustBeMemberOfOrg: Kullanıcı {{.OrgName}} organizasyonunun üyesi olmalıdır. + +Password: + Title: Şifre + Description: Giriş bilgilerinizi girin. + PasswordLabel: Şifre + MinLength: En az + MinLengthp2: karakter uzunluğunda olmalıdır. + MaxLength: 70 karakterden az olmalıdır. + HasUppercase: Büyük harf içermelidir. + HasLowercase: Küçük harf içermelidir. + HasNumber: Rakam içermelidir. + HasSymbol: Sembol içermelidir. + Confirmation: Şifre onayı eşleşti. + ResetLinkText: Şifreyi Sıfırla + BackButtonText: Geri + NextButtonText: İleri + +UsernameChange: + Title: Kullanıcı Adını Değiştir + Description: Yeni kullanıcı adınızı belirleyin + UsernameLabel: Kullanıcı Adı + CancelButtonText: İptal + NextButtonText: İleri + +UsernameChangeDone: + Title: Kullanıcı Adı Değiştirildi + Description: Kullanıcı adınız başarıyla değiştirildi. + NextButtonText: İleri + +InitPassword: + Title: Şifre Belirle + Description: Yeni şifrenizi belirlemek için aşağıdaki forma girmeniz gereken bir kod aldınız. + CodeLabel: Kod + NewPasswordLabel: Yeni Şifre + NewPasswordConfirmLabel: Şifreyi Onayla + ResendButtonText: Kodu Tekrar Gönder + NextButtonText: İleri + +InitPasswordDone: + Title: Şifre Belirlendi + Description: Şifre başarıyla belirlendi + NextButtonText: İleri + CancelButtonText: İptal + +InitUser: + Title: Kullanıcıyı Etkinleştir + Description: E-postanızı aşağıdaki kod ile doğrulayın ve şifrenizi belirleyin. + CodeLabel: Kod + NewPasswordLabel: Yeni Şifre + NewPasswordConfirm: Şifreyi Onayla + NextButtonText: İleri + ResendButtonText: Kodu Tekrar Gönder + +InitUserDone: + Title: Kullanıcı Etkinleştirildi + Description: E-posta doğrulandı ve şifre başarıyla belirlendi + NextButtonText: İleri + CancelButtonText: İptal + +InviteUser: + Title: Kullanıcıyı Etkinleştir + Description: E-postanızı aşağıdaki kod ile doğrulayın ve şifrenizi belirleyin. + CodeLabel: Kod + NewPasswordLabel: Yeni Şifre + NewPasswordConfirm: Şifreyi Onayla + NextButtonText: İleri + ResendButtonText: Kodu Tekrar Gönder + +InitMFAPrompt: + Title: 2-Faktör Kurulumu + Description: 2-faktörlü kimlik doğrulama, kullanıcı hesabınız için ek güvenlik sağlar. Bu sayede hesabınıza yalnızca sizin erişiminiz olması sağlanır. + Provider0: Kimlik Doğrulayıcı Uygulama (örn. Google/Microsoft Authenticator, Authy) + Provider1: Cihaza bağımlı (örn. FaceID, Windows Hello, Parmak izi) + Provider3: OTP SMS + Provider4: OTP E-posta + NextButtonText: İleri + SkipButtonText: Atla + +InitMFAOTP: + Title: 2-Faktör Doğrulama + Description: 2-faktörünüzü oluşturun. Henüz yoksa bir kimlik doğrulayıcı uygulama indirin. + OTPDescription: Kodu kimlik doğrulayıcı uygulamanızla (örn. Google/Microsoft Authenticator, Authy) tarayın veya gizli anahtarı kopyalayın ve aşağıda oluşturulan kodu girin. + SecretLabel: Gizli Anahtar + CodeLabel: Kod + NextButtonText: İleri + CancelButtonText: İptal + +InitMFAOTPSMS: + Title: 2-Faktör Doğrulama + DescriptionPhone: 2-faktörünüzü oluşturun. Doğrulamak için telefon numaranızı girin. + DescriptionCode: 2-faktörünüzü oluşturun. Telefon numaranızı doğrulamak için aldığınız kodu girin. + PhoneLabel: Telefon + CodeLabel: Kod + EditButtonText: Düzenle + ResendButtonText: Kodu Tekrar Gönder + NextButtonText: İleri + +InitMFAU2F: + Title: Güvenlik Anahtarı Ekle + Description: Güvenlik anahtarı, telefonunuza yerleştirilmiş, Bluetooth kullanan veya bilgisayarınızın USB portuna doğrudan takılan bir doğrulama yöntemidir. + TokenNameLabel: Güvenlik anahtarı / cihaz adı + NotSupported: WebAuthN tarayıcınız tarafından desteklenmiyor. Lütfen güncel olduğundan emin olun veya farklı bir tarayıcı kullanın (örn. Chrome, Safari, Firefox) + RegisterTokenButtonText: Güvenlik anahtarı ekle + ErrorRetry: Tekrar deneyin, yeni bir challenge oluşturun veya farklı bir yöntem seçin. + +InitMFADone: + Title: 2-faktör Doğrulandı + Description: Harika! 2-faktörünüzü başarıyla kurdunuz ve hesabınızı çok daha güvenli hale getirdiniz. Faktör her girişte girilmek zorundadır. + NextButtonText: İleri + CancelButtonText: İptal + +MFAProvider: + Provider0: Kimlik Doğrulayıcı Uygulama (örn. Google/Microsoft Authenticator, Authy) + Provider1: Cihaza bağımlı (örn. FaceID, Windows Hello, Parmak izi) + Provider3: OTP SMS + Provider4: OTP E-posta + ChooseOther: veya başka bir seçenek seçin + +VerifyMFAOTP: + Title: 2-Faktör Doğrula + Description: İkinci faktörünüzü doğrulayın + CodeLabel: Kod + NextButtonText: İleri + +VerifyOTP: + Title: 2-Faktör Doğrula + Description: İkinci faktörünüzü doğrulayın + CodeLabel: Kod + ResendButtonText: Kodu Tekrar Gönder + NextButtonText: İleri + +VerifyMFAU2F: + Title: 2-Faktör Doğrulama + Description: Kayıtlı cihazınızla (örn. FaceID, Windows Hello, Parmak izi) 2-Faktörünüzü doğrulayın + NotSupported: WebAuthN tarayıcınız tarafından desteklenmiyor. En yeni sürümü kullandığınızdan emin olun veya desteklenen bir tarayıcıya geçin (Chrome, Safari, Firefox) + ErrorRetry: Tekrar deneyin, yeni bir istek oluşturun veya başka bir yöntem seçin. + ValidateTokenButtonText: 2-Faktör Doğrula + +Passwordless: + Title: Şifresiz Giriş + Description: FaceID, Windows Hello veya Parmak izi gibi cihazınızın sağladığı kimlik doğrulama yöntemleriyle giriş yapın. + NotSupported: WebAuthN tarayıcınız tarafından desteklenmiyor. Lütfen güncel olduğundan emin olun veya farklı bir tarayıcı kullanın (örn. Chrome, Safari, Firefox) + ErrorRetry: Tekrar deneyin, yeni bir challenge oluşturun veya farklı bir yöntem seçin. + LoginWithPwButtonText: Şifre ile giriş yap + ValidateTokenButtonText: Şifresiz giriş yap + +PasswordlessPrompt: + Title: Şifresiz Kurulum + Description: Şifresiz giriş kurmak ister misiniz? (FaceID, Windows Hello veya Parmak izi gibi cihazınızın kimlik doğrulama yöntemleri) + DescriptionInit: Şifresiz giriş kurmanız gerekiyor. Cihazınızı kaydetmek için size verilen bağlantıyı kullanın. + PasswordlessButtonText: Şifresiz devam et + NextButtonText: İleri + SkipButtonText: Atla + +PasswordlessRegistration: + Title: Şifresiz Kurulum + Description: Bir isim vererek (örn. BenimTelefonum, MacBook, vb.) ve ardından aşağıdaki 'Şifresiz kaydet' düğmesine tıklayarak kimlik doğrulamanızı ekleyin. + TokenNameLabel: Cihazın adı + NotSupported: WebAuthN tarayıcınız tarafından desteklenmiyor. Lütfen güncel olduğundan emin olun veya farklı bir tarayıcı kullanın (örn. Chrome, Safari, Firefox) + RegisterTokenButtonText: Şifresiz kaydet + ErrorRetry: Tekrar deneyin, yeni bir challenge oluşturun veya farklı bir yöntem seçin. + +PasswordlessRegistrationDone: + Title: Şifresiz Kurulum Tamamlandı + Description: Şifresiz cihaz başarıyla eklendi. + DescriptionClose: Artık bu pencereyi kapatabilirsiniz. + NextButtonText: İleri + CancelButtonText: İptal + +PasswordChange: + Title: Şifre Değiştir + Description: Şifrenizi değiştirin. Eski ve yeni şifrenizi girin. + ExpiredDescription: Şifrenizin süresi dolmuş ve değiştirilmesi gerekiyor. Eski ve yeni şifrenizi girin. + OldPasswordLabel: Eski Şifre + NewPasswordLabel: Yeni Şifre + NewPasswordConfirmLabel: Şifre onayı + CancelButtonText: İptal + NextButtonText: İleri + Footer: Alt Bilgi + +PasswordChangeDone: + Title: Şifre Değiştir + Description: Şifreniz başarıyla değiştirildi. + NextButtonText: İleri + +PasswordResetDone: + Title: Şifre Sıfırlama Bağlantısı Gönderildi + Description: Şifrenizi sıfırlamak için e-postanızı kontrol edin. + NextButtonText: İleri + +EmailVerification: + Title: E-Posta Doğrulama + Description: Adresinizi doğrulamak için size bir e-posta gönderdik. Lütfen aşağıdaki forma kodu girin. + CodeLabel: Kod + NextButtonText: İleri + ResendButtonText: Kodu Tekrar Gönder + +EmailVerificationDone: + Title: E-Posta Doğrulama + Description: E-posta adresiniz başarıyla doğrulandı. + NextButtonText: İleri + CancelButtonText: Cancel + LoginButtonText: Login + +RegisterOption: + Title: Kayıt Seçenekleri + Description: Nasıl kayıt olmak istediğinizi seçin + RegisterUsernamePasswordButtonText: Kullanıcı adı ve şifre ile + ExternalLoginDescription: veya harici bir kullanıcı ile kayıt olun + LoginButtonText: Giriş + +RegistrationUser: + Title: Kayıt + Description: Kullanıcı verilerinizi girin. E-posta adresiniz giriş adınız olarak kullanılacaktır. + DescriptionOrgRegister: Kullanıcı verilerinizi girin. + EmailLabel: E-Posta + UsernameLabel: Kullanıcı adı + FirstnameLabel: Ad + LastnameLabel: Soyadı + LanguageLabel: Dil + German: Deutsch + English: English + Italian: Italiano + French: Français + Chinese: 简体中文 + Polish: Polski + Japanese: 日本語 + Spanish: Español + Bulgarian: Български + Portuguese: Português + Macedonian: Македонски + Czech: Čeština + Russian: Русский + Dutch: Nederlands + Swedish: Svenska + Indonesian: Bahasa Indonesia + Hungarian: Magyar + Korean: 한국어 + Romanian: Română + Turkish: Türkçe + GenderLabel: Cinsiyet + Female: Kadın + Male: Erkek + Diverse: Çeşitli / X + PasswordLabel: Şifre + PasswordConfirmLabel: Şifre onayı + TosAndPrivacyLabel: Şartlar ve koşullar + TosConfirm: Kabul ediyorum + TosLinkText: Kullanım Şartları + PrivacyConfirm: Kabul ediyorum + PrivacyLinkText: gizlilik politikası + ExternalLogin: veya harici bir kullanıcı ile kayıt olun + BackButtonText: Giriş + NextButtonText: İleri + +ExternalRegistrationUserOverview: + Title: Harici Kullanıcı Kaydı + Description: Seçilen sağlayıcıdan kullanıcı bilgilerinizi aldık. Artık bunları değiştirebilir veya tamamlayabilirsiniz. + EmailLabel: E-Posta + UsernameLabel: Kullanıcı adı + FirstnameLabel: Ad + LastnameLabel: Soyadı + NicknameLabel: Takma ad + PhoneLabel: Telefon numarası + LanguageLabel: Dil + German: Deutsch + English: English + Italian: Italiano + French: Français + Chinese: 简体中文 + Polish: Polski + Japanese: 日本語 + Spanish: Español + Bulgarian: Български + Portuguese: Português + Macedonian: Македонски + Czech: Čeština + Russian: Русский + Dutch: Nederlands + Swedish: Svenska + Indonesian: Bahasa Indonesia + Hungarian: Magyar + Korean: 한국어 + Romanian: Română + Turkish: Türkçe + TosAndPrivacyLabel: Şartlar ve koşullar + TosConfirm: Kabul ediyorum + TosLinkText: Kullanım Şartları + PrivacyConfirm: Kabul ediyorum + PrivacyLinkText: gizlilik politikası + ExternalLogin: veya harici bir kullanıcı ile kayıt olun + BackButtonText: Geri + NextButtonText: Kaydet + +RegistrationOrg: + Title: Organizasyon Kaydı + Description: Organizasyon adınızı ve kullanıcı verilerinizi girin. + OrgNameLabel: Organizasyon adı + EmailLabel: E-Posta + UsernameLabel: Kullanıcı adı + FirstnameLabel: Ad + LastnameLabel: Soyadı + PasswordLabel: Şifre + PasswordConfirmLabel: Şifre onayı + TosAndPrivacyLabel: Şartlar ve koşullar + TosConfirm: Kabul ediyorum + TosLinkText: Kullanım Şartları + PrivacyConfirm: Kabul ediyorum + PrivacyLinkText: gizlilik politikası + SaveButtonText: Organizasyon oluştur + +LoginSuccess: + Title: Giriş Başarılı + AutoRedirectDescription: Uygulamanıza otomatik olarak yönlendirileceksiniz. Değilse, aşağıdaki düğmeye tıklayın. Daha sonra pencereyi kapatabilirsiniz. + RedirectedDescription: Artık bu pencereyi kapatabilirsiniz. + NextButtonText: İleri + +LogoutDone: + Title: Çıkış Yapıldı + Description: Başarıyla çıkış yaptınız. + LoginButtonText: Giriş + +LinkingUserPrompt: + Title: Mevcut Kullanıcı Bulundu + Description: "Mevcut hesabınızı bağlamak ister misiniz:" + LinkButtonText: Bağla + OtherButtonText: Diğer seçenekler + +LinkingUsersDone: + Title: Kullanıcı Bağlama + Description: Kullanıcı bağlandı. + CancelButtonText: İptal + NextButtonText: İleri + +ExternalNotFound: + Title: Harici Kullanıcı Bulunamadı + Description: Harici kullanıcı bulunamadı. Kullanıcınızı bağlamak mı yoksa yeni bir kullanıcı otomatik kaydetmek mi istiyorsunuz? + LinkButtonText: Bağla + AutoRegisterButtonText: Kayıt Ol + TosAndPrivacyLabel: Şartlar ve koşullar + TosConfirm: Kabul ediyorum + TosLinkText: Kullanım Şartları + PrivacyConfirm: Kabul ediyorum + PrivacyLinkText: gizlilik politikası + German: Deutsch + English: English + Italian: Italiano + French: Français + Chinese: 简体中文 + Polish: Polski + Japanese: 日本語 + Spanish: Español + Bulgarian: Български + Portuguese: Português + Macedonian: Македонски + Czech: Čeština + Russian: Русский + Dutch: Nederlands + Swedish: Svenska + Indonesian: Bahasa Indonesia + Hungarian: Magyar + Korean: 한국어 + Romanian: Română + Turkish: Türkçe +DeviceAuth: + Title: Cihaz Yetkilendirme + UserCode: + Label: Kullanıcı Kodu + Description: Cihazda sunulan kullanıcı kodunu girin. + ButtonNext: İleri + Action: + Description: Cihaz erişimi verin. + GrantDevice: cihaza yetki vermek üzeresiniz + AccessToScopes: aşağıdaki kapsam alanlarına erişim + Button: + Allow: İzin Ver + Deny: Reddet + Done: + Description: Tamamlandı. + Approved: Cihaz yetkilendirmesi onaylandı. Artık cihaza dönebilirsiniz. + Denied: Cihaz yetkilendirmesi reddedildi. Artık cihaza dönebilirsiniz. + +Footer: + PoweredBy: Teknoloji Desteği + Tos: Kullanım Şartları + PrivacyPolicy: Gizlilik politikası + Help: Yardım + SupportEmail: Destek E-posta + +SignIn: "{{.Provider}} ile giriş yap" + +Errors: + Internal: Dahili bir hata oluştu + AuthRequest: + NotFound: Kimlik doğrulama isteği bulunamadı + UserAgentNotCorresponding: User Agent uyuşmuyor + UserAgentNotFound: User Agent ID bulunamadı + TokenNotFound: Token bulunamadı + RequestTypeNotSupported: İstek türü desteklenmiyor + MissingParameters: Gerekli parametreler eksik + User: + NotFound: Kullanıcı bulunamadı + AlreadyExists: Kullanıcı zaten mevcut + Inactive: Kullanıcı aktif değil + NotFoundOnOrg: Kullanıcı seçilen organizasyonda bulunamadı + NotAllowedOrg: Kullanıcı gerekli organizasyonun üyesi değil + NotMatchingUserID: Kullanıcı ve kimlik doğrulama isteğindeki kullanıcı uyuşmuyor + UserIDMissing: Kullanıcı ID'si boş + Invalid: Geçersiz kullanıcı verisi + DomainNotAllowedAsUsername: Alan adı zaten rezerve edilmiş ve kullanılamaz + NotAllowedToLink: Kullanıcının harici giriş sağlayıcısı ile bağlantı kurma izni yok + Profile: + NotFound: Profil bulunamadı + NotChanged: Profil değişmedi + Empty: Profil boş + FirstNameEmpty: Profildeki ad boş + LastNameEmpty: Profildeki soyadı boş + IDMissing: Profil ID'si eksik + Email: + NotFound: E-posta bulunamadı + Invalid: E-posta geçersiz + AlreadyVerified: E-posta zaten doğrulanmış + NotChanged: E-posta değişmedi + Empty: E-posta boş + IDMissing: E-posta ID'si eksik + Phone: + NotFound: Telefon bulunamadı + Invalid: Telefon geçersiz + AlreadyVerified: Telefon zaten doğrulanmış + Empty: Telefon boş + NotChanged: Telefon değişmedi + Address: + NotFound: Adres bulunamadı + NotChanged: Adres değişmedi + Username: + AlreadyExists: Kullanıcı adı zaten alınmış + Reserved: Kullanıcı adı zaten alınmış + Empty: Kullanıcı adı boş + Password: + ConfirmationWrong: Şifre onayı yanlış + Empty: Şifre boş + Invalid: Şifre geçersiz + InvalidAndLocked: Şifre geçersiz ve kullanıcı kilitli, yöneticinize başvurun. + NotChanged: Yeni şifre mevcut şifrenizle aynı olamaz + UsernameOrPassword: + Invalid: Kullanıcı adı veya Şifre geçersiz + PasswordComplexityPolicy: + NotFound: Şifre politikası bulunamadı + MinLength: Şifre çok kısa + HasLower: Şifre küçük harf içermeli + HasUpper: Şifre büyük harf içermeli + HasNumber: Şifre sayı içermeli + HasSymbol: Şifre sembol içermeli + Code: + Expired: Kod süresi doldu + Invalid: Kod geçersiz + Empty: Kod boş + CryptoCodeNil: Kripto kodu boş + NotFound: Kod bulunamadı + GeneratorAlgNotSupported: Desteklenmeyen oluşturucu algoritması + EmailVerify: + UserIDEmpty: Kullanıcı ID'si boş + ExternalData: + CouldNotRead: Harici veri doğru okunamadı + MFA: + NoProviders: Kullanılabilir çok faktörlü kimlik doğrulama sağlayıcısı yok + OTP: + AlreadyReady: Çok faktörlü OTP (Tek Seferlik Şifre) zaten ayarlanmış + NotExisting: Çok faktörlü OTP (Tek Seferlik Şifre) mevcut değil + InvalidCode: Geçersiz kod + NotReady: Çok faktörlü OTP (Tek Seferlik Şifre) hazır değil + Locked: Kullanıcı kilitli + SomethingWentWrong: Bir şeyler yanlış gitti + NotActive: Kullanıcı aktif değil + ExternalIDP: + IDPTypeNotImplemented: IDP Türü uygulanmamış + NotAllowed: Harici Giriş Sağlayıcısına izin verilmiyor + IDPConfigIDEmpty: Kimlik Sağlayıcı ID'si boş + ExternalUserIDEmpty: Harici Kullanıcı ID'si boş + UserDisplayNameEmpty: Kullanıcı Görünen Adı boş + NoExternalUserData: Harici Kullanıcı Verisi alınmadı + CreationNotAllowed: Bu sağlayıcıda yeni kullanıcı oluşturmaya izin verilmiyor + LinkingNotAllowed: Bu sağlayıcıda kullanıcı bağlamaya izin verilmiyor + NoOptionAllowed: Bu sağlayıcıda ne oluşturmaya ne de bağlamaya izin verilmiyor. Lütfen yöneticinizle iletişime geçin. + LoginFailedSwitchLocal: | + Harici IDP'de giriş başarısız oldu. Yerel girişe geri dönülüyor. + + Hata detayları: {{.Details}} + GrantRequired: Giriş mümkün değil. Kullanıcının uygulamada en az bir yetkisi olması gerekiyor. Lütfen yöneticinizle iletişime geçin. + ProjectRequired: Giriş mümkün değil. Kullanıcının organizasyonuna proje için yetki verilmiş olması gerekiyor. Lütfen yöneticinizle iletişime geçin. + IdentityProvider: + InvalidConfig: Kimlik Sağlayıcı yapılandırması geçersiz + IAM: + LockoutPolicy: + NotExisting: Kilitleme Politikası mevcut değil + Org: + LoginPolicy: + RegistrationNotAllowed: Kayıt olmasına izin verilmiyor + DeviceAuth: + NotExisting: Kullanıcı Kodu mevcut değil + +optional: (isteğe bağlı) diff --git a/internal/api/ui/login/static/i18n/zh.yaml b/internal/api/ui/login/static/i18n/zh.yaml index 4ba5904700..453bcec107 100644 --- a/internal/api/ui/login/static/i18n/zh.yaml +++ b/internal/api/ui/login/static/i18n/zh.yaml @@ -266,6 +266,7 @@ RegistrationUser: Hungarian: Magyar Korean: 한국어 Romanian: Română + Turkish: Türkçe GenderLabel: 性别 Female: 女性 Male: 男性 @@ -310,6 +311,7 @@ ExternalRegistrationUserOverview: Hungarian: Magyar Korean: 한국어 Romanian: Română + Turkish: Türkçe TosAndPrivacyLabel: 条款和条款 TosConfirm: 我接受 TosLinkText: 服务条款 @@ -388,6 +390,7 @@ ExternalNotFound: Hungarian: Magyar Korean: 한국어 Romanian: Română + Turkish: Türkçe DeviceAuth: Title: 设备授权 UserCode: diff --git a/internal/api/ui/login/static/templates/external_not_found_option.html b/internal/api/ui/login/static/templates/external_not_found_option.html index dca4bd5edc..688e917b3d 100644 --- a/internal/api/ui/login/static/templates/external_not_found_option.html +++ b/internal/api/ui/login/static/templates/external_not_found_option.html @@ -102,6 +102,8 @@ +
diff --git a/internal/auth/repository/eventsourcing/eventstore/auth_request.go b/internal/auth/repository/eventsourcing/eventstore/auth_request.go index 984a1e7145..bf0609673b 100644 --- a/internal/auth/repository/eventsourcing/eventstore/auth_request.go +++ b/internal/auth/repository/eventsourcing/eventstore/auth_request.go @@ -125,7 +125,7 @@ type userGrantProvider interface { type projectProvider interface { ProjectByClientID(context.Context, string) (*query.Project, error) - SearchProjectGrants(ctx context.Context, queries *query.ProjectGrantSearchQueries) (projects *query.ProjectGrants, err error) + SearchProjectGrants(ctx context.Context, queries *query.ProjectGrantSearchQueries, permissionCheck domain.PermissionCheck) (projects *query.ProjectGrants, err error) } type applicationProvider interface { @@ -1845,7 +1845,7 @@ func projectRequired(ctx context.Context, request *domain.AuthRequest, projectPr if err != nil { return false, err } - grants, err := projectProvider.SearchProjectGrants(ctx, &query.ProjectGrantSearchQueries{Queries: []query.SearchQuery{projectID, grantedOrg}}) + grants, err := projectProvider.SearchProjectGrants(ctx, &query.ProjectGrantSearchQueries{Queries: []query.SearchQuery{projectID, grantedOrg}}, nil) if err != nil { return false, err } diff --git a/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go b/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go index 7d71ddecd9..78edd2a7e4 100644 --- a/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go +++ b/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go @@ -286,7 +286,7 @@ func (m *mockProject) ProjectByClientID(ctx context.Context, s string) (*query.P return &query.Project{ResourceOwner: m.resourceOwner, HasProjectCheck: m.projectCheck}, nil } -func (m *mockProject) SearchProjectGrants(ctx context.Context, queries *query.ProjectGrantSearchQueries) (*query.ProjectGrants, error) { +func (m *mockProject) SearchProjectGrants(ctx context.Context, queries *query.ProjectGrantSearchQueries, permissionCheck domain.PermissionCheck) (*query.ProjectGrants, error) { if m.hasProject { mockProjectGrant := new(query.ProjectGrant) return &query.ProjectGrants{ProjectGrants: []*query.ProjectGrant{mockProjectGrant}}, nil diff --git a/internal/auth/repository/eventsourcing/repository.go b/internal/auth/repository/eventsourcing/repository.go index 16bee6e7a4..81dc8abd5c 100644 --- a/internal/auth/repository/eventsourcing/repository.go +++ b/internal/auth/repository/eventsourcing/repository.go @@ -125,7 +125,7 @@ func (q queryViewWrapper) UserGrantsByProjectAndUserID(ctx context.Context, proj return nil, err } queries := &query.UserGrantsQueries{Queries: []query.SearchQuery{userGrantUserID, userGrantProjectID, activeQuery}} - grants, err := q.Queries.UserGrants(ctx, queries, true) + grants, err := q.Queries.UserGrants(ctx, queries, true, nil) if err != nil { return nil, err } diff --git a/internal/authz/repository/eventsourcing/eventstore/token_verifier.go b/internal/authz/repository/eventsourcing/eventstore/token_verifier.go index b707631c22..d6c14afea3 100644 --- a/internal/authz/repository/eventsourcing/eventstore/token_verifier.go +++ b/internal/authz/repository/eventsourcing/eventstore/token_verifier.go @@ -4,7 +4,6 @@ import ( "context" "encoding/base64" "fmt" - "slices" "strings" "time" @@ -329,18 +328,10 @@ type openIDKeySet struct { // VerifySignature implements the oidc.KeySet interface // providing an implementation for the keys retrieved directly from Queries func (o *openIDKeySet) VerifySignature(ctx context.Context, jws *jose.JSONWebSignature) (payload []byte, err error) { - keySet := new(jose.JSONWebKeySet) - if authz.GetFeatures(ctx).WebKey { - keySet, err = o.Queries.GetWebKeySet(ctx) - if err != nil { - return nil, err - } - } - legacyKeySet, err := o.Queries.ActivePublicKeys(ctx, time.Now()) + keySet, err := o.Queries.GetWebKeySet(ctx) if err != nil { - return nil, fmt.Errorf("error fetching keys: %w", err) + return nil, err } - appendPublicKeysToWebKeySet(keySet, legacyKeySet) keyID, alg := oidc.GetKeyIDAndAlg(jws) key, err := oidc.FindMatchingKey(keyID, oidc.KeyUseSignature, alg, keySet.Keys...) if err != nil { @@ -348,19 +339,3 @@ func (o *openIDKeySet) VerifySignature(ctx context.Context, jws *jose.JSONWebSig } return jws.Verify(&key) } - -func appendPublicKeysToWebKeySet(keyset *jose.JSONWebKeySet, pubkeys *query.PublicKeys) { - if pubkeys == nil || len(pubkeys.Keys) == 0 { - return - } - keyset.Keys = slices.Grow(keyset.Keys, len(pubkeys.Keys)) - - for _, key := range pubkeys.Keys { - keyset.Keys = append(keyset.Keys, jose.JSONWebKey{ - Key: key.Key(), - KeyID: key.ID(), - Algorithm: key.Algorithm(), - Use: key.Use().String(), - }) - } -} diff --git a/internal/authz/repository/eventsourcing/view/application.go b/internal/authz/repository/eventsourcing/view/application.go index 8db8ec8e39..7fa920bcfe 100644 --- a/internal/authz/repository/eventsourcing/view/application.go +++ b/internal/authz/repository/eventsourcing/view/application.go @@ -32,7 +32,7 @@ func (v *View) ApplicationByProjecIDAndAppName(ctx context.Context, projectID, a }, } - apps, err := v.Query.SearchApps(ctx, queries, false) + apps, err := v.Query.SearchApps(ctx, queries, nil) if err != nil { return nil, err } diff --git a/internal/command/hosted_login_translation.go b/internal/command/hosted_login_translation.go new file mode 100644 index 0000000000..024ab6bdad --- /dev/null +++ b/internal/command/hosted_login_translation.go @@ -0,0 +1,73 @@ +package command + +import ( + "context" + "crypto/md5" + "encoding/hex" + "fmt" + + "golang.org/x/text/language" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/repository/instance" + "github.com/zitadel/zitadel/internal/repository/org" + "github.com/zitadel/zitadel/internal/telemetry/tracing" + "github.com/zitadel/zitadel/internal/zerrors" + "github.com/zitadel/zitadel/pkg/grpc/settings/v2" +) + +func (c *Commands) SetHostedLoginTranslation(ctx context.Context, req *settings.SetHostedLoginTranslationRequest) (res *settings.SetHostedLoginTranslationResponse, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + var agg eventstore.Aggregate + switch t := req.GetLevel().(type) { + case *settings.SetHostedLoginTranslationRequest_Instance: + agg = instance.NewAggregate(authz.GetInstance(ctx).InstanceID()).Aggregate + case *settings.SetHostedLoginTranslationRequest_OrganizationId: + agg = org.NewAggregate(t.OrganizationId).Aggregate + default: + return nil, zerrors.ThrowInvalidArgument(nil, "COMMA-YB6Sri", "Errors.Arguments.Level.Invalid") + } + + lang, err := language.Parse(req.GetLocale()) + if err != nil || lang.IsRoot() { + return nil, zerrors.ThrowInvalidArgument(nil, "COMMA-xmjATA", "Errors.Arguments.Locale.Invalid") + } + + commands, wm, err := c.setTranslationEvents(ctx, agg, lang, req.GetTranslations().AsMap()) + if err != nil { + return nil, err + } + + pushedEvents, err := c.eventstore.Push(ctx, commands...) + if err != nil { + return nil, zerrors.ThrowInternal(err, "COMMA-i8nqFl", "Errors.Internal") + } + + err = AppendAndReduce(wm, pushedEvents...) + if err != nil { + return nil, err + } + + etag := md5.Sum(fmt.Append(nil, wm.Translation)) + return &settings.SetHostedLoginTranslationResponse{ + Etag: hex.EncodeToString(etag[:]), + }, nil +} + +func (c *Commands) setTranslationEvents(ctx context.Context, agg eventstore.Aggregate, lang language.Tag, translations map[string]any) ([]eventstore.Command, *HostedLoginTranslationWriteModel, error) { + wm := NewHostedLoginTranslationWriteModel(agg.ID) + events := []eventstore.Command{} + switch agg.Type { + case instance.AggregateType: + events = append(events, instance.NewHostedLoginTranslationSetEvent(ctx, &agg, translations, lang)) + case org.AggregateType: + events = append(events, org.NewHostedLoginTranslationSetEvent(ctx, &agg, translations, lang)) + default: + return nil, nil, zerrors.ThrowInvalidArgument(nil, "COMMA-0aw7In", "Errors.Arguments.LevelType.Invalid") + } + + return events, wm, nil +} diff --git a/internal/command/hosted_login_translation_model.go b/internal/command/hosted_login_translation_model.go new file mode 100644 index 0000000000..16bc42c541 --- /dev/null +++ b/internal/command/hosted_login_translation_model.go @@ -0,0 +1,45 @@ +package command + +import ( + "golang.org/x/text/language" + + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/repository/instance" + "github.com/zitadel/zitadel/internal/repository/org" +) + +type HostedLoginTranslationWriteModel struct { + eventstore.WriteModel + Language language.Tag + Translation map[string]any + Level string + LevelID string +} + +func NewHostedLoginTranslationWriteModel(resourceID string) *HostedLoginTranslationWriteModel { + return &HostedLoginTranslationWriteModel{ + WriteModel: eventstore.WriteModel{ + AggregateID: resourceID, + ResourceOwner: resourceID, + }, + } +} + +func (wm *HostedLoginTranslationWriteModel) Reduce() error { + for _, event := range wm.Events { + switch e := event.(type) { + case *org.HostedLoginTranslationSetEvent: + wm.Language = e.Language + wm.Translation = e.Translation + wm.Level = e.Level + wm.LevelID = e.Aggregate().ID + case *instance.HostedLoginTranslationSetEvent: + wm.Language = e.Language + wm.Translation = e.Translation + wm.Level = e.Level + wm.LevelID = e.Aggregate().ID + } + } + + return wm.WriteModel.Reduce() +} diff --git a/internal/command/hosted_login_translation_test.go b/internal/command/hosted_login_translation_test.go new file mode 100644 index 0000000000..a5f0941711 --- /dev/null +++ b/internal/command/hosted_login_translation_test.go @@ -0,0 +1,211 @@ +package command + +import ( + "context" + "crypto/md5" + "encoding/hex" + "errors" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/text/language" + "google.golang.org/protobuf/types/known/structpb" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/api/service" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/repository/instance" + "github.com/zitadel/zitadel/internal/repository/org" + "github.com/zitadel/zitadel/internal/zerrors" + "github.com/zitadel/zitadel/pkg/grpc/settings/v2" +) + +func TestSetTranslationEvents(t *testing.T) { + t.Parallel() + + testCtx := authz.SetCtxData(context.Background(), authz.CtxData{UserID: "test-user"}) + testCtx = service.WithService(testCtx, "test-service") + + tt := []struct { + testName string + + inputAggregate eventstore.Aggregate + inputLanguage language.Tag + inputTranslations map[string]any + + expectedCommands []eventstore.Command + expectedWriteModel *HostedLoginTranslationWriteModel + expectedError error + }{ + { + testName: "when aggregate type is instance should return matching write model and instance.hosted_login_translation_set event", + inputAggregate: eventstore.Aggregate{ID: "123", Type: instance.AggregateType}, + inputLanguage: language.MustParse("en-US"), + inputTranslations: map[string]any{"test": "translation"}, + expectedCommands: []eventstore.Command{ + instance.NewHostedLoginTranslationSetEvent(testCtx, &eventstore.Aggregate{ID: "123", Type: instance.AggregateType}, map[string]any{"test": "translation"}, language.MustParse("en-US")), + }, + expectedWriteModel: &HostedLoginTranslationWriteModel{ + WriteModel: eventstore.WriteModel{AggregateID: "123", ResourceOwner: "123"}, + }, + }, + { + testName: "when aggregate type is org should return matching write model and org.hosted_login_translation_set event", + inputAggregate: eventstore.Aggregate{ID: "123", Type: org.AggregateType}, + inputLanguage: language.MustParse("en-GB"), + inputTranslations: map[string]any{"test": "translation"}, + expectedCommands: []eventstore.Command{ + org.NewHostedLoginTranslationSetEvent(testCtx, &eventstore.Aggregate{ID: "123", Type: org.AggregateType}, map[string]any{"test": "translation"}, language.MustParse("en-GB")), + }, + expectedWriteModel: &HostedLoginTranslationWriteModel{ + WriteModel: eventstore.WriteModel{AggregateID: "123", ResourceOwner: "123"}, + }, + }, + { + testName: "when aggregate type is neither org nor instance should return invalid argument error", + inputAggregate: eventstore.Aggregate{ID: "123"}, + inputLanguage: language.MustParse("en-US"), + inputTranslations: map[string]any{"test": "translation"}, + expectedError: zerrors.ThrowInvalidArgument(nil, "COMMA-0aw7In", "Errors.Arguments.LevelType.Invalid"), + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + + // Given + c := Commands{} + + // When + events, writeModel, err := c.setTranslationEvents(testCtx, tc.inputAggregate, tc.inputLanguage, tc.inputTranslations) + + // Verify + require.Equal(t, tc.expectedError, err) + assert.Equal(t, tc.expectedWriteModel, writeModel) + + require.Len(t, events, len(tc.expectedCommands)) + assert.ElementsMatch(t, tc.expectedCommands, events) + }) + } +} + +func TestSetHostedLoginTranslation(t *testing.T) { + t.Parallel() + + testCtx := authz.SetCtxData(context.Background(), authz.CtxData{UserID: "test-user"}) + testCtx = service.WithService(testCtx, "test-service") + testCtx = authz.WithInstanceID(testCtx, "instance-id") + + testTranslation := map[string]any{"test": "translation", "translation": "2"} + protoTranslation, err := structpb.NewStruct(testTranslation) + require.NoError(t, err) + + hashTestTranslation := md5.Sum(fmt.Append(nil, testTranslation)) + require.NotEmpty(t, hashTestTranslation) + + tt := []struct { + testName string + + mockPush func(*testing.T) *eventstore.Eventstore + + inputReq *settings.SetHostedLoginTranslationRequest + + expectedError error + expectedResult *settings.SetHostedLoginTranslationResponse + }{ + { + testName: "when locale is malformed should return invalid argument error", + mockPush: func(t *testing.T) *eventstore.Eventstore { return &eventstore.Eventstore{} }, + inputReq: &settings.SetHostedLoginTranslationRequest{ + Level: &settings.SetHostedLoginTranslationRequest_Instance{}, + Locale: "123", + }, + + expectedError: zerrors.ThrowInvalidArgument(nil, "COMMA-xmjATA", "Errors.Arguments.Locale.Invalid"), + }, + { + testName: "when locale is unknown should return invalid argument error", + mockPush: func(t *testing.T) *eventstore.Eventstore { return &eventstore.Eventstore{} }, + inputReq: &settings.SetHostedLoginTranslationRequest{ + Level: &settings.SetHostedLoginTranslationRequest_Instance{}, + Locale: "root", + }, + + expectedError: zerrors.ThrowInvalidArgument(nil, "COMMA-xmjATA", "Errors.Arguments.Locale.Invalid"), + }, + { + testName: "when event pushing fails should return internal error", + + mockPush: expectEventstore(expectPushFailed( + errors.New("mock push failed"), + instance.NewHostedLoginTranslationSetEvent( + testCtx, &eventstore.Aggregate{ + ID: "instance-id", + Type: instance.AggregateType, + ResourceOwner: "instance-id", + InstanceID: "instance-id", + Version: instance.AggregateVersion, + }, + testTranslation, + language.MustParse("it-CH"), + ), + )), + + inputReq: &settings.SetHostedLoginTranslationRequest{ + Level: &settings.SetHostedLoginTranslationRequest_Instance{}, + Locale: "it-CH", + Translations: protoTranslation, + }, + + expectedError: zerrors.ThrowInternal(errors.New("mock push failed"), "COMMA-i8nqFl", "Errors.Internal"), + }, + { + testName: "when request is valid should return expected response", + + mockPush: expectEventstore(expectPush( + org.NewHostedLoginTranslationSetEvent( + testCtx, &eventstore.Aggregate{ + ID: "org-id", + Type: org.AggregateType, + ResourceOwner: "org-id", + InstanceID: "", + Version: org.AggregateVersion, + }, + testTranslation, + language.MustParse("it-CH"), + ), + )), + + inputReq: &settings.SetHostedLoginTranslationRequest{ + Level: &settings.SetHostedLoginTranslationRequest_OrganizationId{OrganizationId: "org-id"}, + Locale: "it-CH", + Translations: protoTranslation, + }, + + expectedResult: &settings.SetHostedLoginTranslationResponse{ + Etag: hex.EncodeToString(hashTestTranslation[:]), + }, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + + // Given + c := Commands{ + eventstore: tc.mockPush(t), + } + + // When + res, err := c.SetHostedLoginTranslation(testCtx, tc.inputReq) + + // Verify + require.Equal(t, tc.expectedError, err) + assert.Equal(t, tc.expectedResult, res) + }) + } +} diff --git a/internal/command/idp_intent_test.go b/internal/command/idp_intent_test.go index 6cf835f521..e0f4e2ffdb 100644 --- a/internal/command/idp_intent_test.go +++ b/internal/command/idp_intent_test.go @@ -432,9 +432,8 @@ func TestCommands_AuthFromProvider(t *testing.T) { samlRootURL string } type res struct { - content string - redirect bool - err error + auth idp.Auth + err error } tests := []struct { name string @@ -579,8 +578,7 @@ func TestCommands_AuthFromProvider(t *testing.T) { callbackURL: "url", }, res{ - content: "auth?client_id=clientID&prompt=select_account&redirect_uri=url&response_type=code&state=id", - redirect: true, + auth: &idp.RedirectAuth{RedirectURL: "auth?client_id=clientID&prompt=select_account&redirect_uri=url&response_type=code&state=id"}, }, }, { @@ -671,8 +669,7 @@ func TestCommands_AuthFromProvider(t *testing.T) { callbackURL: "url", }, res{ - content: "https://login.microsoftonline.com/tenant/oauth2/v2.0/authorize?client_id=clientID&prompt=select_account&redirect_uri=url&response_type=code&scope=openid+profile+User.Read&state=id", - redirect: true, + auth: &idp.RedirectAuth{RedirectURL: "https://login.microsoftonline.com/tenant/oauth2/v2.0/authorize?client_id=clientID&prompt=select_account&redirect_uri=url&response_type=code&scope=openid+profile+User.Read&state=id"}, }, }, } @@ -686,13 +683,12 @@ func TestCommands_AuthFromProvider(t *testing.T) { _, session, err := c.AuthFromProvider(tt.args.ctx, tt.args.idpID, tt.args.callbackURL, tt.args.samlRootURL) require.ErrorIs(t, err, tt.res.err) - var content string - var redirect bool + var got idp.Auth if err == nil { - content, redirect = session.GetAuth(tt.args.ctx) + got, err = session.GetAuth(tt.args.ctx) + assert.Equal(t, tt.res.auth, got) + assert.NoError(t, err) } - assert.Equal(t, tt.res.redirect, redirect) - assert.Equal(t, tt.res.content, content) }) } } @@ -811,6 +807,97 @@ func TestCommands_AuthFromProvider_SAML(t *testing.T) { }, }, }, + { + "saml post auth", + fields{ + secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + eventstore: expectEventstore( + expectFilter( + eventFromEventPusherWithInstanceID( + "instance", + instance.NewSAMLIDPAddedEvent(context.Background(), &instance.NewAggregate("instance").Aggregate, + "idp", + "name", + []byte("\n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n \n urn:oasis:names:tc:SAML:2.0:nameid-format:transient\n \n \n \n"), + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("key"), + }, + []byte("certificate"), + "", + false, + gu.Ptr(domain.SAMLNameIDFormatUnspecified), + "", + false, + rep_idp.Options{}, + )), + ), + expectFilter( + eventFromEventPusherWithInstanceID( + "instance", + instance.NewSAMLIDPAddedEvent(context.Background(), &instance.NewAggregate("instance").Aggregate, + "idp", + "name", + []byte("\n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n \n urn:oasis:names:tc:SAML:2.0:nameid-format:transient\n \n \n \n"), + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("-----BEGIN RSA PRIVATE KEY-----\nMIIEogIBAAKCAQEAxHd087RoEm9ywVWZ/H+tDWxQsmVvhfRz4jAq/RfU+OWXNH4J\njMMSHdFs0Q+WP98nNXRyc7fgbMb8NdmlB2yD4qLYapN5SDaBc5dh/3EnyFt53oSs\njTlKnQUPAeJr2qh/NY046CfyUyQMM4JR5OiQFo4TssfWnqdcgamGt0AEnk2lvbMZ\nKQdAqNS9lDzYbjMGavEQPTZE35mFXFQXjaooZXq+TIa7hbaq7/idH7cHNbLcPLgj\nfPQA8q+DYvnvhXlmq0LPQZH3Oiixf+SF2vRwrBzT2mqGD2OiOkUmhuPwyqEiiBHt\nfxklRtRU6WfLa1Gcb1PsV0uoBGpV3KybIl/GlwIDAQABAoIBAEQjDduLgOCL6Gem\n0X3hpdnW6/HC/jed/Sa//9jBECq2LYeWAqff64ON40hqOHi0YvvGA/+gEOSI6mWe\nsv5tIxxRz+6+cLybsq+tG96kluCE4TJMHy/nY7orS/YiWbd+4odnEApr+D3fbZ/b\nnZ1fDsHTyn8hkYx6jLmnWsJpIHDp7zxD76y7k2Bbg6DZrCGiVxngiLJk23dvz79W\np03lHLM7XE92aFwXQmhfxHGxrbuoB/9eY4ai5IHp36H4fw0vL6NXdNQAo/bhe0p9\nAYB7y0ZumF8Hg0Z/BmMeEzLy6HrYB+VE8cO93pNjhSyH+p2yDB/BlUyTiRLQAoM0\nVTmOZXECgYEA7NGlzpKNhyQEJihVqt0MW0LhKIO/xbBn+XgYfX6GpqPa/ucnMx5/\nVezpl3gK8IU4wPUhAyXXAHJiqNBcEeyxrw0MXLujDVMJgYaLysCLJdvMVgoY08mS\nK5IQivpbozpf4+0y3mOnA+Sy1kbfxv2X8xiWLODRQW3f3q/xoklwOR8CgYEA1GEe\nfaibOFTQAYcIVj77KXtBfYZsX3EGAyfAN9O7cKHq5oaxVstwnF47WxpuVtoKZxCZ\nbNm9D5WvQ9b+Ztpioe42tzwE7Bff/Osj868GcDdRPK7nFlh9N2yVn/D514dOYVwR\n4MBr1KrJzgRWt4QqS4H+to1GzudDTSNlG7gnK4kCgYBUi6AbOHzoYzZL/RhgcJwp\ntJ23nhmH1Su5h2OO4e3mbhcP66w19sxU+8iFN+kH5zfUw26utgKk+TE5vXExQQRK\nT2k7bg2PAzcgk80ybD0BHhA8I0yrx4m0nmfjhe/TPVLgh10iwgbtP+eM0i6v1vc5\nZWyvxu9N4ZEL6lpkqr0y1wKBgG/NAIQd8jhhTW7Aav8cAJQBsqQl038avJOEpYe+\nCnpsgoAAf/K0/f8TDCQVceh+t+MxtdK7fO9rWOxZjWsPo8Si5mLnUaAHoX4/OpnZ\nlYYVWMqdOEFnK+O1Yb7k2GFBdV2DXlX2dc1qavntBsls5ecB89id3pyk2aUN8Pf6\npYQhAoGAMGtrHFely9wyaxI0RTCyfmJbWZHGVGkv6ELK8wneJjdjl82XOBUGCg5q\naRCrTZ3dPitKwrUa6ibJCIFCIziiriBmjDvTHzkMvoJEap2TVxYNDR6IfINVsQ57\nlOsiC4A2uGq4Lbfld+gjoplJ5GX6qXtTgZ6m7eo0y7U6zm2tkN0=\n-----END RSA PRIVATE KEY-----\n"), + }, []byte("-----BEGIN CERTIFICATE-----\nMIIC2zCCAcOgAwIBAgIIAy/jm1gAAdEwDQYJKoZIhvcNAQELBQAwEjEQMA4GA1UE\nChMHWklUQURFTDAeFw0yMzA4MzAwNzExMTVaFw0yNDA4MjkwNzExMTVaMBIxEDAO\nBgNVBAoTB1pJVEFERUwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDE\nd3TztGgSb3LBVZn8f60NbFCyZW+F9HPiMCr9F9T45Zc0fgmMwxId0WzRD5Y/3yc1\ndHJzt+Bsxvw12aUHbIPiothqk3lINoFzl2H/cSfIW3nehKyNOUqdBQ8B4mvaqH81\njTjoJ/JTJAwzglHk6JAWjhOyx9aep1yBqYa3QASeTaW9sxkpB0Co1L2UPNhuMwZq\n8RA9NkTfmYVcVBeNqihler5MhruFtqrv+J0ftwc1stw8uCN89ADyr4Ni+e+FeWar\nQs9Bkfc6KLF/5IXa9HCsHNPaaoYPY6I6RSaG4/DKoSKIEe1/GSVG1FTpZ8trUZxv\nU+xXS6gEalXcrJsiX8aXAgMBAAGjNTAzMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUE\nDDAKBggrBgEFBQcDATAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4IBAQCx\n/dRNIj0N/16zJhZR/ahkc2AkvDXYxyr4JRT5wK9GQDNl/oaX3debRuSi/tfaXFIX\naJA6PxM4J49ZaiEpLrKfxMz5kAhjKchCBEMcH3mGt+iNZH7EOyTvHjpGrP2OZrsh\nO17yrvN3HuQxIU6roJlqtZz2iAADsoPtwOO4D7hupm9XTMkSnAmlMWOo/q46Jz89\n1sMxB+dXmH/zV0wgwh0omZfLV0u89mvdq269VhcjNBpBYSnN1ccqYWd5iwziob3I\nvaavGHGfkbvRUn/tKftYuTK30q03R+e9YbmlWZ0v695owh2e/apCzowQsCKfSVC8\nOxVyt5XkHq1tWwVyBmFp\n-----END CERTIFICATE-----\n"), + "", + false, + gu.Ptr(domain.SAMLNameIDFormatUnspecified), + "", + false, + rep_idp.Options{}, + )), + ), + expectFilter( + eventFromEventPusherWithInstanceID( + "instance", + func() eventstore.Command { + success, _ := url.Parse("https://success.url") + failure, _ := url.Parse("https://failure.url") + return idpintent.NewStartedEvent( + context.Background(), + &idpintent.NewAggregate("id", "instance").Aggregate, + success, + failure, + "idp", + nil, + ) + }(), + ), + ), + expectRandomPush( + []eventstore.Command{ + idpintent.NewSAMLRequestEvent( + context.Background(), + &idpintent.NewAggregate("id", "instance").Aggregate, + "request", + ), + }, + ), + ), + idGenerator: mock.ExpectID(t, "id"), + }, + args{ + ctx: authz.SetCtxData(context.Background(), authz.CtxData{OrgID: "ro"}), + idpID: "idp", + callbackURL: "url", + samlRootURL: "samlurl", + }, + res{ + url: "http://localhost:8000/sso", + values: map[string]string{ + "SAMLRequest": "", // generated IDs so not assertable + "RelayState": "id", + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -822,16 +909,30 @@ func TestCommands_AuthFromProvider_SAML(t *testing.T) { _, session, err := c.AuthFromProvider(tt.args.ctx, tt.args.idpID, tt.args.callbackURL, tt.args.samlRootURL) require.ErrorIs(t, err, tt.res.err) - content, _ := session.GetAuth(tt.args.ctx) - authURL, err := url.Parse(content) + auth, err := session.GetAuth(tt.args.ctx) require.NoError(t, err) + var authURL *url.URL + authFields := make(map[string]string) + + switch a := auth.(type) { + case *idp.RedirectAuth: + authURL, err = url.Parse(a.RedirectURL) + for key, values := range authURL.Query() { + authFields[key] = values[0] + } + require.NoError(t, err) + case *idp.FormAuth: + authURL, err = url.Parse(a.URL) + require.NoError(t, err) + authFields = a.Fields + } + assert.Equal(t, tt.res.url, authURL.Scheme+"://"+authURL.Host+authURL.Path) - query := authURL.Query() for k, v := range tt.res.values { - assert.True(t, query.Has(k)) + assert.Contains(t, authFields, k) if v != "" { - assert.Equal(t, v, query.Get(k)) + assert.Equal(t, v, authFields[k]) } } }) diff --git a/internal/command/instance.go b/internal/command/instance.go index 0163b996a1..8a686262d7 100644 --- a/internal/command/instance.go +++ b/internal/command/instance.go @@ -2,6 +2,7 @@ package command import ( "context" + "strings" "time" "github.com/zitadel/logging" @@ -216,33 +217,33 @@ func (s *InstanceSetup) generateIDs(idGenerator id.Generator) (err error) { return err } -func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (string, string, *MachineKey, *domain.ObjectDetails, error) { +func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (string, string, *MachineKey, string, *domain.ObjectDetails, error) { if err := setup.generateIDs(c.idGenerator); err != nil { - return "", "", nil, nil, err + return "", "", nil, "", nil, err } ctx = contextWithInstanceSetupInfo(ctx, setup.zitadel.instanceID, setup.zitadel.projectID, setup.zitadel.consoleAppID, c.externalDomain, setup.DefaultLanguage) - validations, pat, machineKey, err := setUpInstance(ctx, c, setup) + validations, pat, machineKey, loginClientPat, err := setUpInstance(ctx, c, setup) if err != nil { - return "", "", nil, nil, err + return "", "", nil, "", nil, err } //nolint:staticcheck cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, validations...) if err != nil { - return "", "", nil, nil, err + return "", "", nil, "", nil, err } _, err = c.eventstore.Push(ctx, cmds...) if err != nil { - return "", "", nil, nil, err + return "", "", nil, "", nil, err } // RolePermissions need to be pushed in separate transaction. // https://github.com/zitadel/zitadel/issues/9293 details, err := c.SynchronizeRolePermission(ctx, setup.zitadel.instanceID, setup.RolePermissionMappings) if err != nil { - return "", "", nil, nil, err + return "", "", nil, "", nil, err } details.ResourceOwner = setup.zitadel.orgID @@ -250,8 +251,12 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str if pat != nil { token = pat.Token } + var loginClientToken string + if loginClientPat != nil { + loginClientToken = loginClientPat.Token + } - return setup.zitadel.instanceID, token, machineKey, details, nil + return setup.zitadel.instanceID, token, machineKey, loginClientToken, details, nil } func contextWithInstanceSetupInfo(ctx context.Context, instanceID, projectID, consoleAppID, externalDomain string, defaultLanguage language.Tag) context.Context { @@ -273,38 +278,38 @@ func contextWithInstanceSetupInfo(ctx context.Context, instanceID, projectID, co ) } -func setUpInstance(ctx context.Context, c *Commands, setup *InstanceSetup) (validations []preparation.Validation, pat *PersonalAccessToken, machineKey *MachineKey, err error) { +func setUpInstance(ctx context.Context, c *Commands, setup *InstanceSetup) (validations []preparation.Validation, pat *PersonalAccessToken, machineKey *MachineKey, loginClientPat *PersonalAccessToken, err error) { instanceAgg := instance.NewAggregate(setup.zitadel.instanceID) validations = setupInstanceElements(instanceAgg, setup) // default organization on setup'd instance - pat, machineKey, err = setupDefaultOrg(ctx, c, &validations, instanceAgg, setup.Org.Name, setup.Org.Machine, setup.Org.Human, setup.zitadel) + pat, machineKey, loginClientPat, err = setupDefaultOrg(ctx, c, &validations, instanceAgg, setup.Org.Name, setup.Org.Machine, setup.Org.Human, setup.Org.LoginClient, setup.zitadel) if err != nil { - return nil, nil, nil, err + return nil, nil, nil, nil, err } // domains if err := setupGeneratedDomain(ctx, c, &validations, instanceAgg, setup.InstanceName); err != nil { - return nil, nil, nil, err + return nil, nil, nil, nil, err } setupCustomDomain(c, &validations, instanceAgg, setup.CustomDomain) // optional setting if set setupMessageTexts(&validations, setup.MessageTexts, instanceAgg) if err := setupQuotas(c, &validations, setup.Quotas, setup.zitadel.instanceID); err != nil { - return nil, nil, nil, err + return nil, nil, nil, nil, err } setupSMTPSettings(c, &validations, setup.SMTPConfiguration, instanceAgg) if err := setupWebKeys(c, &validations, setup.zitadel.instanceID, setup); err != nil { - return nil, nil, nil, err + return nil, nil, nil, nil, err } setupOIDCSettings(c, &validations, setup.OIDCSettings, instanceAgg) setupFeatures(&validations, setup.Features, setup.zitadel.instanceID) setupLimits(c, &validations, limits.NewAggregate(setup.zitadel.limitsID, setup.zitadel.instanceID), setup.Limits) setupRestrictions(c, &validations, restrictions.NewAggregate(setup.zitadel.restrictionsID, setup.zitadel.instanceID, setup.zitadel.instanceID), setup.Restrictions) setupInstanceCreatedMilestone(&validations, setup.zitadel.instanceID) - return validations, pat, machineKey, nil + return validations, pat, machineKey, loginClientPat, nil } func setupInstanceElements(instanceAgg *instance.Aggregate, setup *InstanceSetup) []preparation.Validation { @@ -571,8 +576,9 @@ func setupDefaultOrg(ctx context.Context, name string, machine *AddMachine, human *AddHuman, + loginClient *AddLoginClient, ids ZitadelConfig, -) (pat *PersonalAccessToken, machineKey *MachineKey, err error) { +) (pat *PersonalAccessToken, machineKey *MachineKey, loginClientPat *PersonalAccessToken, err error) { orgAgg := org.NewAggregate(ids.orgID) *validations = append( @@ -581,12 +587,12 @@ func setupDefaultOrg(ctx context.Context, commands.prepareSetDefaultOrg(instanceAgg, ids.orgID), ) - projectOwner, pat, machineKey, err := setupAdmins(commands, validations, instanceAgg, orgAgg, machine, human) + projectOwner, pat, machineKey, loginClientPat, err := setupAdmins(commands, validations, instanceAgg, orgAgg, machine, human, loginClient) if err != nil { - return nil, nil, err + return nil, nil, nil, err } setupMinimalInterfaces(commands, validations, instanceAgg, orgAgg, projectOwner, ids) - return pat, machineKey, nil + return pat, machineKey, loginClientPat, nil } func setupAdmins(commands *Commands, @@ -595,21 +601,22 @@ func setupAdmins(commands *Commands, orgAgg *org.Aggregate, machine *AddMachine, human *AddHuman, -) (owner string, pat *PersonalAccessToken, machineKey *MachineKey, err error) { + loginClient *AddLoginClient, +) (owner string, pat *PersonalAccessToken, machineKey *MachineKey, loginClientPat *PersonalAccessToken, err error) { if human == nil && machine == nil { - return "", nil, nil, zerrors.ThrowInvalidArgument(nil, "INSTANCE-z1yi2q2ot7", "Error.Instance.NoAdmin") + return "", nil, nil, nil, zerrors.ThrowInvalidArgument(nil, "INSTANCE-z1yi2q2ot7", "Error.Instance.NoAdmin") } if machine != nil && machine.Machine != nil && !machine.Machine.IsZero() { machineUserID, err := commands.idGenerator.Next() if err != nil { - return "", nil, nil, err + return "", nil, nil, nil, err } owner = machineUserID pat, machineKey, err = setupMachineAdmin(commands, validations, machine, orgAgg.ID, machineUserID) if err != nil { - return "", nil, nil, err + return "", nil, nil, nil, err } setupAdminMembers(commands, validations, instanceAgg, orgAgg, machineUserID) @@ -617,7 +624,7 @@ func setupAdmins(commands *Commands, if human != nil { humanUserID, err := commands.idGenerator.Next() if err != nil { - return "", nil, nil, err + return "", nil, nil, nil, err } owner = humanUserID human.ID = humanUserID @@ -628,7 +635,18 @@ func setupAdmins(commands *Commands, setupAdminMembers(commands, validations, instanceAgg, orgAgg, humanUserID) } - return owner, pat, machineKey, nil + if loginClient != nil { + loginClientUserID, err := commands.idGenerator.Next() + if err != nil { + return "", nil, nil, nil, err + } + + loginClientPat, err = setupLoginClient(commands, validations, instanceAgg, loginClient, orgAgg.ID, loginClientUserID) + if err != nil { + return "", nil, nil, nil, err + } + } + return owner, pat, machineKey, loginClientPat, nil } func setupMachineAdmin(commands *Commands, validations *[]preparation.Validation, machine *AddMachine, orgID, userID string) (pat *PersonalAccessToken, machineKey *MachineKey, err error) { @@ -654,9 +672,25 @@ func setupMachineAdmin(commands *Commands, validations *[]preparation.Validation return pat, machineKey, nil } +func setupLoginClient(commands *Commands, validations *[]preparation.Validation, instanceAgg *instance.Aggregate, loginClient *AddLoginClient, orgID, userID string) (pat *PersonalAccessToken, err error) { + *validations = append(*validations, + AddMachineCommand(user.NewAggregate(userID, orgID), loginClient.Machine), + commands.AddInstanceMemberCommand(instanceAgg, userID, domain.RoleIAMLoginClient), + ) + if loginClient.Pat != nil { + pat = NewPersonalAccessToken(orgID, userID, loginClient.Pat.ExpirationDate, loginClient.Pat.Scopes, domain.UserTypeMachine) + pat.TokenID, err = commands.idGenerator.Next() + if err != nil { + return nil, err + } + *validations = append(*validations, prepareAddPersonalAccessToken(pat, commands.keyAlgorithm)) + } + return pat, nil +} + func setupAdminMembers(commands *Commands, validations *[]preparation.Validation, instanceAgg *instance.Aggregate, orgAgg *org.Aggregate, userID string) { *validations = append(*validations, - commands.AddOrgMemberCommand(orgAgg, userID, domain.RoleOrgOwner), + commands.AddOrgMemberCommand(&AddOrgMember{orgAgg.ID, userID, []string{domain.RoleOrgOwner}}), commands.AddInstanceMemberCommand(instanceAgg, userID, domain.RoleIAMOwner), ) } @@ -669,7 +703,7 @@ func setupMessageTexts(validations *[]preparation.Validation, setupMessageTexts func (c *Commands) UpdateInstance(ctx context.Context, name string) (*domain.ObjectDetails, error) { instanceAgg := instance.NewAggregate(authz.GetInstance(ctx).InstanceID()) - validation := c.prepareUpdateInstance(instanceAgg, name) + validation := c.prepareUpdateInstance(instanceAgg, strings.TrimSpace(name)) cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, validation) if err != nil { return nil, err @@ -888,7 +922,12 @@ func getSystemConfigWriteModel(ctx context.Context, filter preparation.FilterToQ } func (c *Commands) RemoveInstance(ctx context.Context, id string) (*domain.ObjectDetails, error) { - instanceAgg := instance.NewAggregate(id) + instID := strings.TrimSpace(id) + if instID == "" || len(instID) > 200 { + return nil, zerrors.ThrowInvalidArgument(nil, "COMMA-VeS2zI", "Errors.Invalid.Argument") + } + + instanceAgg := instance.NewAggregate(instID) cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareRemoveInstance(instanceAgg)) if err != nil { return nil, err diff --git a/internal/command/instance_features.go b/internal/command/instance_features.go index cb12bff828..04f2621705 100644 --- a/internal/command/instance_features.go +++ b/internal/command/instance_features.go @@ -3,11 +3,8 @@ package command import ( "context" - "github.com/muhlemmer/gu" - "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/command/preparation" - "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/feature" @@ -16,31 +13,25 @@ import ( ) type InstanceFeatures struct { - LoginDefaultOrg *bool - TriggerIntrospectionProjections *bool - LegacyIntrospection *bool - UserSchema *bool - TokenExchange *bool - ImprovedPerformance []feature.ImprovedPerformanceType - WebKey *bool - DebugOIDCParentError *bool - OIDCSingleV1SessionTermination *bool - DisableUserTokenEvent *bool - EnableBackChannelLogout *bool - LoginV2 *feature.LoginV2 - PermissionCheckV2 *bool - ConsoleUseV2UserApi *bool + LoginDefaultOrg *bool + UserSchema *bool + TokenExchange *bool + ImprovedPerformance []feature.ImprovedPerformanceType + DebugOIDCParentError *bool + OIDCSingleV1SessionTermination *bool + DisableUserTokenEvent *bool + EnableBackChannelLogout *bool + LoginV2 *feature.LoginV2 + PermissionCheckV2 *bool + ConsoleUseV2UserApi *bool } func (m *InstanceFeatures) isEmpty() bool { return m.LoginDefaultOrg == nil && - m.TriggerIntrospectionProjections == nil && - m.LegacyIntrospection == nil && m.UserSchema == nil && m.TokenExchange == nil && // nil check to allow unset improvements m.ImprovedPerformance == nil && - m.WebKey == nil && m.DebugOIDCParentError == nil && m.OIDCSingleV1SessionTermination == nil && m.DisableUserTokenEvent == nil && @@ -57,9 +48,6 @@ func (c *Commands) SetInstanceFeatures(ctx context.Context, f *InstanceFeatures) if err := c.eventstore.FilterToQueryReducer(ctx, wm); err != nil { return nil, err } - if err := c.setupWebKeyFeature(ctx, wm, f); err != nil { - return nil, err - } commands := wm.setCommands(ctx, f) if len(commands) == 0 { return writeModelToObjectDetails(wm.WriteModel), nil @@ -80,21 +68,6 @@ func prepareSetFeatures(instanceID string, f *InstanceFeatures) preparation.Vali } } -// setupWebKeyFeature generates the initial web keys for the instance, -// if the feature is enabled in the request and the feature wasn't enabled already in the writeModel. -// [Commands.GenerateInitialWebKeys] checks if keys already exist and does nothing if that's the case. -// The default config of a RSA key with 2048 and the SHA256 hasher is assumed. -// Users can customize this after using the webkey/v3 API. -func (c *Commands) setupWebKeyFeature(ctx context.Context, wm *InstanceFeaturesWriteModel, f *InstanceFeatures) error { - if !gu.Value(f.WebKey) || gu.Value(wm.WebKey) { - return nil - } - return c.GenerateInitialWebKeys(ctx, &crypto.WebKeyRSAConfig{ - Bits: crypto.RSABits2048, - Hasher: crypto.RSAHasherSHA256, - }) -} - func (c *Commands) ResetInstanceFeatures(ctx context.Context) (*domain.ObjectDetails, error) { instanceID := authz.GetInstance(ctx).InstanceID() wm := NewInstanceFeaturesWriteModel(instanceID) diff --git a/internal/command/instance_features_model.go b/internal/command/instance_features_model.go index 977a46b6c2..8fe9dd0284 100644 --- a/internal/command/instance_features_model.go +++ b/internal/command/instance_features_model.go @@ -67,12 +67,9 @@ func (m *InstanceFeaturesWriteModel) Query() *eventstore.SearchQueryBuilder { feature_v1.DefaultLoginInstanceEventType, feature_v2.InstanceResetEventType, feature_v2.InstanceLoginDefaultOrgEventType, - feature_v2.InstanceTriggerIntrospectionProjectionsEventType, - feature_v2.InstanceLegacyIntrospectionEventType, feature_v2.InstanceUserSchemaEventType, feature_v2.InstanceTokenExchangeEventType, feature_v2.InstanceImprovedPerformanceEventType, - feature_v2.InstanceWebKeyEventType, feature_v2.InstanceDebugOIDCParentErrorEventType, feature_v2.InstanceOIDCSingleV1SessionTerminationEventType, feature_v2.InstanceDisableUserTokenEvent, @@ -95,12 +92,6 @@ func reduceInstanceFeature(features *InstanceFeatures, key feature.Key, value an case feature.KeyLoginDefaultOrg: v := value.(bool) features.LoginDefaultOrg = &v - case feature.KeyTriggerIntrospectionProjections: - v := value.(bool) - features.TriggerIntrospectionProjections = &v - case feature.KeyLegacyIntrospection: - v := value.(bool) - features.LegacyIntrospection = &v case feature.KeyTokenExchange: v := value.(bool) features.TokenExchange = &v @@ -110,9 +101,6 @@ func reduceInstanceFeature(features *InstanceFeatures, key feature.Key, value an case feature.KeyImprovedPerformance: v := value.([]feature.ImprovedPerformanceType) features.ImprovedPerformance = v - case feature.KeyWebKey: - v := value.(bool) - features.WebKey = &v case feature.KeyDebugOIDCParentError: v := value.(bool) features.DebugOIDCParentError = &v @@ -140,12 +128,9 @@ func (wm *InstanceFeaturesWriteModel) setCommands(ctx context.Context, f *Instan aggregate := feature_v2.NewAggregate(wm.AggregateID, wm.ResourceOwner) cmds := make([]eventstore.Command, 0, len(feature.KeyValues())-1) cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.LoginDefaultOrg, f.LoginDefaultOrg, feature_v2.InstanceLoginDefaultOrgEventType) - cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.TriggerIntrospectionProjections, f.TriggerIntrospectionProjections, feature_v2.InstanceTriggerIntrospectionProjectionsEventType) - cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.LegacyIntrospection, f.LegacyIntrospection, feature_v2.InstanceLegacyIntrospectionEventType) cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.TokenExchange, f.TokenExchange, feature_v2.InstanceTokenExchangeEventType) cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.UserSchema, f.UserSchema, feature_v2.InstanceUserSchemaEventType) cmds = appendFeatureSliceUpdate(ctx, cmds, aggregate, wm.ImprovedPerformance, f.ImprovedPerformance, feature_v2.InstanceImprovedPerformanceEventType) - cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.WebKey, f.WebKey, feature_v2.InstanceWebKeyEventType) cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.DebugOIDCParentError, f.DebugOIDCParentError, feature_v2.InstanceDebugOIDCParentErrorEventType) cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.OIDCSingleV1SessionTermination, f.OIDCSingleV1SessionTermination, feature_v2.InstanceOIDCSingleV1SessionTerminationEventType) cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.DisableUserTokenEvent, f.DisableUserTokenEvent, feature_v2.InstanceDisableUserTokenEvent) diff --git a/internal/command/instance_features_test.go b/internal/command/instance_features_test.go index 02e8896a0c..f0bea9752d 100644 --- a/internal/command/instance_features_test.go +++ b/internal/command/instance_features_test.go @@ -95,42 +95,6 @@ func TestCommands_SetInstanceFeatures(t *testing.T) { ResourceOwner: "instance1", }, }, - { - name: "set TriggerIntrospectionProjections", - eventstore: expectEventstore( - expectFilter(), - expectPush( - feature_v2.NewSetEvent[bool]( - ctx, aggregate, - feature_v2.InstanceTriggerIntrospectionProjectionsEventType, true, - ), - ), - ), - args: args{ctx, &InstanceFeatures{ - TriggerIntrospectionProjections: gu.Ptr(true), - }}, - want: &domain.ObjectDetails{ - ResourceOwner: "instance1", - }, - }, - { - name: "set LegacyIntrospection", - eventstore: expectEventstore( - expectFilter(), - expectPush( - feature_v2.NewSetEvent[bool]( - ctx, aggregate, - feature_v2.InstanceLegacyIntrospectionEventType, true, - ), - ), - ), - args: args{ctx, &InstanceFeatures{ - LegacyIntrospection: gu.Ptr(true), - }}, - want: &domain.ObjectDetails{ - ResourceOwner: "instance1", - }, - }, { name: "set UserSchema", eventstore: expectEventstore( @@ -156,12 +120,12 @@ func TestCommands_SetInstanceFeatures(t *testing.T) { expectPushFailed(io.ErrClosedPipe, feature_v2.NewSetEvent[bool]( ctx, aggregate, - feature_v2.InstanceLegacyIntrospectionEventType, true, + feature_v2.InstanceConsoleUseV2UserApi, true, ), ), ), args: args{ctx, &InstanceFeatures{ - LegacyIntrospection: gu.Ptr(true), + ConsoleUseV2UserApi: gu.Ptr(true), }}, wantErr: io.ErrClosedPipe, }, @@ -174,14 +138,6 @@ func TestCommands_SetInstanceFeatures(t *testing.T) { ctx, aggregate, feature_v2.InstanceLoginDefaultOrgEventType, true, ), - feature_v2.NewSetEvent[bool]( - ctx, aggregate, - feature_v2.InstanceTriggerIntrospectionProjectionsEventType, false, - ), - feature_v2.NewSetEvent[bool]( - ctx, aggregate, - feature_v2.InstanceLegacyIntrospectionEventType, true, - ), feature_v2.NewSetEvent[bool]( ctx, aggregate, feature_v2.InstanceUserSchemaEventType, true, @@ -193,11 +149,9 @@ func TestCommands_SetInstanceFeatures(t *testing.T) { ), ), args: args{ctx, &InstanceFeatures{ - LoginDefaultOrg: gu.Ptr(true), - TriggerIntrospectionProjections: gu.Ptr(false), - LegacyIntrospection: gu.Ptr(true), - UserSchema: gu.Ptr(true), - OIDCSingleV1SessionTermination: gu.Ptr(true), + LoginDefaultOrg: gu.Ptr(true), + UserSchema: gu.Ptr(true), + OIDCSingleV1SessionTermination: gu.Ptr(true), }}, want: &domain.ObjectDetails{ ResourceOwner: "instance1", @@ -212,10 +166,6 @@ func TestCommands_SetInstanceFeatures(t *testing.T) { ctx, aggregate, feature_v2.InstanceLoginDefaultOrgEventType, true, )), - eventFromEventPusher(feature_v2.NewSetEvent[bool]( - ctx, aggregate, - feature_v2.InstanceTriggerIntrospectionProjectionsEventType, false, - )), eventFromEventPusher(feature_v2.NewResetEvent( ctx, aggregate, feature_v2.InstanceResetEventType, @@ -224,10 +174,6 @@ func TestCommands_SetInstanceFeatures(t *testing.T) { ctx, aggregate, feature_v2.InstanceLoginDefaultOrgEventType, false, )), - eventFromEventPusher(feature_v2.NewSetEvent[bool]( - ctx, aggregate, - feature_v2.InstanceLegacyIntrospectionEventType, true, - )), feature_v2.NewSetEvent[bool]( context.Background(), aggregate, feature_v2.InstanceOIDCSingleV1SessionTerminationEventType, false, @@ -238,17 +184,11 @@ func TestCommands_SetInstanceFeatures(t *testing.T) { ctx, aggregate, feature_v2.InstanceLoginDefaultOrgEventType, true, ), - feature_v2.NewSetEvent[bool]( - ctx, aggregate, - feature_v2.InstanceTriggerIntrospectionProjectionsEventType, false, - ), ), ), args: args{ctx, &InstanceFeatures{ - LoginDefaultOrg: gu.Ptr(true), - TriggerIntrospectionProjections: gu.Ptr(false), - LegacyIntrospection: gu.Ptr(true), - OIDCSingleV1SessionTermination: gu.Ptr(false), + LoginDefaultOrg: gu.Ptr(true), + OIDCSingleV1SessionTermination: gu.Ptr(false), }}, want: &domain.ObjectDetails{ ResourceOwner: "instance1", diff --git a/internal/command/instance_member.go b/internal/command/instance_member.go index ee9bf15f84..0657170f75 100644 --- a/internal/command/instance_member.go +++ b/internal/command/instance_member.go @@ -2,7 +2,7 @@ package command import ( "context" - "reflect" + "slices" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/command/preparation" @@ -22,7 +22,7 @@ func (c *Commands) AddInstanceMemberCommand(a *instance.Aggregate, userID string return nil, zerrors.ThrowInvalidArgument(nil, "INSTANCE-4m0fS", "Errors.IAM.MemberInvalid") } return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { - if exists, err := ExistsUser(ctx, filter, userID, ""); err != nil || !exists { + if exists, err := ExistsUser(ctx, filter, userID, "", false); err != nil || !exists { return nil, zerrors.ThrowPreconditionFailed(err, "INSTA-GSXOn", "Errors.User.NotFound") } if isMember, err := IsInstanceMember(ctx, filter, a.ID, userID); err != nil || isMember { @@ -69,9 +69,19 @@ func IsInstanceMember(ctx context.Context, filter preparation.FilterToQueryReduc return isMember, nil } -func (c *Commands) AddInstanceMember(ctx context.Context, userID string, roles ...string) (*domain.Member, error) { - instanceAgg := instance.NewAggregate(authz.GetInstance(ctx).InstanceID()) - cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.AddInstanceMemberCommand(instanceAgg, userID, roles...)) +type AddInstanceMember struct { + InstanceID string + UserID string + Roles []string +} + +func (c *Commands) AddInstanceMember(ctx context.Context, member *AddInstanceMember) (*domain.ObjectDetails, error) { + instanceAgg := instance.NewAggregate(member.InstanceID) + if err := c.checkPermissionUpdateInstanceMember(ctx, member.InstanceID); err != nil { + return nil, err + } + //nolint:staticcheck + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.AddInstanceMemberCommand(instanceAgg, member.UserID, member.Roles...)) if err != nil { return nil, err } @@ -79,33 +89,56 @@ func (c *Commands) AddInstanceMember(ctx context.Context, userID string, roles . if err != nil { return nil, err } - addedMember := NewInstanceMemberWriteModel(ctx, userID) + addedMember := NewInstanceMemberWriteModel(member.InstanceID, member.UserID) err = AppendAndReduce(addedMember, events...) if err != nil { return nil, err } - return memberWriteModelToMember(&addedMember.MemberWriteModel), nil + return writeModelToObjectDetails(&addedMember.WriteModel), nil +} + +type ChangeInstanceMember struct { + InstanceID string + UserID string + Roles []string +} + +func (i *ChangeInstanceMember) IsValid(zitadelRoles []authz.RoleMapping) error { + if i.InstanceID == "" || i.UserID == "" || len(i.Roles) == 0 { + return zerrors.ThrowInvalidArgument(nil, "INSTANCE-LiaZi", "Errors.IAM.MemberInvalid") + } + if len(domain.CheckForInvalidRoles(i.Roles, domain.IAMRolePrefix, zitadelRoles)) > 0 { + return zerrors.ThrowInvalidArgument(nil, "INSTANCE-3m9fs", "Errors.IAM.MemberInvalid") + } + return nil } // ChangeInstanceMember updates an existing member -func (c *Commands) ChangeInstanceMember(ctx context.Context, member *domain.Member) (*domain.Member, error) { - if !member.IsIAMValid() { - return nil, zerrors.ThrowInvalidArgument(nil, "INSTANCE-LiaZi", "Errors.IAM.MemberInvalid") - } - if len(domain.CheckForInvalidRoles(member.Roles, domain.IAMRolePrefix, c.zitadelRoles)) > 0 { - return nil, zerrors.ThrowInvalidArgument(nil, "INSTANCE-3m9fs", "Errors.IAM.MemberInvalid") - } - - existingMember, err := c.instanceMemberWriteModelByID(ctx, member.UserID) - if err != nil { +func (c *Commands) ChangeInstanceMember(ctx context.Context, member *ChangeInstanceMember) (*domain.ObjectDetails, error) { + if err := member.IsValid(c.zitadelRoles); err != nil { return nil, err } - if reflect.DeepEqual(existingMember.Roles, member.Roles) { - return nil, zerrors.ThrowPreconditionFailed(nil, "INSTANCE-LiaZi", "Errors.IAM.Member.RolesNotChanged") + existingMember, err := c.instanceMemberWriteModelByID(ctx, member.InstanceID, member.UserID) + if err != nil { + return nil, err } - instanceAgg := InstanceAggregateFromWriteModel(&existingMember.MemberWriteModel.WriteModel) - pushedEvents, err := c.eventstore.Push(ctx, instance.NewMemberChangedEvent(ctx, instanceAgg, member.UserID, member.Roles...)) + if !existingMember.State.Exists() { + return nil, zerrors.ThrowNotFound(nil, "INSTANCE-D8JxR", "Errors.NotFound") + } + if err := c.checkPermissionUpdateInstanceMember(ctx, existingMember.AggregateID); err != nil { + return nil, err + } + if slices.Compare(existingMember.Roles, member.Roles) == 0 { + return writeModelToObjectDetails(&existingMember.WriteModel), nil + } + pushedEvents, err := c.eventstore.Push(ctx, + instance.NewMemberChangedEvent(ctx, + InstanceAggregateFromWriteModel(&existingMember.WriteModel), + member.UserID, + member.Roles..., + ), + ) if err != nil { return nil, err } @@ -114,34 +147,40 @@ func (c *Commands) ChangeInstanceMember(ctx context.Context, member *domain.Memb return nil, err } - return memberWriteModelToMember(&existingMember.MemberWriteModel), nil + return writeModelToObjectDetails(&existingMember.WriteModel), nil } -func (c *Commands) RemoveInstanceMember(ctx context.Context, userID string) (*domain.ObjectDetails, error) { +func (c *Commands) RemoveInstanceMember(ctx context.Context, instanceID, userID string) (*domain.ObjectDetails, error) { if userID == "" { return nil, zerrors.ThrowInvalidArgument(nil, "INSTANCE-LiaZi", "Errors.IDMissing") } - memberWriteModel, err := c.instanceMemberWriteModelByID(ctx, userID) - if err != nil && !zerrors.IsNotFound(err) { - return nil, err - } - if zerrors.IsNotFound(err) { - // empty response because we have no data that match the request - return &domain.ObjectDetails{}, nil - } - - instanceAgg := InstanceAggregateFromWriteModel(&memberWriteModel.MemberWriteModel.WriteModel) - removeEvent := c.removeInstanceMember(ctx, instanceAgg, userID, false) - pushedEvents, err := c.eventstore.Push(ctx, removeEvent) + existingMember, err := c.instanceMemberWriteModelByID(ctx, instanceID, userID) if err != nil { return nil, err } - err = AppendAndReduce(memberWriteModel, pushedEvents...) + if err := c.checkPermissionDeleteInstanceMember(ctx, instanceID); err != nil { + return nil, err + } + if !existingMember.State.Exists() { + return writeModelToObjectDetails(&existingMember.WriteModel), nil + } + + pushedEvents, err := c.eventstore.Push(ctx, + c.removeInstanceMember(ctx, + InstanceAggregateFromWriteModel(&existingMember.WriteModel), + userID, + false, + ), + ) + if err != nil { + return nil, err + } + err = AppendAndReduce(existingMember, pushedEvents...) if err != nil { return nil, err } - return writeModelToObjectDetails(&memberWriteModel.MemberWriteModel.WriteModel), nil + return writeModelToObjectDetails(&existingMember.WriteModel), nil } func (c *Commands) removeInstanceMember(ctx context.Context, instanceAgg *eventstore.Aggregate, userID string, cascade bool) eventstore.Command { @@ -155,19 +194,15 @@ func (c *Commands) removeInstanceMember(ctx context.Context, instanceAgg *events } } -func (c *Commands) instanceMemberWriteModelByID(ctx context.Context, userID string) (member *InstanceMemberWriteModel, err error) { +func (c *Commands) instanceMemberWriteModelByID(ctx context.Context, instanceID, userID string) (member *InstanceMemberWriteModel, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - writeModel := NewInstanceMemberWriteModel(ctx, userID) + writeModel := NewInstanceMemberWriteModel(instanceID, userID) err = c.eventstore.FilterToQueryReducer(ctx, writeModel) if err != nil { return nil, err } - if writeModel.State == domain.MemberStateUnspecified || writeModel.State == domain.MemberStateRemoved { - return nil, zerrors.ThrowNotFound(nil, "INSTANCE-D8JxR", "Errors.NotFound") - } - return writeModel, nil } diff --git a/internal/command/instance_member_model.go b/internal/command/instance_member_model.go index e987a41d97..9cd2ca26a0 100644 --- a/internal/command/instance_member_model.go +++ b/internal/command/instance_member_model.go @@ -1,9 +1,6 @@ package command import ( - "context" - - "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/repository/instance" ) @@ -12,12 +9,12 @@ type InstanceMemberWriteModel struct { MemberWriteModel } -func NewInstanceMemberWriteModel(ctx context.Context, userID string) *InstanceMemberWriteModel { +func NewInstanceMemberWriteModel(instanceID, userID string) *InstanceMemberWriteModel { return &InstanceMemberWriteModel{ MemberWriteModel{ WriteModel: eventstore.WriteModel{ - AggregateID: authz.GetInstance(ctx).InstanceID(), - ResourceOwner: authz.GetInstance(ctx).InstanceID(), + AggregateID: instanceID, + ResourceOwner: instanceID, }, UserID: userID, }, diff --git a/internal/command/instance_member_test.go b/internal/command/instance_member_test.go index 8d254c56bc..264e7d16aa 100644 --- a/internal/command/instance_member_test.go +++ b/internal/command/instance_member_test.go @@ -10,24 +10,22 @@ import ( "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" - "github.com/zitadel/zitadel/internal/eventstore/v1/models" "github.com/zitadel/zitadel/internal/repository/instance" "github.com/zitadel/zitadel/internal/repository/user" "github.com/zitadel/zitadel/internal/zerrors" ) -func TestCommandSide_AddIAMMember(t *testing.T) { +func TestCommandSide_AddInstanceMember(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore - zitadelRoles []authz.RoleMapping + eventstore func(t *testing.T) *eventstore.Eventstore + zitadelRoles []authz.RoleMapping + checkPermission domain.PermissionCheck } type args struct { - ctx context.Context - userID string - roles []string + member *AddInstanceMember } type res struct { - want *domain.Member + want *domain.ObjectDetails err func(error) bool } tests := []struct { @@ -39,28 +37,28 @@ func TestCommandSide_AddIAMMember(t *testing.T) { { name: "invalid member, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - ctx: context.Background(), + member: &AddInstanceMember{}, }, res: res{ - err: zerrors.IsErrorInvalidArgument, + err: zerrors.IsInternal, }, }, { name: "invalid roles, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - ctx: context.Background(), - userID: "user1", - roles: []string{"IAM_OWNER"}, + member: &AddInstanceMember{ + InstanceID: "INSTANCE", + UserID: "user1", + Roles: []string{"IAM_OWNER"}, + }, }, res: res{ err: zerrors.IsErrorInvalidArgument, @@ -69,10 +67,10 @@ func TestCommandSide_AddIAMMember(t *testing.T) { { name: "user not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), zitadelRoles: []authz.RoleMapping{ { Role: "IAM_OWNER", @@ -80,9 +78,11 @@ func TestCommandSide_AddIAMMember(t *testing.T) { }, }, args: args{ - ctx: context.Background(), - userID: "user1", - roles: []string{"IAM_OWNER"}, + member: &AddInstanceMember{ + InstanceID: "INSTANCE", + UserID: "user1", + Roles: []string{"IAM_OWNER"}, + }, }, res: res{ err: zerrors.IsPreconditionFailed, @@ -91,8 +91,7 @@ func TestCommandSide_AddIAMMember(t *testing.T) { { name: "member already exists, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -118,6 +117,7 @@ func TestCommandSide_AddIAMMember(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), zitadelRoles: []authz.RoleMapping{ { Role: "IAM_OWNER", @@ -125,9 +125,11 @@ func TestCommandSide_AddIAMMember(t *testing.T) { }, }, args: args{ - ctx: context.Background(), - userID: "user1", - roles: []string{"IAM_OWNER"}, + member: &AddInstanceMember{ + InstanceID: "INSTANCE", + UserID: "user1", + Roles: []string{"IAM_OWNER"}, + }, }, res: res{ err: zerrors.IsErrorAlreadyExists, @@ -136,8 +138,7 @@ func TestCommandSide_AddIAMMember(t *testing.T) { { name: "member add uniqueconstraint err, already exists", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -163,6 +164,7 @@ func TestCommandSide_AddIAMMember(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), zitadelRoles: []authz.RoleMapping{ { Role: "IAM_OWNER", @@ -170,9 +172,11 @@ func TestCommandSide_AddIAMMember(t *testing.T) { }, }, args: args{ - ctx: authz.WithInstanceID(context.Background(), "INSTANCE"), - userID: "user1", - roles: []string{"IAM_OWNER"}, + member: &AddInstanceMember{ + InstanceID: "INSTANCE", + UserID: "user1", + Roles: []string{"IAM_OWNER"}, + }, }, res: res{ err: zerrors.IsErrorAlreadyExists, @@ -181,8 +185,7 @@ func TestCommandSide_AddIAMMember(t *testing.T) { { name: "member add, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusherWithInstanceID( "INSTANCE", @@ -209,6 +212,7 @@ func TestCommandSide_AddIAMMember(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), zitadelRoles: []authz.RoleMapping{ { Role: "IAM_OWNER", @@ -216,30 +220,49 @@ func TestCommandSide_AddIAMMember(t *testing.T) { }, }, args: args{ - ctx: authz.WithInstanceID(context.Background(), "INSTANCE"), - userID: "user1", - roles: []string{"IAM_OWNER"}, + member: &AddInstanceMember{ + InstanceID: "INSTANCE", + UserID: "user1", + Roles: []string{"IAM_OWNER"}, + }, }, res: res{ - want: &domain.Member{ - ObjectRoot: models.ObjectRoot{ - InstanceID: "INSTANCE", - ResourceOwner: "INSTANCE", - AggregateID: "INSTANCE", - }, - UserID: "user1", - Roles: []string{"IAM_OWNER"}, + want: &domain.ObjectDetails{ + ResourceOwner: "INSTANCE", }, }, }, + { + name: "member add, no permission", + fields: fields{ + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckNotAllowed(), + zitadelRoles: []authz.RoleMapping{ + { + Role: "IAM_OWNER", + }, + }, + }, + args: args{ + member: &AddInstanceMember{ + InstanceID: "INSTANCE", + UserID: "user1", + Roles: []string{"IAM_OWNER"}, + }, + }, + res: res{ + err: zerrors.IsPermissionDenied, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, - zitadelRoles: tt.fields.zitadelRoles, + eventstore: tt.fields.eventstore(t), + zitadelRoles: tt.fields.zitadelRoles, + checkPermission: tt.fields.checkPermission, } - got, err := r.AddInstanceMember(tt.args.ctx, tt.args.userID, tt.args.roles...) + got, err := r.AddInstanceMember(context.Background(), tt.args.member) if tt.res.err == nil { assert.NoError(t, err) } @@ -247,24 +270,23 @@ func TestCommandSide_AddIAMMember(t *testing.T) { t.Errorf("got wrong err: %v ", err) } if tt.res.err == nil { - assert.Equal(t, tt.res.want, got) + assertObjectDetails(t, tt.res.want, got) } }) } } -func TestCommandSide_ChangeIAMMember(t *testing.T) { +func TestCommandSide_ChangeInstanceMember(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore - zitadelRoles []authz.RoleMapping + eventstore func(t *testing.T) *eventstore.Eventstore + zitadelRoles []authz.RoleMapping + checkPermission domain.PermissionCheck } type args struct { - ctx context.Context - instanceID string - member *domain.Member + member *ChangeInstanceMember } type res struct { - want *domain.Member + want *domain.ObjectDetails err func(error) bool } tests := []struct { @@ -276,13 +298,11 @@ func TestCommandSide_ChangeIAMMember(t *testing.T) { { name: "invalid member, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - ctx: context.Background(), - member: &domain.Member{}, + member: &ChangeInstanceMember{}, }, res: res{ err: zerrors.IsErrorInvalidArgument, @@ -291,15 +311,14 @@ func TestCommandSide_ChangeIAMMember(t *testing.T) { { name: "invalid roles, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - ctx: context.Background(), - member: &domain.Member{ - UserID: "user1", - Roles: []string{"IAM_OWNER"}, + member: &ChangeInstanceMember{ + InstanceID: "INSTANCE", + UserID: "user1", + Roles: []string{"IAM_OWNER"}, }, }, res: res{ @@ -309,10 +328,10 @@ func TestCommandSide_ChangeIAMMember(t *testing.T) { { name: "member not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), zitadelRoles: []authz.RoleMapping{ { Role: "IAM_OWNER", @@ -320,10 +339,10 @@ func TestCommandSide_ChangeIAMMember(t *testing.T) { }, }, args: args{ - ctx: context.Background(), - member: &domain.Member{ - UserID: "user1", - Roles: []string{"IAM_OWNER"}, + member: &ChangeInstanceMember{ + InstanceID: "INSTANCE", + UserID: "user1", + Roles: []string{"IAM_OWNER"}, }, }, res: res{ @@ -333,8 +352,7 @@ func TestCommandSide_ChangeIAMMember(t *testing.T) { { name: "member not changed, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( instance.NewMemberAddedEvent(context.Background(), @@ -345,6 +363,7 @@ func TestCommandSide_ChangeIAMMember(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), zitadelRoles: []authz.RoleMapping{ { Role: domain.RoleIAMOwner, @@ -352,21 +371,22 @@ func TestCommandSide_ChangeIAMMember(t *testing.T) { }, }, args: args{ - ctx: context.Background(), - member: &domain.Member{ - UserID: "user1", - Roles: []string{"IAM_OWNER"}, + member: &ChangeInstanceMember{ + InstanceID: "INSTANCE", + UserID: "user1", + Roles: []string{"IAM_OWNER"}, }, }, res: res{ - err: zerrors.IsPreconditionFailed, + want: &domain.ObjectDetails{ + ResourceOwner: "INSTANCE", + }, }, }, { name: "member change, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( instance.NewMemberAddedEvent(context.Background(), @@ -384,6 +404,7 @@ func TestCommandSide_ChangeIAMMember(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), zitadelRoles: []authz.RoleMapping{ { Role: "IAM_OWNER", @@ -394,32 +415,62 @@ func TestCommandSide_ChangeIAMMember(t *testing.T) { }, }, args: args{ - ctx: context.Background(), - member: &domain.Member{ - UserID: "user1", - Roles: []string{"IAM_OWNER", "IAM_OWNER_VIEWER"}, + member: &ChangeInstanceMember{ + InstanceID: "INSTANCE", + UserID: "user1", + Roles: []string{"IAM_OWNER", "IAM_OWNER_VIEWER"}, }, }, res: res{ - want: &domain.Member{ - ObjectRoot: models.ObjectRoot{ - ResourceOwner: "INSTANCE", - AggregateID: "INSTANCE", - InstanceID: "INSTANCE", - }, - UserID: "user1", - Roles: []string{"IAM_OWNER", "IAM_OWNER_VIEWER"}, + want: &domain.ObjectDetails{ + ResourceOwner: "INSTANCE", }, }, }, + { + name: "member change, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + instance.NewMemberAddedEvent(context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "user1", + []string{"IAM_OWNER"}..., + ), + ), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + zitadelRoles: []authz.RoleMapping{ + { + Role: "IAM_OWNER", + }, + { + Role: "IAM_OWNER_VIEWER", + }, + }, + }, + args: args{ + member: &ChangeInstanceMember{ + InstanceID: "INSTANCE", + UserID: "user1", + Roles: []string{"IAM_OWNER"}, + }, + }, + res: res{ + err: zerrors.IsPermissionDenied, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, - zitadelRoles: tt.fields.zitadelRoles, + eventstore: tt.fields.eventstore(t), + zitadelRoles: tt.fields.zitadelRoles, + checkPermission: tt.fields.checkPermission, } - got, err := r.ChangeInstanceMember(tt.args.ctx, tt.args.member) + got, err := r.ChangeInstanceMember(context.Background(), tt.args.member) if tt.res.err == nil { assert.NoError(t, err) } @@ -427,18 +478,18 @@ func TestCommandSide_ChangeIAMMember(t *testing.T) { t.Errorf("got wrong err: %v ", err) } if tt.res.err == nil { - assert.Equal(t, tt.res.want, got) + assertObjectDetails(t, tt.res.want, got) } }) } } -func TestCommandSide_RemoveIAMMember(t *testing.T) { +func TestCommandSide_RemoveInstanceMember(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck } type args struct { - ctx context.Context instanceID string userID string } @@ -455,13 +506,12 @@ func TestCommandSide_RemoveIAMMember(t *testing.T) { { name: "invalid member userid missing, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - ctx: context.Background(), - userID: "", + instanceID: "INSTANCE", + userID: "", }, res: res{ err: zerrors.IsErrorInvalidArgument, @@ -470,24 +520,25 @@ func TestCommandSide_RemoveIAMMember(t *testing.T) { { name: "member not existing, empty object details result", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - ctx: context.Background(), - userID: "user1", + instanceID: "INSTANCE", + userID: "user1", }, res: res{ - want: &domain.ObjectDetails{}, + want: &domain.ObjectDetails{ + ResourceOwner: "INSTANCE", + }, }, }, { name: "member remove, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( instance.NewMemberAddedEvent(context.Background(), @@ -504,10 +555,11 @@ func TestCommandSide_RemoveIAMMember(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - ctx: context.Background(), - userID: "user1", + instanceID: "INSTANCE", + userID: "user1", }, res: res{ want: &domain.ObjectDetails{ @@ -515,13 +567,38 @@ func TestCommandSide_RemoveIAMMember(t *testing.T) { }, }, }, + { + name: "member remove, no permission", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + instance.NewMemberAddedEvent(context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "user1", + []string{"IAM_OWNER"}..., + ), + ), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args: args{ + instanceID: "INSTANCE", + userID: "user1", + }, + res: res{ + err: zerrors.IsPermissionDenied, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, } - got, err := r.RemoveInstanceMember(tt.args.ctx, tt.args.userID) + got, err := r.RemoveInstanceMember(context.Background(), tt.args.instanceID, tt.args.userID) if tt.res.err == nil { assert.NoError(t, err) } diff --git a/internal/command/instance_test.go b/internal/command/instance_test.go index 0b400a3278..b40bba19af 100644 --- a/internal/command/instance_test.go +++ b/internal/command/instance_test.go @@ -129,7 +129,7 @@ func oidcAppEvents(ctx context.Context, orgID, projectID, id, name, clientID str } } -func orgFilters(orgID string, machine, human bool) []expect { +func orgFilters(orgID string, machine, human, loginClient bool) []expect { filters := []expect{ expectFilter(), expectFilter( @@ -144,13 +144,17 @@ func orgFilters(orgID string, machine, human bool) []expect { filters = append(filters, humanFilters(orgID)...) filters = append(filters, adminMemberFilters(orgID, "USER")...) } + if loginClient { + filters = append(filters, loginClientFilters(orgID, true)...) + filters = append(filters, instanceMemberFilters(orgID, "USER-LOGIN-CLIENT")...) + } return append(filters, projectFilters()..., ) } -func orgEvents(ctx context.Context, instanceID, orgID, name, projectID, defaultDomain string, externalSecure bool, machine, human bool) []eventstore.Command { +func orgEvents(ctx context.Context, instanceID, orgID, name, projectID, defaultDomain string, externalSecure bool, machine, human, loginClient bool) []eventstore.Command { instanceAgg := instance.NewAggregate(instanceID) orgAgg := org.NewAggregate(orgID) domain := strings.ToLower(name + "." + defaultDomain) @@ -173,13 +177,17 @@ func orgEvents(ctx context.Context, instanceID, orgID, name, projectID, defaultD events = append(events, humanEvents(ctx, instanceID, orgID, userID)...) owner = userID } + if loginClient { + userID := "USER-LOGIN-CLIENT" + events = append(events, loginClientEvents(ctx, instanceID, orgID, userID, "LOGIN-CLIENT-PAT")...) + } events = append(events, projectAddedEvents(ctx, instanceID, orgID, projectID, owner, externalSecure)...) return events } func orgIDs() []string { - return slices.Concat([]string{"USER-MACHINE", "PAT", "USER"}, projectClientIDs()) + return slices.Concat([]string{"USER-MACHINE", "PAT", "USER", "USER-LOGIN-CLIENT", "LOGIN-CLIENT-PAT"}, projectClientIDs()) } func instancePoliciesFilters(instanceID string) []expect { @@ -363,7 +371,7 @@ func instanceElementsConfig() *SecretGenerators { func setupInstanceFilters(instanceID, orgID, projectID, appID, domain string) []expect { return slices.Concat( setupInstanceElementsFilters(instanceID), - orgFilters(orgID, true, true), + orgFilters(orgID, true, true, true), generatedDomainFilters(instanceID, orgID, projectID, appID, domain), ) } @@ -371,7 +379,7 @@ func setupInstanceFilters(instanceID, orgID, projectID, appID, domain string) [] func setupInstanceEvents(ctx context.Context, instanceID, orgID, projectID, appID, instanceName, orgName string, defaultLanguage language.Tag, domain string, externalSecure bool) []eventstore.Command { return slices.Concat( setupInstanceElementsEvents(ctx, instanceID, instanceName, defaultLanguage), - orgEvents(ctx, instanceID, orgID, orgName, projectID, domain, externalSecure, true, true), + orgEvents(ctx, instanceID, orgID, orgName, projectID, domain, externalSecure, true, true, true), generatedDomainEvents(ctx, instanceID, orgID, projectID, appID, domain), instanceCreatedMilestoneEvent(ctx, instanceID), ) @@ -380,9 +388,10 @@ func setupInstanceEvents(ctx context.Context, instanceID, orgID, projectID, appI func setupInstanceConfig() *InstanceSetup { conf := setupInstanceElementsConfig() conf.Org = InstanceOrgSetup{ - Name: "ZITADEL", - Machine: instanceSetupMachineConfig(), - Human: instanceSetupHumanConfig(), + Name: "ZITADEL", + Machine: instanceSetupMachineConfig(), + Human: instanceSetupHumanConfig(), + LoginClient: instanceSetupLoginClientConfig(), } conf.CustomDomain = "" return conf @@ -541,6 +550,43 @@ func instanceSetupMachineConfig() *AddMachine { } } +func loginClientFilters(orgID string, pat bool) []expect { + filters := []expect{ + expectFilter(), + expectFilter( + org.NewDomainPolicyAddedEvent( + context.Background(), + &org.NewAggregate(orgID).Aggregate, + true, + true, + true, + ), + ), + } + if pat { + filters = append(filters, + expectFilter(), + expectFilter(), + ) + } + return filters +} + +func instanceSetupLoginClientConfig() *AddLoginClient { + return &AddLoginClient{ + Machine: &Machine{ + Username: "zitadel-login-client", + Name: "ZITADEL-login-client", + Description: "Login Client", + AccessTokenType: domain.OIDCTokenTypeBearer, + }, + Pat: &AddPat{ + ExpirationDate: time.Time{}, + Scopes: nil, + }, + } +} + func projectFilters() []expect { return []expect{ expectFilter(), @@ -551,11 +597,23 @@ func projectFilters() []expect { } func adminMemberFilters(orgID, userID string) []expect { + filters := append( + orgMemberFilters(orgID, userID), + instanceMemberFilters(orgID, userID)..., + ) + return filters +} +func orgMemberFilters(orgID, userID string) []expect { return []expect{ expectFilter( addHumanEvent(context.Background(), orgID, userID), ), expectFilter(), + } +} + +func instanceMemberFilters(orgID, userID string) []expect { + return []expect{ expectFilter( addHumanEvent(context.Background(), orgID, userID), ), @@ -631,6 +689,40 @@ func addMachineEvent(ctx context.Context, orgID, userID string) *user.MachineAdd ) } +// loginClientEvents all events from setup to create the login client user +func loginClientEvents(ctx context.Context, instanceID, orgID, userID, patID string) []eventstore.Command { + agg := user.NewAggregate(userID, orgID) + instanceAgg := instance.NewAggregate(instanceID) + events := []eventstore.Command{ + addLoginClientEvent(ctx, orgID, userID), + instance.NewMemberAddedEvent(ctx, &instanceAgg.Aggregate, userID, domain.RoleIAMLoginClient), + } + if patID != "" { + events = append(events, + user.NewPersonalAccessTokenAddedEvent( + ctx, + &agg.Aggregate, + patID, + time.Date(9999, time.December, 31, 23, 59, 59, 0, time.UTC), + nil, + ), + ) + } + return events +} + +func addLoginClientEvent(ctx context.Context, orgID, userID string) *user.MachineAddedEvent { + agg := user.NewAggregate(userID, orgID) + return user.NewMachineAddedEvent(ctx, + &agg.Aggregate, + "zitadel-login-client", + "ZITADEL-login-client", + "Login Client", + false, + domain.OIDCTokenTypeBearer, + ) +} + func testSetup(ctx context.Context, c *Commands, validations []preparation.Validation) error { //nolint:staticcheck cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, validations...) @@ -715,6 +807,13 @@ func TestCommandSide_setupMinimalInterfaces(t *testing.T) { }) } } +func validZitadelRoles() []authz.RoleMapping { + return []authz.RoleMapping{ + {Role: domain.RoleOrgOwner, Permissions: []string{""}}, + {Role: domain.RoleIAMOwner, Permissions: []string{""}}, + {Role: domain.RoleIAMLoginClient, Permissions: []string{""}}, + } +} func TestCommandSide_setupAdmins(t *testing.T) { type fields struct { @@ -730,12 +829,14 @@ func TestCommandSide_setupAdmins(t *testing.T) { orgAgg *org.Aggregate machine *AddMachine human *AddHuman + loginClient *AddLoginClient } type res struct { - owner string - pat bool - machineKey bool - err func(error) bool + owner string + pat bool + machineKey bool + loginClientPat bool + err func(error) bool } tests := []struct { name string @@ -763,10 +864,7 @@ func TestCommandSide_setupAdmins(t *testing.T) { ), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "USER"), userPasswordHasher: mockPasswordHasher("x"), - roles: []authz.RoleMapping{ - {Role: domain.RoleOrgOwner, Permissions: []string{""}}, - {Role: domain.RoleIAMOwner, Permissions: []string{""}}, - }, + roles: validZitadelRoles(), }, args: args{ ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN", language.Dutch), @@ -800,11 +898,8 @@ func TestCommandSide_setupAdmins(t *testing.T) { }, )..., ), - idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "USER-MACHINE", "PAT"), - roles: []authz.RoleMapping{ - {Role: domain.RoleOrgOwner, Permissions: []string{""}}, - {Role: domain.RoleIAMOwner, Permissions: []string{""}}, - }, + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "USER-MACHINE", "PAT"), + roles: validZitadelRoles(), keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), }, args: args{ @@ -850,11 +945,8 @@ func TestCommandSide_setupAdmins(t *testing.T) { ), userPasswordHasher: mockPasswordHasher("x"), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "USER-MACHINE", "PAT", "USER"), - roles: []authz.RoleMapping{ - {Role: domain.RoleOrgOwner, Permissions: []string{""}}, - {Role: domain.RoleIAMOwner, Permissions: []string{""}}, - }, - keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + roles: validZitadelRoles(), + keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), }, args: args{ ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN", language.Dutch), @@ -870,6 +962,63 @@ func TestCommandSide_setupAdmins(t *testing.T) { err: nil, }, }, + { + name: "human, machine and login client, ok", + fields: fields{ + eventstore: expectEventstore( + slices.Concat( + machineFilters("ORG", true), + adminMemberFilters("ORG", "USER-MACHINE"), + humanFilters("ORG"), + adminMemberFilters("ORG", "USER"), + loginClientFilters("ORG", true), + instanceMemberFilters("ORG", "USER-LOGIN-CLIENT"), + []expect{ + expectPush( + slices.Concat( + machineEvents(context.Background(), + "INSTANCE", + "ORG", + "USER-MACHINE", + "PAT", + ), + humanEvents(context.Background(), + "INSTANCE", + "ORG", + "USER", + ), + loginClientEvents(context.Background(), + "INSTANCE", + "ORG", + "USER-LOGIN-CLIENT", + "LOGIN-CLIENT-PAT", + ), + )..., + ), + }, + )..., + ), + userPasswordHasher: mockPasswordHasher("x"), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "USER-MACHINE", "PAT", "USER", "USER-LOGIN-CLIENT", "LOGIN-CLIENT-PAT"), + roles: validZitadelRoles(), + keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args: args{ + ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN", language.Dutch), + instanceAgg: instance.NewAggregate("INSTANCE"), + orgAgg: org.NewAggregate("ORG"), + machine: instanceSetupMachineConfig(), + human: instanceSetupHumanConfig(), + loginClient: instanceSetupLoginClientConfig(), + }, + res: res{ + owner: "USER", + pat: true, + machineKey: false, + loginClientPat: true, + err: nil, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -881,7 +1030,7 @@ func TestCommandSide_setupAdmins(t *testing.T) { keyAlgorithm: tt.fields.keyAlgorithm, } validations := make([]preparation.Validation, 0) - owner, pat, mk, err := setupAdmins(r, &validations, tt.args.instanceAgg, tt.args.orgAgg, tt.args.machine, tt.args.human) + owner, pat, mk, loginClientPat, err := setupAdmins(r, &validations, tt.args.instanceAgg, tt.args.orgAgg, tt.args.machine, tt.args.human, tt.args.loginClient) if tt.res.err == nil { assert.NoError(t, err) } @@ -905,6 +1054,9 @@ func TestCommandSide_setupAdmins(t *testing.T) { if tt.res.machineKey { assert.NotNil(t, mk) } + if tt.res.loginClientPat { + assert.NotNil(t, loginClientPat) + } } }) } @@ -924,12 +1076,14 @@ func TestCommandSide_setupDefaultOrg(t *testing.T) { orgName string machine *AddMachine human *AddHuman + loginClient *AddLoginClient ids ZitadelConfig } type res struct { - pat bool - machineKey bool - err func(error) bool + pat bool + machineKey bool + loginClientPat bool + err func(error) bool } tests := []struct { name string @@ -938,7 +1092,7 @@ func TestCommandSide_setupDefaultOrg(t *testing.T) { res res }{ { - name: "human and machine, ok", + name: "human, machine and login client, ok", fields: fields{ eventstore: expectEventstore( slices.Concat( @@ -946,6 +1100,7 @@ func TestCommandSide_setupDefaultOrg(t *testing.T) { "ORG", true, true, + true, ), []expect{ expectPush( @@ -959,6 +1114,7 @@ func TestCommandSide_setupDefaultOrg(t *testing.T) { false, true, true, + true, ), )..., ), @@ -967,11 +1123,8 @@ func TestCommandSide_setupDefaultOrg(t *testing.T) { ), userPasswordHasher: mockPasswordHasher("x"), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, orgIDs()...), - roles: []authz.RoleMapping{ - {Role: domain.RoleOrgOwner, Permissions: []string{""}}, - {Role: domain.RoleIAMOwner, Permissions: []string{""}}, - }, - keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + roles: validZitadelRoles(), + keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), }, args: args{ ctx: contextWithInstanceSetupInfo(context.Background(), "INSTANCE", "PROJECT", "console-id", "DOMAIN", language.Dutch), @@ -1007,6 +1160,18 @@ func TestCommandSide_setupDefaultOrg(t *testing.T) { Password: "password", PasswordChangeRequired: false, }, + loginClient: &AddLoginClient{ + Machine: &Machine{ + Username: "zitadel-login-client", + Name: "ZITADEL-login-client", + Description: "Login Client", + AccessTokenType: domain.OIDCTokenTypeBearer, + }, + Pat: &AddPat{ + ExpirationDate: time.Time{}, + Scopes: nil, + }, + }, ids: ZitadelConfig{ instanceID: "INSTANCE", orgID: "ORG", @@ -1018,9 +1183,10 @@ func TestCommandSide_setupDefaultOrg(t *testing.T) { }, }, res: res{ - pat: true, - machineKey: false, - err: nil, + pat: true, + machineKey: false, + loginClientPat: true, + err: nil, }, }, } @@ -1034,7 +1200,7 @@ func TestCommandSide_setupDefaultOrg(t *testing.T) { keyAlgorithm: tt.fields.keyAlgorithm, } validations := make([]preparation.Validation, 0) - pat, mk, err := setupDefaultOrg(tt.args.ctx, r, &validations, tt.args.instanceAgg, tt.args.orgName, tt.args.machine, tt.args.human, tt.args.ids) + pat, mk, loginClientPat, err := setupDefaultOrg(tt.args.ctx, r, &validations, tt.args.instanceAgg, tt.args.orgName, tt.args.machine, tt.args.human, tt.args.loginClient, tt.args.ids) if tt.res.err == nil { assert.NoError(t, err) } @@ -1057,6 +1223,9 @@ func TestCommandSide_setupDefaultOrg(t *testing.T) { if tt.res.machineKey { assert.NotNil(t, mk) } + if tt.res.loginClientPat { + assert.NotNil(t, loginClientPat) + } } }) } @@ -1140,9 +1309,10 @@ func TestCommandSide_setUpInstance(t *testing.T) { setup *InstanceSetup } type res struct { - pat bool - machineKey bool - err func(error) bool + pat bool + machineKey bool + loginClientPat bool + err func(error) bool } tests := []struct { name string @@ -1175,11 +1345,8 @@ func TestCommandSide_setUpInstance(t *testing.T) { ), userPasswordHasher: mockPasswordHasher("x"), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, orgIDs()...), - roles: []authz.RoleMapping{ - {Role: domain.RoleOrgOwner, Permissions: []string{""}}, - {Role: domain.RoleIAMOwner, Permissions: []string{""}}, - }, - keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + roles: validZitadelRoles(), + keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), generateDomain: func(string, string) (string, error) { return "DOMAIN", nil }, @@ -1204,7 +1371,7 @@ func TestCommandSide_setUpInstance(t *testing.T) { GenerateDomain: tt.fields.generateDomain, } - validations, pat, mk, err := setUpInstance(tt.args.ctx, r, tt.args.setup) + validations, pat, mk, loginClientPat, err := setUpInstance(tt.args.ctx, r, tt.args.setup) if tt.res.err == nil { assert.NoError(t, err) } @@ -1227,6 +1394,9 @@ func TestCommandSide_setUpInstance(t *testing.T) { if tt.res.machineKey { assert.NotNil(t, mk) } + if tt.res.loginClientPat { + assert.NotNil(t, loginClientPat) + } } }) } @@ -1259,7 +1429,7 @@ func TestCommandSide_UpdateInstance(t *testing.T) { }, args: args{ ctx: authz.WithInstanceID(context.Background(), "INSTANCE"), - name: "", + name: " ", }, res: res{ err: zerrors.IsErrorInvalidArgument, @@ -1406,6 +1576,32 @@ func TestCommandSide_RemoveInstance(t *testing.T) { args args res res }{ + { + name: "instance empty, invalid argument error", + fields: fields{ + eventstore: func(t *testing.T) *eventstore.Eventstore { return &eventstore.Eventstore{} }, + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), " "), + instanceID: " ", + }, + res: res{ + err: zerrors.IsErrorInvalidArgument, + }, + }, + { + name: "instance too long, invalid argument error", + fields: fields{ + eventstore: func(t *testing.T) *eventstore.Eventstore { return &eventstore.Eventstore{} }, + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "averylonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonginstance"), + instanceID: "averylonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonginstance", + }, + res: res{ + err: zerrors.IsErrorInvalidArgument, + }, + }, { name: "instance not existing, not found error", fields: fields{ diff --git a/internal/command/instance_trusted_domain.go b/internal/command/instance_trusted_domain.go index f404e6665a..7d3e6abeb1 100644 --- a/internal/command/instance_trusted_domain.go +++ b/internal/command/instance_trusted_domain.go @@ -34,6 +34,14 @@ func (c *Commands) AddTrustedDomain(ctx context.Context, trustedDomain string) ( } func (c *Commands) RemoveTrustedDomain(ctx context.Context, trustedDomain string) (*domain.ObjectDetails, error) { + trustedDomain = strings.TrimSpace(trustedDomain) + if trustedDomain == "" || len(trustedDomain) > 253 { + return nil, zerrors.ThrowInvalidArgument(nil, "COMMA-ajAzwu", "Errors.Invalid.Argument") + } + if !allowDomainRunes.MatchString(trustedDomain) { + return nil, zerrors.ThrowInvalidArgument(nil, "COMMA-lfs3Te", "Errors.Instance.Domain.InvalidCharacter") + } + model := NewInstanceTrustedDomainsWriteModel(ctx) err := c.eventstore.FilterToQueryReducer(ctx, model) if err != nil { diff --git a/internal/command/instance_trusted_domain_test.go b/internal/command/instance_trusted_domain_test.go index 3caef90f01..4ba9f773ed 100644 --- a/internal/command/instance_trusted_domain_test.go +++ b/internal/command/instance_trusted_domain_test.go @@ -142,6 +142,45 @@ func TestCommands_RemoveTrustedDomain(t *testing.T) { args args want want }{ + { + name: "domain empty string, error", + fields: fields{ + eventstore: func(t *testing.T) *eventstore.Eventstore { return &eventstore.Eventstore{} }, + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instanceID"), + trustedDomain: " ", + }, + want: want{ + err: zerrors.ThrowInvalidArgument(nil, "COMMA-ajAzwu", "Errors.Invalid.Argument"), + }, + }, + { + name: "domain invalid character, error", + fields: fields{ + eventstore: func(t *testing.T) *eventstore.Eventstore { return &eventstore.Eventstore{} }, + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instanceID"), + trustedDomain: "? ", + }, + want: want{ + err: zerrors.ThrowInvalidArgument(nil, "COMMA-lfs3Te", "Errors.Instance.Domain.InvalidCharacter"), + }, + }, + { + name: "domain length exceeded, error", + fields: fields{ + eventstore: func(t *testing.T) *eventstore.Eventstore { return &eventstore.Eventstore{} }, + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instanceID"), + trustedDomain: "averylonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglongdomain", + }, + want: want{ + err: zerrors.ThrowInvalidArgument(nil, "COMMA-ajAzwu", "Errors.Invalid.Argument"), + }, + }, { name: "domain does not exists, error", fields: fields{ diff --git a/internal/command/key_pair.go b/internal/command/key_pair.go index 90eaf7e3da..76193431d6 100644 --- a/internal/command/key_pair.go +++ b/internal/command/key_pair.go @@ -13,31 +13,6 @@ import ( "github.com/zitadel/zitadel/internal/repository/keypair" ) -func (c *Commands) GenerateSigningKeyPair(ctx context.Context, algorithm string) error { - privateCrypto, publicCrypto, err := crypto.GenerateEncryptedKeyPair(c.keySize, c.keyAlgorithm) - if err != nil { - return err - } - keyID, err := c.idGenerator.Next() - if err != nil { - return err - } - - privateKeyExp := time.Now().UTC().Add(c.privateKeyLifetime) - publicKeyExp := time.Now().UTC().Add(c.publicKeyLifetime) - - keyPairWriteModel := NewKeyPairWriteModel(keyID, authz.GetInstance(ctx).InstanceID()) - keyAgg := KeyPairAggregateFromWriteModel(&keyPairWriteModel.WriteModel) - _, err = c.eventstore.Push(ctx, keypair.NewAddedEvent( - ctx, - keyAgg, - crypto.KeyUsageSigning, - algorithm, - privateCrypto, publicCrypto, - privateKeyExp, publicKeyExp)) - return err -} - func (c *Commands) GenerateSAMLCACertificate(ctx context.Context, algorithm string) error { now := time.Now().UTC() after := now.Add(c.certificateLifetime) diff --git a/internal/command/milestone.go b/internal/command/milestone.go index e2f4fdc9de..9ef3393325 100644 --- a/internal/command/milestone.go +++ b/internal/command/milestone.go @@ -133,6 +133,30 @@ func (s *OIDCSessionEvents) SetMilestones(ctx context.Context, clientID string, return postCommit, nil } +func (s *SAMLSessionEvents) SetMilestones(ctx context.Context) (postCommit func(ctx context.Context), err error) { + postCommit = func(ctx context.Context) {} + milestones, err := s.commands.GetMilestonesReached(ctx) + if err != nil { + return postCommit, err + } + + instance := authz.GetInstance(ctx) + aggregate := milestone.NewAggregate(ctx) + var invalidate bool + if !milestones.AuthenticationSucceededOnInstance { + s.events = append(s.events, milestone.NewReachedEvent(ctx, aggregate, milestone.AuthenticationSucceededOnInstance)) + invalidate = true + } + if !milestones.AuthenticationSucceededOnApplication { + s.events = append(s.events, milestone.NewReachedEvent(ctx, aggregate, milestone.AuthenticationSucceededOnApplication)) + invalidate = true + } + if invalidate { + postCommit = s.commands.invalidateMilestoneCachePostCommit(instance.InstanceID()) + } + return postCommit, nil +} + func (c *Commands) projectCreatedMilestone(ctx context.Context, cmds *[]eventstore.Command) (postCommit func(ctx context.Context), err error) { postCommit = func(ctx context.Context) {} if isSystemUser(ctx) { diff --git a/internal/command/milestone_test.go b/internal/command/milestone_test.go index 3c4bffc704..d70f12f9bc 100644 --- a/internal/command/milestone_test.go +++ b/internal/command/milestone_test.go @@ -627,3 +627,84 @@ func TestCommands_applicationCreatedMilestone(t *testing.T) { func (c *Commands) setMilestonesCompletedForTest(instanceID string) { c.milestonesCompleted.Store(instanceID, struct{}{}) } + +func TestSAMLSessionEvents_SetMilestones(t *testing.T) { + ctx := authz.WithInstanceID(context.Background(), "instanceID") + aggregate := milestone.NewAggregate(ctx) + + type fields struct { + eventstore func(*testing.T) *eventstore.Eventstore + } + tests := []struct { + name string + fields fields + wantEvents []eventstore.Command + wantErr error + }{ + { + name: "get error", + fields: fields{ + eventstore: expectEventstore( + expectFilterError(io.ErrClosedPipe), + ), + }, + wantErr: io.ErrClosedPipe, + }, + { + name: "auth on instance, auth on application", + fields: fields{ + eventstore: expectEventstore( + expectFilter(), + ), + }, + wantEvents: []eventstore.Command{ + milestone.NewReachedEvent(ctx, aggregate, milestone.AuthenticationSucceededOnInstance), + milestone.NewReachedEvent(ctx, aggregate, milestone.AuthenticationSucceededOnApplication), + }, + wantErr: nil, + }, + { + name: "auth on app with a previous auth on instance", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(milestone.NewReachedEvent(ctx, aggregate, milestone.AuthenticationSucceededOnInstance)), + ), + ), + }, + wantEvents: []eventstore.Command{ + milestone.NewReachedEvent(ctx, aggregate, milestone.AuthenticationSucceededOnApplication), + }, + wantErr: nil, + }, + { + name: "milestones already reached", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(milestone.NewReachedEvent(ctx, aggregate, milestone.AuthenticationSucceededOnInstance)), + eventFromEventPusher(milestone.NewReachedEvent(ctx, aggregate, milestone.AuthenticationSucceededOnApplication)), + ), + ), + }, + wantErr: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + eventstore: tt.fields.eventstore(t), + caches: &Caches{ + milestones: noop.NewCache[milestoneIndex, string, *MilestonesReached](), + }, + } + s := &SAMLSessionEvents{ + commands: c, + } + postCommit, err := s.SetMilestones(ctx) + postCommit(ctx) + require.ErrorIs(t, err, tt.wantErr) + assert.Equal(t, tt.wantEvents, s.events) + }) + } +} diff --git a/internal/command/org.go b/internal/command/org.go index a018a90c82..215fe0b5cc 100644 --- a/internal/command/org.go +++ b/internal/command/org.go @@ -24,13 +24,20 @@ type InstanceOrgSetup struct { CustomDomain string Human *AddHuman Machine *AddMachine + LoginClient *AddLoginClient Roles []string } +type AddLoginClient struct { + Machine *Machine + Pat *AddPat +} + type OrgSetup struct { Name string CustomDomain string Admins []*OrgSetupAdmin + OrgID string } // OrgSetupAdmin describes a user to be created (Human / Machine) or an existing (ID) to be used for an org setup. @@ -53,7 +60,11 @@ type orgSetupCommands struct { type CreatedOrg struct { ObjectDetails *domain.ObjectDetails - CreatedAdmins []*CreatedOrgAdmin + OrgAdmins []OrgAdmin +} + +type OrgAdmin interface { + GetID() string } type CreatedOrgAdmin struct { @@ -64,6 +75,25 @@ type CreatedOrgAdmin struct { MachineKey *MachineKey } +func (a *CreatedOrgAdmin) GetID() string { + return a.ID +} + +type AssignedOrgAdmin struct { + ID string +} + +func (a *AssignedOrgAdmin) GetID() string { + return a.ID +} + +func (o *OrgSetup) Validate() (err error) { + if o.OrgID != "" && strings.TrimSpace(o.OrgID) == "" { + return zerrors.ThrowInvalidArgument(nil, "ORG-4ABd3", "Errors.Invalid.Argument") + } + return nil +} + func (c *Commands) setUpOrgWithIDs(ctx context.Context, o *OrgSetup, orgID string, allowInitialMail bool, userIDs ...string) (_ *CreatedOrg, err error) { cmds := c.newOrgSetupCommands(ctx, orgID, o) for _, admin := range o.Admins { @@ -93,7 +123,7 @@ func (c *Commands) newOrgSetupCommands(ctx context.Context, orgID string, orgSet func (c *orgSetupCommands) setupOrgAdmin(admin *OrgSetupAdmin, allowInitialMail bool) error { if admin.ID != "" { - c.validations = append(c.validations, c.commands.AddOrgMemberCommand(c.aggregate, admin.ID, orgAdminRoles(admin.Roles)...)) + c.validations = append(c.validations, c.commands.AddOrgMemberCommand(&AddOrgMember{OrgID: c.aggregate.ID, UserID: admin.ID, Roles: orgAdminRoles(admin.Roles)})) return nil } @@ -117,7 +147,7 @@ func (c *orgSetupCommands) setupOrgAdmin(admin *OrgSetupAdmin, allowInitialMail return err } } - c.validations = append(c.validations, c.commands.AddOrgMemberCommand(c.aggregate, userID, orgAdminRoles(admin.Roles)...)) + c.validations = append(c.validations, c.commands.AddOrgMemberCommand(&AddOrgMember{OrgID: c.aggregate.ID, UserID: userID, Roles: orgAdminRoles(admin.Roles)})) return nil } @@ -180,14 +210,15 @@ func (c *orgSetupCommands) push(ctx context.Context) (_ *CreatedOrg, err error) EventDate: events[len(events)-1].CreatedAt(), ResourceOwner: c.aggregate.ID, }, - CreatedAdmins: c.createdAdmins(), + OrgAdmins: c.createdAdmins(), }, nil } -func (c *orgSetupCommands) createdAdmins() []*CreatedOrgAdmin { - users := make([]*CreatedOrgAdmin, 0, len(c.admins)) +func (c *orgSetupCommands) createdAdmins() []OrgAdmin { + users := make([]OrgAdmin, 0, len(c.admins)) for _, admin := range c.admins { if admin.ID != "" && admin.Human == nil { + users = append(users, &AssignedOrgAdmin{ID: admin.ID}) continue } if admin.Human != nil { @@ -233,12 +264,28 @@ func (c *orgSetupCommands) createdMachineAdmin(admin *OrgSetupAdmin) *CreatedOrg } func (c *Commands) SetUpOrg(ctx context.Context, o *OrgSetup, allowInitialMail bool, userIDs ...string) (*CreatedOrg, error) { - orgID, err := c.idGenerator.Next() - if err != nil { + if err := o.Validate(); err != nil { return nil, err } - return c.setUpOrgWithIDs(ctx, o, orgID, allowInitialMail, userIDs...) + if o.OrgID == "" { + var err error + o.OrgID, err = c.idGenerator.Next() + if err != nil { + return nil, err + } + } + + // because users can choose their own ID, we must check that an org with the same ID does not already exist + existingOrg, err := c.getOrgWriteModelByID(ctx, o.OrgID) + if err != nil { + return nil, err + } + if existingOrg.State.Exists() { + return nil, zerrors.ThrowAlreadyExists(nil, "ORG-laho2n", "Errors.Org.AlreadyExisting") + } + + return c.setUpOrgWithIDs(ctx, o, o.OrgID, allowInitialMail, userIDs...) } // AddOrgCommand defines the commands to create a new org, @@ -285,19 +332,20 @@ func (c *Commands) checkOrgExists(ctx context.Context, orgID string) error { return nil } -func (c *Commands) AddOrgWithID(ctx context.Context, name, userID, resourceOwner, orgID string, claimedUserIDs []string) (_ *domain.Org, err error) { +func (c *Commands) AddOrgWithID(ctx context.Context, name, userID, resourceOwner, orgID string, setOrgInactive bool, claimedUserIDs []string) (_ *domain.Org, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() + // because users can choose their own ID, we must check that an org with the same ID does not already exist existingOrg, err := c.getOrgWriteModelByID(ctx, orgID) if err != nil { return nil, err } - if existingOrg.State != domain.OrgStateUnspecified { - return nil, zerrors.ThrowNotFound(nil, "ORG-lapo2m", "Errors.Org.AlreadyExisting") + if existingOrg.State.Exists() { + return nil, zerrors.ThrowAlreadyExists(nil, "ORG-lapo2n", "Errors.Org.AlreadyExisting") } - return c.addOrgWithIDAndMember(ctx, name, userID, resourceOwner, orgID, claimedUserIDs) + return c.addOrgWithIDAndMember(ctx, name, userID, resourceOwner, orgID, setOrgInactive, claimedUserIDs) } func (c *Commands) AddOrg(ctx context.Context, name, userID, resourceOwner string, claimedUserIDs []string) (*domain.Org, error) { @@ -310,10 +358,10 @@ func (c *Commands) AddOrg(ctx context.Context, name, userID, resourceOwner strin return nil, zerrors.ThrowInternal(err, "COMMA-OwciI", "Errors.Internal") } - return c.addOrgWithIDAndMember(ctx, name, userID, resourceOwner, orgID, claimedUserIDs) + return c.addOrgWithIDAndMember(ctx, name, userID, resourceOwner, orgID, false, claimedUserIDs) } -func (c *Commands) addOrgWithIDAndMember(ctx context.Context, name, userID, resourceOwner, orgID string, claimedUserIDs []string) (_ *domain.Org, err error) { +func (c *Commands) addOrgWithIDAndMember(ctx context.Context, name, userID, resourceOwner, orgID string, setOrgInactive bool, claimedUserIDs []string) (_ *domain.Org, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -321,20 +369,24 @@ func (c *Commands) addOrgWithIDAndMember(ctx context.Context, name, userID, reso if err != nil { return nil, err } - err = c.checkUserExists(ctx, userID, resourceOwner) + _, err = c.checkUserExists(ctx, userID, resourceOwner) if err != nil { return nil, err } - addedMember := NewOrgMemberWriteModel(addedOrg.AggregateID, userID) - orgMemberEvent, err := c.addOrgMember(ctx, orgAgg, addedMember, domain.NewMember(orgAgg.ID, userID, domain.RoleOrgOwner)) - if err != nil { + addMember := &AddOrgMember{OrgID: orgAgg.ID, UserID: userID, Roles: []string{domain.RoleOrgOwner}} + if err := addMember.IsValid(c.zitadelRoles); err != nil { return nil, err } - events = append(events, orgMemberEvent) + events = append(events, org.NewMemberAddedEvent(ctx, orgAgg, addMember.UserID, addMember.Roles...)) + if setOrgInactive { + deactivateOrgEvent := org.NewOrgDeactivatedEvent(ctx, orgAgg) + events = append(events, deactivateOrgEvent) + } pushedEvents, err := c.eventstore.Push(ctx, events...) if err != nil { return nil, err } + err = AppendAndReduce(addedOrg, pushedEvents...) if err != nil { return nil, err @@ -453,7 +505,7 @@ func (c *Commands) prepareRemoveOrg(a *org.Aggregate) preparation.Validation { return nil, zerrors.ThrowPreconditionFailed(nil, "COMMA-wG9p1", "Errors.Org.DefaultOrgNotDeletable") } - err := c.checkProjectExists(ctx, instance.ProjectID(), a.ID) + _, err := c.checkProjectExists(ctx, instance.ProjectID(), a.ID) // if there is no error, the ZITADEL project was found on the org to be deleted if err == nil { return nil, zerrors.ThrowPreconditionFailed(err, "COMMA-AF3JW", "Errors.Org.ZitadelOrgNotDeletable") diff --git a/internal/command/org_member.go b/internal/command/org_member.go index ae9bef2151..a384145e50 100644 --- a/internal/command/org_member.go +++ b/internal/command/org_member.go @@ -2,8 +2,9 @@ package command import ( "context" - "reflect" + "slices" + "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/command/preparation" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" @@ -12,29 +13,22 @@ import ( "github.com/zitadel/zitadel/internal/zerrors" ) -func (c *Commands) AddOrgMemberCommand(a *org.Aggregate, userID string, roles ...string) preparation.Validation { +func (c *Commands) AddOrgMemberCommand(member *AddOrgMember) preparation.Validation { return func() (preparation.CreateCommands, error) { - if userID == "" { - return nil, zerrors.ThrowInvalidArgument(nil, "ORG-4Mlfs", "Errors.Invalid.Argument") - } - if len(roles) == 0 { - return nil, zerrors.ThrowInvalidArgument(nil, "V2-PfYhb", "Errors.Invalid.Argument") - } - - if len(domain.CheckForInvalidRoles(roles, domain.OrgRolePrefix, c.zitadelRoles)) > 0 && len(domain.CheckForInvalidRoles(roles, domain.RoleSelfManagementGlobal, c.zitadelRoles)) > 0 { - return nil, zerrors.ThrowInvalidArgument(nil, "Org-4N8es", "Errors.Org.MemberInvalid") + if err := member.IsValid(c.zitadelRoles); err != nil { + return nil, err } return func(ctx context.Context, filter preparation.FilterToQueryReducer) (_ []eventstore.Command, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - if exists, err := ExistsUser(ctx, filter, userID, ""); err != nil || !exists { + if exists, err := ExistsUser(ctx, filter, member.UserID, "", false); err != nil || !exists { return nil, zerrors.ThrowPreconditionFailed(err, "ORG-GoXOn", "Errors.User.NotFound") } - if isMember, err := IsOrgMember(ctx, filter, a.ID, userID); err != nil || isMember { + if isMember, err := IsOrgMember(ctx, filter, member.OrgID, member.UserID); err != nil || isMember { return nil, zerrors.ThrowAlreadyExists(err, "ORG-poWwe", "Errors.Org.Member.AlreadyExists") } - return []eventstore.Command{org.NewMemberAddedEvent(ctx, &a.Aggregate, userID, roles...)}, nil + return []eventstore.Command{org.NewMemberAddedEvent(ctx, &org.NewAggregate(member.OrgID).Aggregate, member.UserID, member.Roles...)}, nil }, nil } @@ -76,12 +70,33 @@ func IsOrgMember(ctx context.Context, filter preparation.FilterToQueryReducer, o return isMember, nil } -func (c *Commands) AddOrgMember(ctx context.Context, orgID, userID string, roles ...string) (_ *domain.Member, err error) { +type AddOrgMember struct { + OrgID string + UserID string + Roles []string +} + +func (m *AddOrgMember) IsValid(zitadelRoles []authz.RoleMapping) error { + if m.UserID == "" || m.OrgID == "" || len(m.Roles) == 0 { + return zerrors.ThrowInvalidArgument(nil, "ORG-4Mlfs", "Errors.Invalid.Argument") + } + if len(domain.CheckForInvalidRoles(m.Roles, domain.OrgRolePrefix, zitadelRoles)) > 0 && len(domain.CheckForInvalidRoles(m.Roles, domain.RoleSelfManagementGlobal, zitadelRoles)) > 0 { + return zerrors.ThrowInvalidArgument(nil, "Org-4N8es", "Errors.Org.MemberInvalid") + } + return nil +} + +func (c *Commands) AddOrgMember(ctx context.Context, member *AddOrgMember) (_ *domain.ObjectDetails, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - - orgAgg := org.NewAggregate(orgID) - cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.AddOrgMemberCommand(orgAgg, userID, roles...)) + if err := c.checkOrgExists(ctx, member.OrgID); err != nil { + return nil, err + } + if err := c.checkPermissionUpdateOrgMember(ctx, member.OrgID, member.OrgID); err != nil { + return nil, err + } + //nolint:staticcheck + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.AddOrgMemberCommand(member)) if err != nil { return nil, err } @@ -89,51 +104,59 @@ func (c *Commands) AddOrgMember(ctx context.Context, orgID, userID string, roles if err != nil { return nil, err } - addedMember := NewOrgMemberWriteModel(orgID, userID) + addedMember := NewOrgMemberWriteModel(member.OrgID, member.UserID) err = AppendAndReduce(addedMember, events...) if err != nil { return nil, err } - return memberWriteModelToMember(&addedMember.MemberWriteModel), nil + return writeModelToObjectDetails(&addedMember.WriteModel), nil } -func (c *Commands) addOrgMember(ctx context.Context, orgAgg *eventstore.Aggregate, addedMember *OrgMemberWriteModel, member *domain.Member) (eventstore.Command, error) { - if !member.IsValid() { - return nil, zerrors.ThrowInvalidArgument(nil, "Org-W8m4l", "Errors.Org.MemberInvalid") +type ChangeOrgMember struct { + OrgID string + UserID string + Roles []string +} + +func (c *ChangeOrgMember) IsValid(zitadelRoles []authz.RoleMapping) error { + if c.OrgID == "" || c.UserID == "" || len(c.Roles) == 0 { + return zerrors.ThrowInvalidArgument(nil, "Org-LiaZi", "Errors.Org.MemberInvalid") } - if len(domain.CheckForInvalidRoles(member.Roles, domain.OrgRolePrefix, c.zitadelRoles)) > 0 && len(domain.CheckForInvalidRoles(member.Roles, domain.RoleSelfManagementGlobal, c.zitadelRoles)) > 0 { - return nil, zerrors.ThrowInvalidArgument(nil, "Org-4N8es", "Errors.Org.MemberInvalid") - } - err := c.eventstore.FilterToQueryReducer(ctx, addedMember) - if err != nil { - return nil, err - } - if addedMember.State == domain.MemberStateActive { - return nil, zerrors.ThrowAlreadyExists(nil, "Org-PtXi1", "Errors.Org.Member.AlreadyExists") + if len(domain.CheckForInvalidRoles(c.Roles, domain.OrgRolePrefix, zitadelRoles)) > 0 { + return zerrors.ThrowInvalidArgument(nil, "IAM-m9fG8", "Errors.Org.MemberInvalid") } - return org.NewMemberAddedEvent(ctx, orgAgg, member.UserID, member.Roles...), nil + return nil } // ChangeOrgMember updates an existing member -func (c *Commands) ChangeOrgMember(ctx context.Context, member *domain.Member) (*domain.Member, error) { - if !member.IsValid() { - return nil, zerrors.ThrowInvalidArgument(nil, "Org-LiaZi", "Errors.Org.MemberInvalid") - } - if len(domain.CheckForInvalidRoles(member.Roles, domain.OrgRolePrefix, c.zitadelRoles)) > 0 { - return nil, zerrors.ThrowInvalidArgument(nil, "IAM-m9fG8", "Errors.Org.MemberInvalid") - } - - existingMember, err := c.orgMemberWriteModelByID(ctx, member.AggregateID, member.UserID) - if err != nil { +func (c *Commands) ChangeOrgMember(ctx context.Context, member *ChangeOrgMember) (*domain.ObjectDetails, error) { + if err := member.IsValid(c.zitadelRoles); err != nil { return nil, err } - if reflect.DeepEqual(existingMember.Roles, member.Roles) { - return nil, zerrors.ThrowPreconditionFailed(nil, "Org-LiaZi", "Errors.Org.Member.RolesNotChanged") + existingMember, err := c.orgMemberWriteModelByID(ctx, member.OrgID, member.UserID) + if err != nil { + return nil, err } - orgAgg := OrgAggregateFromWriteModel(&existingMember.MemberWriteModel.WriteModel) - pushedEvents, err := c.eventstore.Push(ctx, org.NewMemberChangedEvent(ctx, orgAgg, member.UserID, member.Roles...)) + if !existingMember.State.Exists() { + return nil, zerrors.ThrowNotFound(nil, "Org-D8JxR", "Errors.NotFound") + } + if err := c.checkPermissionUpdateOrgMember(ctx, existingMember.ResourceOwner, existingMember.AggregateID); err != nil { + return nil, err + } + + if slices.Compare(existingMember.Roles, member.Roles) == 0 { + return writeModelToObjectDetails(&existingMember.WriteModel), nil + } + + pushedEvents, err := c.eventstore.Push(ctx, + org.NewMemberChangedEvent(ctx, + OrgAggregateFromWriteModelWithCTX(ctx, &existingMember.WriteModel), + member.UserID, + member.Roles..., + ), + ) if err != nil { return nil, err } @@ -142,30 +165,39 @@ func (c *Commands) ChangeOrgMember(ctx context.Context, member *domain.Member) ( return nil, err } - return memberWriteModelToMember(&existingMember.MemberWriteModel), nil + return writeModelToObjectDetails(&existingMember.WriteModel), nil } func (c *Commands) RemoveOrgMember(ctx context.Context, orgID, userID string) (*domain.ObjectDetails, error) { - m, err := c.orgMemberWriteModelByID(ctx, orgID, userID) - if err != nil && !zerrors.IsNotFound(err) { + if orgID == "" || userID == "" { + return nil, zerrors.ThrowInvalidArgument(nil, "Org-LiaZi", "Errors.Org.MemberInvalid") + } + existingMember, err := c.orgMemberWriteModelByID(ctx, orgID, userID) + if err != nil { return nil, err } - if zerrors.IsNotFound(err) { - // empty response because we have no data that match the request - return &domain.ObjectDetails{}, nil + if !existingMember.State.Exists() { + return writeModelToObjectDetails(&existingMember.WriteModel), nil + } + if err := c.checkPermissionDeleteOrgMember(ctx, existingMember.ResourceOwner, existingMember.AggregateID); err != nil { + return nil, err } - orgAgg := OrgAggregateFromWriteModel(&m.MemberWriteModel.WriteModel) - removeEvent := c.removeOrgMember(ctx, orgAgg, userID, false) - pushedEvents, err := c.eventstore.Push(ctx, removeEvent) + pushedEvents, err := c.eventstore.Push(ctx, + c.removeOrgMember(ctx, + OrgAggregateFromWriteModelWithCTX(ctx, &existingMember.WriteModel), + userID, + false, + ), + ) if err != nil { return nil, err } - err = AppendAndReduce(m, pushedEvents...) + err = AppendAndReduce(existingMember, pushedEvents...) if err != nil { return nil, err } - return writeModelToObjectDetails(&m.WriteModel), nil + return writeModelToObjectDetails(&existingMember.WriteModel), nil } func (c *Commands) removeOrgMember(ctx context.Context, orgAgg *eventstore.Aggregate, userID string, cascade bool) eventstore.Command { @@ -189,9 +221,5 @@ func (c *Commands) orgMemberWriteModelByID(ctx context.Context, orgID, userID st return nil, err } - if writeModel.State == domain.MemberStateUnspecified || writeModel.State == domain.MemberStateRemoved { - return nil, zerrors.ThrowNotFound(nil, "Org-D8JxR", "Errors.NotFound") - } - return writeModel, nil } diff --git a/internal/command/org_member_test.go b/internal/command/org_member_test.go index 4e2e926b52..25dd3b818a 100644 --- a/internal/command/org_member_test.go +++ b/internal/command/org_member_test.go @@ -11,24 +11,19 @@ import ( "github.com/zitadel/zitadel/internal/command/preparation" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" - "github.com/zitadel/zitadel/internal/eventstore/v1/models" "github.com/zitadel/zitadel/internal/repository/org" - "github.com/zitadel/zitadel/internal/repository/project" "github.com/zitadel/zitadel/internal/repository/user" "github.com/zitadel/zitadel/internal/zerrors" ) func TestAddMember(t *testing.T) { type args struct { - a *org.Aggregate - userID string - roles []string + member *AddOrgMember zitadelRoles []authz.RoleMapping filter preparation.FilterToQueryReducer } ctx := context.Background() - agg := org.NewAggregate("test") tests := []struct { name string @@ -38,8 +33,10 @@ func TestAddMember(t *testing.T) { { name: "no user id", args: args{ - a: agg, - userID: "", + member: &AddOrgMember{ + OrgID: "test", + UserID: "", + }, }, want: Want{ ValidationErr: zerrors.ThrowInvalidArgument(nil, "ORG-4Mlfs", "Errors.Invalid.Argument"), @@ -48,19 +45,23 @@ func TestAddMember(t *testing.T) { { name: "no roles", args: args{ - a: agg, - userID: "12342", + member: &AddOrgMember{ + OrgID: "test", + UserID: "12342", + }, }, want: Want{ - ValidationErr: zerrors.ThrowInvalidArgument(nil, "V2-PfYhb", "Errors.Invalid.Argument"), + ValidationErr: zerrors.ThrowInvalidArgument(nil, "ORG-4Mlfs", "Errors.Invalid.Argument"), }, }, { name: "TODO: invalid roles", args: args{ - a: agg, - userID: "123", - roles: []string{"ORG_OWNER"}, + member: &AddOrgMember{ + OrgID: "test", + UserID: "12342", + Roles: []string{"ORG_OWNER"}, + }, }, want: Want{ ValidationErr: zerrors.ThrowInvalidArgument(nil, "Org-4N8es", ""), @@ -69,9 +70,11 @@ func TestAddMember(t *testing.T) { { name: "user not exists", args: args{ - a: agg, - userID: "userID", - roles: []string{"ORG_OWNER"}, + member: &AddOrgMember{ + OrgID: "test", + UserID: "userID", + Roles: []string{"ORG_OWNER"}, + }, zitadelRoles: []authz.RoleMapping{ { Role: "ORG_OWNER", @@ -89,9 +92,11 @@ func TestAddMember(t *testing.T) { { name: "already member", args: args{ - a: agg, - userID: "userID", - roles: []string{"ORG_OWNER"}, + member: &AddOrgMember{ + OrgID: "test", + UserID: "userID", + Roles: []string{"ORG_OWNER"}, + }, zitadelRoles: []authz.RoleMapping{ { Role: "ORG_OWNER", @@ -129,9 +134,11 @@ func TestAddMember(t *testing.T) { { name: "correct", args: args{ - a: agg, - userID: "userID", - roles: []string{"ORG_OWNER"}, + member: &AddOrgMember{ + OrgID: "test", + UserID: "userID", + Roles: []string{"ORG_OWNER"}, + }, zitadelRoles: []authz.RoleMapping{ { Role: "ORG_OWNER", @@ -158,14 +165,14 @@ func TestAddMember(t *testing.T) { }, want: Want{ Commands: []eventstore.Command{ - org.NewMemberAddedEvent(ctx, &agg.Aggregate, "userID", "ORG_OWNER"), + org.NewMemberAddedEvent(ctx, &org.NewAggregate("test").Aggregate, "userID", "ORG_OWNER"), }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - AssertValidation(t, context.Background(), (&Commands{zitadelRoles: tt.args.zitadelRoles}).AddOrgMemberCommand(tt.args.a, tt.args.userID, tt.args.roles...), tt.args.filter, tt.want) + AssertValidation(t, context.Background(), (&Commands{zitadelRoles: tt.args.zitadelRoles}).AddOrgMemberCommand(tt.args.member), tt.args.filter, tt.want) }) } } @@ -287,17 +294,15 @@ func TestIsMember(t *testing.T) { func TestCommandSide_AddOrgMember(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore - zitadelRoles []authz.RoleMapping + checkPermission domain.PermissionCheck + eventstore func(t *testing.T) *eventstore.Eventstore + zitadelRoles []authz.RoleMapping } type args struct { - ctx context.Context - userID string - orgID string - roles []string + member *AddOrgMember } type res struct { - want *domain.Member + want *domain.ObjectDetails err func(error) bool } tests := []struct { @@ -309,13 +314,22 @@ func TestCommandSide_AddOrgMember(t *testing.T) { { name: "invalid member, error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "org", + ), + ), + ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - ctx: context.Background(), - orgID: "org1", + member: &AddOrgMember{ + OrgID: "org1", + }, }, res: res{ err: zerrors.IsErrorInvalidArgument, @@ -324,15 +338,24 @@ func TestCommandSide_AddOrgMember(t *testing.T) { { name: "invalid roles, error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "org", + ), + ), + ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - ctx: context.Background(), - orgID: "org1", - userID: "user1", - roles: []string{"ORG_OWNER"}, + member: &AddOrgMember{ + OrgID: "org1", + UserID: "user1", + Roles: []string{"ORG_OWNER"}, + }, }, res: res{ err: zerrors.IsErrorInvalidArgument, @@ -341,10 +364,18 @@ func TestCommandSide_AddOrgMember(t *testing.T) { { name: "user not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "org", + ), + ), + ), expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), zitadelRoles: []authz.RoleMapping{ { Role: domain.RoleOrgOwner, @@ -352,10 +383,11 @@ func TestCommandSide_AddOrgMember(t *testing.T) { }, }, args: args{ - ctx: context.Background(), - orgID: "org1", - userID: "user1", - roles: []string{domain.RoleOrgOwner}, + member: &AddOrgMember{ + OrgID: "org1", + UserID: "user1", + Roles: []string{"ORG_OWNER"}, + }, }, res: res{ err: zerrors.IsPreconditionFailed, @@ -364,8 +396,15 @@ func TestCommandSide_AddOrgMember(t *testing.T) { { name: "member already exists, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "org", + ), + ), + ), expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -391,6 +430,7 @@ func TestCommandSide_AddOrgMember(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), zitadelRoles: []authz.RoleMapping{ { Role: domain.RoleOrgOwner, @@ -398,10 +438,11 @@ func TestCommandSide_AddOrgMember(t *testing.T) { }, }, args: args{ - ctx: context.Background(), - orgID: "org1", - userID: "user1", - roles: []string{"ORG_OWNER"}, + member: &AddOrgMember{ + OrgID: "org1", + UserID: "user1", + Roles: []string{"ORG_OWNER"}, + }, }, res: res{ err: zerrors.IsErrorAlreadyExists, @@ -410,8 +451,15 @@ func TestCommandSide_AddOrgMember(t *testing.T) { { name: "member add uniqueconstraint err, already exists", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "org", + ), + ), + ), expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -437,6 +485,7 @@ func TestCommandSide_AddOrgMember(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), zitadelRoles: []authz.RoleMapping{ { Role: domain.RoleOrgOwner, @@ -444,10 +493,11 @@ func TestCommandSide_AddOrgMember(t *testing.T) { }, }, args: args{ - ctx: context.Background(), - orgID: "org1", - userID: "user1", - roles: []string{"ORG_OWNER"}, + member: &AddOrgMember{ + OrgID: "org1", + UserID: "user1", + Roles: []string{"ORG_OWNER"}, + }, }, res: res{ err: zerrors.IsErrorAlreadyExists, @@ -456,8 +506,15 @@ func TestCommandSide_AddOrgMember(t *testing.T) { { name: "member add, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "org", + ), + ), + ), expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -483,6 +540,7 @@ func TestCommandSide_AddOrgMember(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), zitadelRoles: []authz.RoleMapping{ { Role: domain.RoleOrgOwner, @@ -490,30 +548,58 @@ func TestCommandSide_AddOrgMember(t *testing.T) { }, }, args: args{ - ctx: context.Background(), - orgID: "org1", - userID: "user1", - roles: []string{"ORG_OWNER"}, + member: &AddOrgMember{ + OrgID: "org1", + UserID: "user1", + Roles: []string{"ORG_OWNER"}, + }, }, res: res{ - want: &domain.Member{ - ObjectRoot: models.ObjectRoot{ - ResourceOwner: "org1", - AggregateID: "org1", - }, - UserID: "user1", - Roles: []string{domain.RoleOrgOwner}, + want: &domain.ObjectDetails{ + ResourceOwner: "org1", }, }, }, + { + name: "member add, no permission", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "org", + ), + ), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + zitadelRoles: []authz.RoleMapping{ + { + Role: domain.RoleOrgOwner, + }, + }, + }, + args: args{ + member: &AddOrgMember{ + OrgID: "org1", + UserID: "user1", + Roles: []string{"ORG_OWNER"}, + }, + }, + res: res{ + err: zerrors.IsPermissionDenied, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, - zitadelRoles: tt.fields.zitadelRoles, + eventstore: tt.fields.eventstore(t), + zitadelRoles: tt.fields.zitadelRoles, + checkPermission: tt.fields.checkPermission, } - got, err := r.AddOrgMember(tt.args.ctx, tt.args.orgID, tt.args.userID, tt.args.roles...) + got, err := r.AddOrgMember(context.Background(), tt.args.member) if tt.res.err == nil { assert.NoError(t, err) } @@ -521,7 +607,7 @@ func TestCommandSide_AddOrgMember(t *testing.T) { t.Errorf("got wrong err: %v ", err) } if tt.res.err == nil { - assert.Equal(t, tt.res.want, got) + assertObjectDetails(t, tt.res.want, got) } }) } @@ -529,15 +615,15 @@ func TestCommandSide_AddOrgMember(t *testing.T) { func TestCommandSide_ChangeOrgMember(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore - zitadelRoles []authz.RoleMapping + checkPermission domain.PermissionCheck + eventstore func(t *testing.T) *eventstore.Eventstore + zitadelRoles []authz.RoleMapping } type args struct { - ctx context.Context - member *domain.Member + member *ChangeOrgMember } type res struct { - want *domain.Member + want *domain.ObjectDetails err func(error) bool } tests := []struct { @@ -549,16 +635,12 @@ func TestCommandSide_ChangeOrgMember(t *testing.T) { { name: "invalid member, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - ctx: context.Background(), - member: &domain.Member{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "org1", - }, + member: &ChangeOrgMember{ + OrgID: "org1", }, }, res: res{ @@ -568,16 +650,12 @@ func TestCommandSide_ChangeOrgMember(t *testing.T) { { name: "invalid roles, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - ctx: context.Background(), - member: &domain.Member{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "org1", - }, + member: &ChangeOrgMember{ + OrgID: "org1", UserID: "user1", Roles: []string{"PROJECT_OWNER"}, }, @@ -589,10 +667,10 @@ func TestCommandSide_ChangeOrgMember(t *testing.T) { { name: "member not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), zitadelRoles: []authz.RoleMapping{ { Role: domain.RoleOrgOwner, @@ -600,11 +678,8 @@ func TestCommandSide_ChangeOrgMember(t *testing.T) { }, }, args: args{ - ctx: context.Background(), - member: &domain.Member{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "org1", - }, + member: &ChangeOrgMember{ + OrgID: "org1", UserID: "user1", Roles: []string{"ORG_OWNER"}, }, @@ -614,10 +689,9 @@ func TestCommandSide_ChangeOrgMember(t *testing.T) { }, }, { - name: "member not changed, precondition error", + name: "member not changed, no change", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewMemberAddedEvent(context.Background(), @@ -628,6 +702,7 @@ func TestCommandSide_ChangeOrgMember(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), zitadelRoles: []authz.RoleMapping{ { Role: domain.RoleOrgOwner, @@ -635,24 +710,22 @@ func TestCommandSide_ChangeOrgMember(t *testing.T) { }, }, args: args{ - ctx: context.Background(), - member: &domain.Member{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "org1", - }, + member: &ChangeOrgMember{ + OrgID: "org1", UserID: "user1", Roles: []string{"ORG_OWNER"}, }, }, res: res{ - err: zerrors.IsPreconditionFailed, + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, }, }, { name: "member change, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewMemberAddedEvent(context.Background(), @@ -670,6 +743,7 @@ func TestCommandSide_ChangeOrgMember(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), zitadelRoles: []authz.RoleMapping{ { Role: "ORG_OWNER", @@ -680,160 +754,210 @@ func TestCommandSide_ChangeOrgMember(t *testing.T) { }, }, args: args{ - ctx: context.Background(), - member: &domain.Member{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "org1", - }, + member: &ChangeOrgMember{ + OrgID: "org1", UserID: "user1", Roles: []string{"ORG_OWNER", "ORG_OWNER_VIEWER"}, }, }, - res: res{ - want: &domain.Member{ - ObjectRoot: models.ObjectRoot{ - ResourceOwner: "org1", - AggregateID: "org1", - }, - UserID: "user1", - Roles: []string{"ORG_OWNER", "ORG_OWNER_VIEWER"}, - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - r := &Commands{ - eventstore: tt.fields.eventstore, - zitadelRoles: tt.fields.zitadelRoles, - } - got, err := r.ChangeOrgMember(tt.args.ctx, tt.args.member) - if tt.res.err == nil { - assert.NoError(t, err) - } - if tt.res.err != nil && !tt.res.err(err) { - t.Errorf("got wrong err: %v ", err) - } - if tt.res.err == nil { - assert.Equal(t, tt.res.want, got) - } - }) - } -} - -func TestCommandSide_RemoveOrgMember(t *testing.T) { - type fields struct { - eventstore *eventstore.Eventstore - } - type args struct { - ctx context.Context - projectID string - userID string - resourceOwner string - } - type res struct { - want *domain.ObjectDetails - err func(error) bool - } - tests := []struct { - name string - fields fields - args args - res res - }{ - { - name: "invalid member projectid missing, error", - fields: fields{ - eventstore: eventstoreExpect( - t, - ), - }, - args: args{ - ctx: context.Background(), - projectID: "", - userID: "user1", - resourceOwner: "org1", - }, - res: res{ - err: zerrors.IsErrorInvalidArgument, - }, - }, - { - name: "invalid member userid missing, error", - fields: fields{ - eventstore: eventstoreExpect( - t, - ), - }, - args: args{ - ctx: context.Background(), - projectID: "project1", - userID: "", - resourceOwner: "org1", - }, - res: res{ - err: zerrors.IsErrorInvalidArgument, - }, - }, - { - name: "member not existing, empty object details result", - fields: fields{ - eventstore: eventstoreExpect( - t, - expectFilter(), - ), - }, - args: args{ - ctx: context.Background(), - projectID: "project1", - userID: "user1", - resourceOwner: "org1", - }, - res: res{ - want: &domain.ObjectDetails{}, - }, - }, - { - name: "member remove, ok", - fields: fields{ - eventstore: eventstoreExpect( - t, - expectFilter( - eventFromEventPusher( - project.NewProjectMemberAddedEvent(context.Background(), - &project.NewAggregate("project1", "org1").Aggregate, - "user1", - []string{"PROJECT_OWNER"}..., - ), - ), - ), - expectPush( - project.NewProjectMemberRemovedEvent(context.Background(), - &project.NewAggregate("project1", "org1").Aggregate, - "user1", - ), - ), - ), - }, - args: args{ - ctx: context.Background(), - projectID: "project1", - userID: "user1", - resourceOwner: "org1", - }, res: res{ want: &domain.ObjectDetails{ ResourceOwner: "org1", }, }, }, + { + name: "member change, no permission", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + org.NewMemberAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "user1", + []string{"ORG_OWNER"}..., + ), + ), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + zitadelRoles: []authz.RoleMapping{ + { + Role: "ORG_OWNER", + }, + { + Role: "ORG_OWNER_VIEWER", + }, + }, + }, + args: args{ + member: &ChangeOrgMember{ + OrgID: "org1", + UserID: "user1", + Roles: []string{"ORG_OWNER", "ORG_OWNER_VIEWER"}, + }, + }, + res: res{ + err: zerrors.IsPermissionDenied, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), + zitadelRoles: tt.fields.zitadelRoles, + checkPermission: tt.fields.checkPermission, } - got, err := r.RemoveProjectMember(tt.args.ctx, tt.args.projectID, tt.args.userID, tt.args.resourceOwner) + got, err := r.ChangeOrgMember(context.Background(), tt.args.member) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assertObjectDetails(t, tt.res.want, got) + } + }) + } +} + +func TestCommandSide_RemoveOrgMember(t *testing.T) { + type fields struct { + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck + } + type args struct { + ctx context.Context + orgID string + userID string + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "invalid member orgID missing, error", + fields: fields{ + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + orgID: "", + userID: "user1", + }, + res: res{ + err: zerrors.IsErrorInvalidArgument, + }, + }, + { + name: "invalid member userid missing, error", + fields: fields{ + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userID: "", + }, + res: res{ + err: zerrors.IsErrorInvalidArgument, + }, + }, + { + name: "member not existing, empty object details result", + fields: fields{ + eventstore: expectEventstore( + expectFilter(), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userID: "user1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "member remove, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + org.NewMemberAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "user1", + []string{"PROJECT_OWNER"}..., + ), + ), + ), + expectPush( + org.NewMemberRemovedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "user1", + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userID: "user1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "member remove, no permission", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + org.NewMemberAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "user1", + []string{"PROJECT_OWNER"}..., + ), + ), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userID: "user1", + }, + res: res{ + err: zerrors.IsPermissionDenied, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, + } + got, err := r.RemoveOrgMember(tt.args.ctx, tt.args.orgID, tt.args.userID) if tt.res.err == nil { assert.NoError(t, err) } diff --git a/internal/command/org_model.go b/internal/command/org_model.go index 2661af9bd9..9b39805609 100644 --- a/internal/command/org_model.go +++ b/internal/command/org_model.go @@ -1,6 +1,8 @@ package command import ( + "context" + "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/repository/org" @@ -63,3 +65,7 @@ func (wm *OrgWriteModel) Query() *eventstore.SearchQueryBuilder { func OrgAggregateFromWriteModel(wm *eventstore.WriteModel) *eventstore.Aggregate { return eventstore.AggregateFromWriteModel(wm, org.AggregateType, org.AggregateVersion) } + +func OrgAggregateFromWriteModelWithCTX(ctx context.Context, wm *eventstore.WriteModel) *eventstore.Aggregate { + return org.AggregateFromWriteModel(ctx, wm) +} diff --git a/internal/command/org_test.go b/internal/command/org_test.go index 4ec85d61e1..c07d5f7678 100644 --- a/internal/command/org_test.go +++ b/internal/command/org_test.go @@ -73,7 +73,7 @@ func TestAddOrg(t *testing.T) { func TestCommandSide_AddOrg(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore idGenerator id.Generator zitadelRoles []authz.RoleMapping } @@ -97,9 +97,7 @@ func TestCommandSide_AddOrg(t *testing.T) { { name: "invalid org, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ ctx: context.Background(), @@ -113,9 +111,7 @@ func TestCommandSide_AddOrg(t *testing.T) { { name: "invalid org (spaces), error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ ctx: context.Background(), @@ -130,8 +126,7 @@ func TestCommandSide_AddOrg(t *testing.T) { { name: "user removed, error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilterOrgDomainNotFound(), expectFilter( eventFromEventPusher( @@ -174,8 +169,7 @@ func TestCommandSide_AddOrg(t *testing.T) { { name: "push failed unique constraint, error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilterOrgDomainNotFound(), expectFilter( eventFromEventPusher( @@ -193,7 +187,6 @@ func TestCommandSide_AddOrg(t *testing.T) { ), ), ), - expectFilterOrgMemberNotFound(), expectPushFailed(zerrors.ThrowAlreadyExists(nil, "id", "internal"), org.NewOrgAddedEvent( context.Background(), @@ -242,8 +235,7 @@ func TestCommandSide_AddOrg(t *testing.T) { { name: "push failed, error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilterOrgDomainNotFound(), expectFilter( eventFromEventPusher( @@ -261,7 +253,6 @@ func TestCommandSide_AddOrg(t *testing.T) { ), ), ), - expectFilterOrgMemberNotFound(), expectPushFailed(zerrors.ThrowInternal(nil, "id", "internal"), org.NewOrgAddedEvent( context.Background(), @@ -310,8 +301,7 @@ func TestCommandSide_AddOrg(t *testing.T) { { name: "add org, no error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilterOrgDomainNotFound(), expectFilter( eventFromEventPusher( @@ -329,7 +319,6 @@ func TestCommandSide_AddOrg(t *testing.T) { ), ), ), - expectFilterOrgMemberNotFound(), expectPush( org.NewOrgAddedEvent(context.Background(), &org.NewAggregate("org2").Aggregate, @@ -381,8 +370,7 @@ func TestCommandSide_AddOrg(t *testing.T) { { name: "add org (remove spaces), no error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilterOrgDomainNotFound(), expectFilter( eventFromEventPusher( @@ -400,7 +388,6 @@ func TestCommandSide_AddOrg(t *testing.T) { ), ), ), - expectFilterOrgMemberNotFound(), expectPush( org.NewOrgAddedEvent(context.Background(), &org.NewAggregate("org2").Aggregate, @@ -453,7 +440,7 @@ func TestCommandSide_AddOrg(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), idGenerator: tt.fields.idGenerator, zitadelRoles: tt.fields.zitadelRoles, } @@ -473,7 +460,7 @@ func TestCommandSide_AddOrg(t *testing.T) { func TestCommandSide_ChangeOrg(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore } type args struct { ctx context.Context @@ -492,9 +479,7 @@ func TestCommandSide_ChangeOrg(t *testing.T) { { name: "empty name, invalid argument error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ ctx: context.Background(), @@ -507,9 +492,7 @@ func TestCommandSide_ChangeOrg(t *testing.T) { { name: "empty name (spaces), invalid argument error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ ctx: context.Background(), @@ -523,8 +506,7 @@ func TestCommandSide_ChangeOrg(t *testing.T) { { name: "org not found, error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -540,8 +522,7 @@ func TestCommandSide_ChangeOrg(t *testing.T) { { name: "no change (spaces), error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewOrgAddedEvent(context.Background(), @@ -563,8 +544,7 @@ func TestCommandSide_ChangeOrg(t *testing.T) { { name: "push failed, error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewOrgAddedEvent(context.Background(), @@ -593,8 +573,7 @@ func TestCommandSide_ChangeOrg(t *testing.T) { { name: "change org name verified, not primary", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewOrgAddedEvent(context.Background(), @@ -645,8 +624,7 @@ func TestCommandSide_ChangeOrg(t *testing.T) { { name: "change org name verified, with primary", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewOrgAddedEvent(context.Background(), @@ -705,8 +683,7 @@ func TestCommandSide_ChangeOrg(t *testing.T) { { name: "change org name case verified, with primary", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewOrgAddedEvent(context.Background(), @@ -754,7 +731,7 @@ func TestCommandSide_ChangeOrg(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), } _, err := r.ChangeOrg(tt.args.ctx, tt.args.orgID, tt.args.name) if tt.res.err == nil { @@ -769,7 +746,7 @@ func TestCommandSide_ChangeOrg(t *testing.T) { func TestCommandSide_DeactivateOrg(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore idGenerator id.Generator iamDomain string } @@ -790,8 +767,7 @@ func TestCommandSide_DeactivateOrg(t *testing.T) { { name: "org not found, error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -806,8 +782,7 @@ func TestCommandSide_DeactivateOrg(t *testing.T) { { name: "org already inactive, error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewOrgAddedEvent(context.Background(), @@ -832,8 +807,7 @@ func TestCommandSide_DeactivateOrg(t *testing.T) { { name: "push failed, error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewOrgAddedEvent(context.Background(), @@ -860,8 +834,7 @@ func TestCommandSide_DeactivateOrg(t *testing.T) { { name: "deactivate org", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewOrgAddedEvent(context.Background(), @@ -886,7 +859,7 @@ func TestCommandSide_DeactivateOrg(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), idGenerator: tt.fields.idGenerator, } _, err := r.DeactivateOrg(tt.args.ctx, tt.args.orgID) @@ -902,7 +875,7 @@ func TestCommandSide_DeactivateOrg(t *testing.T) { func TestCommandSide_ReactivateOrg(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore idGenerator id.Generator iamDomain string } @@ -923,8 +896,7 @@ func TestCommandSide_ReactivateOrg(t *testing.T) { { name: "org not found, error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -939,8 +911,7 @@ func TestCommandSide_ReactivateOrg(t *testing.T) { { name: "org already active, error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewOrgAddedEvent(context.Background(), @@ -961,8 +932,7 @@ func TestCommandSide_ReactivateOrg(t *testing.T) { { name: "push failed, error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewOrgAddedEvent(context.Background(), @@ -994,8 +964,7 @@ func TestCommandSide_ReactivateOrg(t *testing.T) { { name: "reactivate org", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( org.NewOrgAddedEvent(context.Background(), @@ -1024,7 +993,7 @@ func TestCommandSide_ReactivateOrg(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), idGenerator: tt.fields.idGenerator, } _, err := r.ReactivateOrg(tt.args.ctx, tt.args.orgID) @@ -1040,7 +1009,7 @@ func TestCommandSide_ReactivateOrg(t *testing.T) { func TestCommandSide_RemoveOrg(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore idGenerator id.Generator } type args struct { @@ -1059,9 +1028,7 @@ func TestCommandSide_RemoveOrg(t *testing.T) { { name: "default org, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ ctx: authz.WithInstance(context.Background(), &mockInstance{}), @@ -1074,8 +1041,7 @@ func TestCommandSide_RemoveOrg(t *testing.T) { { name: "zitadel org, error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -1100,8 +1066,7 @@ func TestCommandSide_RemoveOrg(t *testing.T) { { name: "org not found, error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), // zitadel project check expectFilter(), ), @@ -1117,8 +1082,7 @@ func TestCommandSide_RemoveOrg(t *testing.T) { { name: "push failed, error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), // zitadel project check expectFilter( eventFromEventPusher( @@ -1160,8 +1124,7 @@ func TestCommandSide_RemoveOrg(t *testing.T) { { name: "remove org", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), // zitadel project check expectFilter( eventFromEventPusher( @@ -1200,8 +1163,7 @@ func TestCommandSide_RemoveOrg(t *testing.T) { { name: "remove org with usernames and domains", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), // zitadel project check expectFilter( eventFromEventPusher( @@ -1291,7 +1253,7 @@ func TestCommandSide_RemoveOrg(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), idGenerator: tt.fields.idGenerator, } _, err := r.RemoveOrg(tt.args.ctx, tt.args.orgID) @@ -1331,7 +1293,9 @@ func TestCommandSide_SetUpOrg(t *testing.T) { { name: "org name empty, error", fields: fields{ - eventstore: expectEventstore(), + eventstore: expectEventstore( + expectFilter(), // org already exists check + ), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "orgID"), }, args: args{ @@ -1344,10 +1308,27 @@ func TestCommandSide_SetUpOrg(t *testing.T) { err: zerrors.ThrowInvalidArgument(nil, "ORG-mruNY", "Errors.Invalid.Argument"), }, }, + { + name: "org id empty, error", + fields: fields{ + eventstore: expectEventstore(), + }, + args: args{ + ctx: http_util.WithRequestedHost(context.Background(), "iam-domain"), + setupOrg: &OrgSetup{ + Name: "Org", + OrgID: " ", + }, + }, + res: res{ + err: zerrors.ThrowInvalidArgument(nil, "ORG-4ABd3", "Errors.Invalid.Argument"), + }, + }, { name: "userID not existing, error", fields: fields{ eventstore: expectEventstore( + expectFilter(), // org already exists check expectFilter(), ), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "orgID"), @@ -1370,7 +1351,9 @@ func TestCommandSide_SetUpOrg(t *testing.T) { { name: "human invalid, error", fields: fields{ - eventstore: expectEventstore(), + eventstore: expectEventstore( + expectFilter(), // org already exists check + ), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "orgID", "userID"), }, args: args{ @@ -1403,6 +1386,7 @@ func TestCommandSide_SetUpOrg(t *testing.T) { fields: fields{ eventstore: expectEventstore( expectFilter(), // add human exists check + expectFilter(), expectFilter( eventFromEventPusher( org.NewDomainPolicyAddedEvent(context.Background(), @@ -1515,18 +1499,130 @@ func TestCommandSide_SetUpOrg(t *testing.T) { ObjectDetails: &domain.ObjectDetails{ ResourceOwner: "orgID", }, - CreatedAdmins: []*CreatedOrgAdmin{ - { + OrgAdmins: []OrgAdmin{ + &CreatedOrgAdmin{ ID: "userID", }, }, }, }, }, + { + name: "org already exists", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &org.NewAggregate("custom-org-ID").Aggregate, "Org"), + ), + ), + ), + }, + args: args{ + ctx: http_util.WithRequestedHost(context.Background(), "iam-domain"), + setupOrg: &OrgSetup{ + Name: "Org", + OrgID: "custom-org-ID", + }, + }, + res: res{ + err: zerrors.ThrowAlreadyExists(nil, "ORG-laho2n", "Errors.Org.AlreadyExisting"), + }, + }, + { + name: "org with same id deleted", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &org.NewAggregate("custom-org-ID").Aggregate, "Org"), + ), + org.NewOrgRemovedEvent( + context.Background(), &org.NewAggregate("custom-org-ID").Aggregate, + "Org", []string{}, false, []string{}, []*domain.UserIDPLink{}, []string{}), + ), + expectPush( + eventFromEventPusher(org.NewOrgAddedEvent(context.Background(), + &org.NewAggregate("custom-org-ID").Aggregate, + "Org", + )), + eventFromEventPusher(org.NewDomainAddedEvent(context.Background(), + &org.NewAggregate("custom-org-ID").Aggregate, "org.iam-domain", + )), + eventFromEventPusher(org.NewDomainVerifiedEvent(context.Background(), + &org.NewAggregate("custom-org-ID").Aggregate, + "org.iam-domain", + )), + eventFromEventPusher(org.NewDomainPrimarySetEvent(context.Background(), + &org.NewAggregate("custom-org-ID").Aggregate, + "org.iam-domain", + )), + ), + ), + }, + args: args{ + ctx: http_util.WithRequestedHost(context.Background(), "iam-domain"), + setupOrg: &OrgSetup{ + Name: "Org", + OrgID: "custom-org-ID", + }, + }, + res: res{ + createdOrg: &CreatedOrg{ + ObjectDetails: &domain.ObjectDetails{ + ResourceOwner: "custom-org-ID", + }, + OrgAdmins: []OrgAdmin{}, + }, + }, + }, + { + name: "no human added, custom org ID", + fields: fields{ + eventstore: expectEventstore( + expectFilter(), // org already exists check + expectPush( + eventFromEventPusher(org.NewOrgAddedEvent(context.Background(), + &org.NewAggregate("custom-org-ID").Aggregate, + "Org", + )), + eventFromEventPusher(org.NewDomainAddedEvent(context.Background(), + &org.NewAggregate("custom-org-ID").Aggregate, "org.iam-domain", + )), + eventFromEventPusher(org.NewDomainVerifiedEvent(context.Background(), + &org.NewAggregate("custom-org-ID").Aggregate, + "org.iam-domain", + )), + eventFromEventPusher(org.NewDomainPrimarySetEvent(context.Background(), + &org.NewAggregate("custom-org-ID").Aggregate, + "org.iam-domain", + )), + ), + ), + }, + args: args{ + ctx: http_util.WithRequestedHost(context.Background(), "iam-domain"), + setupOrg: &OrgSetup{ + Name: "Org", + OrgID: "custom-org-ID", + }, + }, + res: res{ + createdOrg: &CreatedOrg{ + ObjectDetails: &domain.ObjectDetails{ + ResourceOwner: "custom-org-ID", + }, + OrgAdmins: []OrgAdmin{}, + }, + }, + }, { name: "existing human added", fields: fields{ eventstore: expectEventstore( + expectFilter(), // org already exists check expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -1586,7 +1682,11 @@ func TestCommandSide_SetUpOrg(t *testing.T) { ObjectDetails: &domain.ObjectDetails{ ResourceOwner: "orgID", }, - CreatedAdmins: []*CreatedOrgAdmin{}, + OrgAdmins: []OrgAdmin{ + &AssignedOrgAdmin{ + ID: "userID", + }, + }, }, }, }, @@ -1595,6 +1695,7 @@ func TestCommandSide_SetUpOrg(t *testing.T) { fields: fields{ eventstore: expectEventstore( expectFilter(), // add machine exists check + expectFilter(), expectFilter( eventFromEventPusher( org.NewDomainPolicyAddedEvent(context.Background(), @@ -1696,8 +1797,8 @@ func TestCommandSide_SetUpOrg(t *testing.T) { ObjectDetails: &domain.ObjectDetails{ ResourceOwner: "orgID", }, - CreatedAdmins: []*CreatedOrgAdmin{ - { + OrgAdmins: []OrgAdmin{ + &CreatedOrgAdmin{ ID: "userID", PAT: &PersonalAccessToken{ ObjectRoot: models.ObjectRoot{ diff --git a/internal/command/permission_checks.go b/internal/command/permission_checks.go new file mode 100644 index 0000000000..128880341a --- /dev/null +++ b/internal/command/permission_checks.go @@ -0,0 +1,158 @@ +package command + +import ( + "context" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/repository/instance" + "github.com/zitadel/zitadel/internal/repository/org" + "github.com/zitadel/zitadel/internal/repository/project" + "github.com/zitadel/zitadel/internal/v2/user" + "github.com/zitadel/zitadel/internal/zerrors" +) + +type PermissionCheck func(resourceOwner, aggregateID string) error + +type UserGrantPermissionCheck func(projectID, projectGrantID string) PermissionCheck + +func (c *Commands) newPermissionCheck(ctx context.Context, permission string, aggregateType eventstore.AggregateType) PermissionCheck { + return func(resourceOwner, aggregateID string) error { + if aggregateID == "" { + return zerrors.ThrowInternal(nil, "COMMAND-ulBlS", "Errors.IDMissing") + } + // For example if a write model didn't query any events, the resource owner is probably empty. + // In this case, we have to query an event on the given aggregate to get the resource owner. + if resourceOwner == "" { + r := NewResourceOwnerModel(authz.GetInstance(ctx).InstanceID(), aggregateType, aggregateID) + err := c.eventstore.FilterToQueryReducer(ctx, r) + if err != nil { + return err + } + resourceOwner = r.resourceOwner + } + if resourceOwner == "" { + return zerrors.ThrowNotFound(nil, "COMMAND-4g3xq", "Errors.NotFound") + } + return c.checkPermission(ctx, permission, resourceOwner, aggregateID) + } +} + +func (c *Commands) checkPermissionOnUser(ctx context.Context, permission string) PermissionCheck { + return func(resourceOwner, aggregateID string) error { + if aggregateID != "" && aggregateID == authz.GetCtxData(ctx).UserID { + return nil + } + return c.newPermissionCheck(ctx, permission, user.AggregateType)(resourceOwner, aggregateID) + } +} + +func (c *Commands) NewPermissionCheckUserWrite(ctx context.Context) PermissionCheck { + return c.checkPermissionOnUser(ctx, domain.PermissionUserWrite) +} + +func (c *Commands) checkPermissionDeleteUser(ctx context.Context, resourceOwner, userID string) error { + return c.checkPermissionOnUser(ctx, domain.PermissionUserDelete)(resourceOwner, userID) +} + +func (c *Commands) checkPermissionUpdateUser(ctx context.Context, resourceOwner, userID string) error { + return c.NewPermissionCheckUserWrite(ctx)(resourceOwner, userID) +} + +func (c *Commands) checkPermissionUpdateUserCredentials(ctx context.Context, resourceOwner, userID string) error { + return c.checkPermissionOnUser(ctx, domain.PermissionUserCredentialWrite)(resourceOwner, userID) +} + +func (c *Commands) checkPermissionDeleteProject(ctx context.Context, resourceOwner, projectID string) error { + return c.newPermissionCheck(ctx, domain.PermissionProjectDelete, project.AggregateType)(resourceOwner, projectID) +} + +func (c *Commands) checkPermissionUpdateProject(ctx context.Context, resourceOwner, projectID string) error { + return c.newPermissionCheck(ctx, domain.PermissionProjectWrite, project.AggregateType)(resourceOwner, projectID) +} + +func (c *Commands) checkPermissionUpdateProjectGrant(ctx context.Context, resourceOwner, projectID, projectGrantID string) (err error) { + if err := c.newPermissionCheck(ctx, domain.PermissionProjectGrantWrite, project.AggregateType)(resourceOwner, projectGrantID); err != nil { + if err := c.newPermissionCheck(ctx, domain.PermissionProjectGrantWrite, project.AggregateType)(resourceOwner, projectID); err != nil { + return err + } + } + return nil +} + +func (c *Commands) checkPermissionDeleteProjectGrant(ctx context.Context, resourceOwner, projectID, projectGrantID string) (err error) { + if err := c.newPermissionCheck(ctx, domain.PermissionProjectGrantDelete, project.AggregateType)(resourceOwner, projectGrantID); err != nil { + if err := c.newPermissionCheck(ctx, domain.PermissionProjectGrantDelete, project.AggregateType)(resourceOwner, projectID); err != nil { + return err + } + } + return nil +} + +func (c *Commands) checkPermissionUpdateApplication(ctx context.Context, resourceOwner, appID string) error { + return c.newPermissionCheck(ctx, domain.PermissionProjectAppWrite, project.AggregateType)(resourceOwner, appID) +} + +func (c *Commands) checkPermissionDeleteApp(ctx context.Context, resourceOwner, appID string) error { + return c.newPermissionCheck(ctx, domain.PermissionProjectAppDelete, project.AggregateType)(resourceOwner, appID) +} + +func (c *Commands) checkPermissionUpdateInstanceMember(ctx context.Context, instanceID string) error { + return c.newPermissionCheck(ctx, domain.PermissionInstanceMemberWrite, instance.AggregateType)(instanceID, instanceID) +} + +func (c *Commands) checkPermissionDeleteInstanceMember(ctx context.Context, instanceID string) error { + return c.newPermissionCheck(ctx, domain.PermissionInstanceMemberDelete, instance.AggregateType)(instanceID, instanceID) +} + +func (c *Commands) checkPermissionUpdateOrgMember(ctx context.Context, instanceID, orgID string) error { + return c.newPermissionCheck(ctx, domain.PermissionOrgMemberWrite, org.AggregateType)(instanceID, orgID) +} +func (c *Commands) checkPermissionDeleteOrgMember(ctx context.Context, instanceID, orgID string) error { + return c.newPermissionCheck(ctx, domain.PermissionOrgMemberDelete, org.AggregateType)(instanceID, orgID) +} + +func (c *Commands) checkPermissionUpdateProjectMember(ctx context.Context, resourceOwner, projectID string) error { + return c.newPermissionCheck(ctx, domain.PermissionProjectMemberWrite, project.AggregateType)(resourceOwner, projectID) +} + +func (c *Commands) checkPermissionDeleteProjectMember(ctx context.Context, resourceOwner, projectID string) error { + return c.newPermissionCheck(ctx, domain.PermissionProjectMemberDelete, project.AggregateType)(resourceOwner, projectID) +} + +func (c *Commands) checkPermissionUpdateProjectGrantMember(ctx context.Context, grantedOrgID, projectGrantID string) (err error) { + // TODO: add permission check for project grant owners + //if err := c.newPermissionCheck(ctx, domain.PermissionProjectGrantMemberWrite, project.AggregateType)(resourceOwner, projectGrantID); err != nil { + return c.newPermissionCheck(ctx, domain.PermissionProjectGrantMemberWrite, project.AggregateType)(grantedOrgID, projectGrantID) + //} + //return nil +} + +func (c *Commands) checkPermissionDeleteProjectGrantMember(ctx context.Context, grantedOrgID, projectGrantID string) (err error) { + // TODO: add permission check for project grant owners + //if err := c.newPermissionCheck(ctx, domain.PermissionProjectGrantMemberDelete, project.AggregateType)(resourceOwner, projectGrantID); err != nil { + return c.newPermissionCheck(ctx, domain.PermissionProjectGrantMemberDelete, project.AggregateType)(grantedOrgID, projectGrantID) + //} + //return nil +} + +func (c *Commands) newUserGrantPermissionCheck(ctx context.Context, permission string) UserGrantPermissionCheck { + check := c.newPermissionCheck(ctx, permission, project.AggregateType) + return func(projectID, projectGrantID string) PermissionCheck { + return func(resourceOwner, _ string) error { + if projectGrantID != "" { + return check(resourceOwner, projectGrantID) + } + return check(resourceOwner, projectID) + } + } +} + +func (c *Commands) NewPermissionCheckUserGrantWrite(ctx context.Context) UserGrantPermissionCheck { + return c.newUserGrantPermissionCheck(ctx, domain.PermissionUserGrantWrite) +} + +func (c *Commands) NewPermissionCheckUserGrantDelete(ctx context.Context) UserGrantPermissionCheck { + return c.newUserGrantPermissionCheck(ctx, domain.PermissionUserGrantDelete) +} diff --git a/internal/command/permission_checks_test.go b/internal/command/permission_checks_test.go new file mode 100644 index 0000000000..5c36dc14f9 --- /dev/null +++ b/internal/command/permission_checks_test.go @@ -0,0 +1,278 @@ +package command + +import ( + "context" + "database/sql" + "errors" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/eventstore/repository" + "github.com/zitadel/zitadel/internal/zerrors" +) + +func TestCommands_CheckPermission(t *testing.T) { + type fields struct { + eventstore func(*testing.T) *eventstore.Eventstore + domainPermissionCheck func(*testing.T) domain.PermissionCheck + } + type args struct { + ctx context.Context + permission string + aggregateType eventstore.AggregateType + resourceOwner, aggregateID string + } + type want struct { + err func(error) bool + } + ctx := context.Background() + filterErr := errors.New("filter error") + tests := []struct { + name string + fields fields + args args + want want + }{ + { + name: "resource owner is given, no query", + fields: fields{ + domainPermissionCheck: mockDomainPermissionCheck( + ctx, + "permission", + "resourceOwner", + "aggregateID"), + }, + args: args{ + ctx: ctx, + permission: "permission", + resourceOwner: "resourceOwner", + aggregateID: "aggregateID", + }, + }, + { + name: "resource owner is empty, query for resource owner", + fields: fields{ + eventstore: expectEventstore( + expectFilter(&repository.Event{ + AggregateID: "aggregateID", + ResourceOwner: sql.NullString{String: "resourceOwner"}, + }), + ), + domainPermissionCheck: mockDomainPermissionCheck(ctx, "permission", "resourceOwner", "aggregateID"), + }, + args: args{ + ctx: ctx, + permission: "permission", + resourceOwner: "", + aggregateID: "aggregateID", + }, + }, + { + name: "resource owner is empty, query for resource owner, error", + fields: fields{ + eventstore: expectEventstore( + expectFilterError(filterErr), + ), + }, + args: args{ + ctx: ctx, + permission: "permission", + resourceOwner: "", + aggregateID: "aggregateID", + }, + want: want{ + err: func(err error) bool { + return errors.Is(err, filterErr) + }, + }, + }, + { + name: "resource owner is empty, query for resource owner, no events", + fields: fields{ + eventstore: expectEventstore( + expectFilter(), + ), + }, + args: args{ + ctx: ctx, + permission: "permission", + resourceOwner: "", + aggregateID: "aggregateID", + }, + want: want{ + err: zerrors.IsNotFound, + }, + }, + { + name: "no aggregateID, internal error", + args: args{ + ctx: ctx, + }, + want: want{ + err: zerrors.IsInternal, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + checkPermission: func(ctx context.Context, permission, orgID, resourceID string) (err error) { + assert.Failf(t, "Domain permission check should not be called", "Called c.checkPermission(%v,%v,%v,%v)", ctx, permission, orgID, resourceID) + return nil + }, + eventstore: expectEventstore()(t), + } + if tt.fields.domainPermissionCheck != nil { + c.checkPermission = tt.fields.domainPermissionCheck(t) + } + if tt.fields.eventstore != nil { + c.eventstore = tt.fields.eventstore(t) + } + err := c.newPermissionCheck(tt.args.ctx, tt.args.permission, tt.args.aggregateType)(tt.args.resourceOwner, tt.args.aggregateID) + if tt.want.err != nil { + assert.True(t, tt.want.err(err)) + } + }) + } +} + +func TestCommands_CheckPermissionUserWrite(t *testing.T) { + type fields struct { + domainPermissionCheck func(*testing.T) domain.PermissionCheck + } + type args struct { + ctx context.Context + resourceOwner, aggregateID string + } + type want struct { + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + want want + }{ + { + name: "self, no permission check", + args: args{ + ctx: authz.SetCtxData(context.Background(), authz.CtxData{ + UserID: "aggregateID", + }), + resourceOwner: "resourceOwner", + aggregateID: "aggregateID", + }, + }, + { + name: "not self, permission check", + fields: fields{ + domainPermissionCheck: mockDomainPermissionCheck( + context.Background(), + "user.write", + "resourceOwner", + "foreignAggregateID"), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "resourceOwner", + aggregateID: "foreignAggregateID", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + checkPermission: func(ctx context.Context, permission, orgID, resourceID string) (err error) { + assert.Failf(t, "Domain permission check should not be called", "Called c.checkPermission(%v,%v,%v,%v)", ctx, permission, orgID, resourceID) + return nil + }, + } + if tt.fields.domainPermissionCheck != nil { + c.checkPermission = tt.fields.domainPermissionCheck(t) + } + err := c.NewPermissionCheckUserWrite(tt.args.ctx)(tt.args.resourceOwner, tt.args.aggregateID) + if tt.want.err != nil { + assert.True(t, tt.want.err(err)) + } + }) + } +} + +func TestCommands_CheckPermissionUserDelete(t *testing.T) { + type fields struct { + domainPermissionCheck func(*testing.T) domain.PermissionCheck + } + type args struct { + ctx context.Context + resourceOwner, aggregateID string + } + type want struct { + err func(error) bool + } + userCtx := authz.SetCtxData(context.Background(), authz.CtxData{ + UserID: "aggregateID", + }) + tests := []struct { + name string + fields fields + args args + want want + }{ + { + name: "self, no permission check", + args: args{ + ctx: userCtx, + resourceOwner: "resourceOwner", + aggregateID: "aggregateID", + }, + }, + { + name: "not self, permission check", + fields: fields{ + domainPermissionCheck: mockDomainPermissionCheck( + context.Background(), + "user.delete", + "resourceOwner", + "foreignAggregateID"), + }, + args: args{ + ctx: context.Background(), + resourceOwner: "resourceOwner", + aggregateID: "foreignAggregateID", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + checkPermission: func(ctx context.Context, permission, orgID, resourceID string) (err error) { + assert.Failf(t, "Domain permission check should not be called", "Called c.checkPermission(%v,%v,%v,%v)", ctx, permission, orgID, resourceID) + return nil + }, + } + if tt.fields.domainPermissionCheck != nil { + c.checkPermission = tt.fields.domainPermissionCheck(t) + } + err := c.checkPermissionDeleteUser(tt.args.ctx, tt.args.resourceOwner, tt.args.aggregateID) + if tt.want.err != nil { + assert.True(t, tt.want.err(err)) + } + }) + } +} + +func mockDomainPermissionCheck(expectCtx context.Context, expectPermission, expectResourceOwner, expectResourceID string) func(t *testing.T) domain.PermissionCheck { + return func(t *testing.T) domain.PermissionCheck { + return func(ctx context.Context, permission, orgID, resourceID string) (err error) { + assert.Equal(t, expectCtx, ctx) + assert.Equal(t, expectPermission, permission) + assert.Equal(t, expectResourceOwner, orgID) + assert.Equal(t, expectResourceID, resourceID) + return nil + } + } +} diff --git a/internal/command/project.go b/internal/command/project.go index df4f8ab545..4cdf1b7373 100644 --- a/internal/command/project.go +++ b/internal/command/project.go @@ -3,6 +3,7 @@ package command import ( "context" "strings" + "time" "github.com/zitadel/logging" @@ -10,6 +11,7 @@ import ( "github.com/zitadel/zitadel/internal/command/preparation" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/eventstore/v1/models" "github.com/zitadel/zitadel/internal/feature" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/repository/project" @@ -17,67 +19,60 @@ import ( "github.com/zitadel/zitadel/internal/zerrors" ) -func (c *Commands) AddProjectWithID(ctx context.Context, project *domain.Project, resourceOwner, projectID string) (_ *domain.Project, err error) { - ctx, span := tracing.NewSpan(ctx) - defer func() { span.EndWithError(err) }() - if resourceOwner == "" { - return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-w8tnSoJxtn", "Errors.ResourceOwnerMissing") - } - if projectID == "" { - return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-nDXf5vXoUj", "Errors.IDMissing") - } - if !project.IsValid() { - return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-IOVCC", "Errors.Project.Invalid") - } - project, err = c.addProjectWithID(ctx, project, resourceOwner, projectID) - if err != nil { - return nil, err - } - return project, nil +type AddProject struct { + models.ObjectRoot + + Name string + ProjectRoleAssertion bool + ProjectRoleCheck bool + HasProjectCheck bool + PrivateLabelingSetting domain.PrivateLabelingSetting } -func (c *Commands) AddProject(ctx context.Context, project *domain.Project, resourceOwner string) (_ *domain.Project, err error) { - ctx, span := tracing.NewSpan(ctx) - defer func() { span.EndWithError(err) }() - if !project.IsValid() { - return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-IOVCC", "Errors.Project.Invalid") +func (p *AddProject) IsValid() error { + if p.ResourceOwner == "" { + return zerrors.ThrowInvalidArgument(nil, "COMMAND-fmq7bqQX1s", "Errors.ResourceOwnerMissing") } - if resourceOwner == "" { - return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-fmq7bqQX1s", "Errors.ResourceOwnerMissing") + if p.Name == "" { + return zerrors.ThrowInvalidArgument(nil, "PROJECT-IOVCC", "Errors.Project.Invalid") } - - projectID, err := c.idGenerator.Next() - if err != nil { - return nil, err - } - - project, err = c.addProjectWithID(ctx, project, resourceOwner, projectID) - if err != nil { - return nil, err - } - return project, nil + return nil } -func (c *Commands) addProjectWithID(ctx context.Context, projectAdd *domain.Project, resourceOwner, projectID string) (_ *domain.Project, err error) { - projectAdd.AggregateID = projectID - projectWriteModel, err := c.getProjectWriteModelByID(ctx, projectAdd.AggregateID, resourceOwner) +func (c *Commands) AddProject(ctx context.Context, add *AddProject) (_ *domain.ObjectDetails, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + if err := add.IsValid(); err != nil { + return nil, err + } + + if add.AggregateID == "" { + add.AggregateID, err = c.idGenerator.Next() + if err != nil { + return nil, err + } + } + wm, err := c.getProjectWriteModelByID(ctx, add.AggregateID, add.ResourceOwner) if err != nil { return nil, err } - if isProjectStateExists(projectWriteModel.State) { + if isProjectStateExists(wm.State) { return nil, zerrors.ThrowAlreadyExists(nil, "COMMAND-opamwu", "Errors.Project.AlreadyExisting") } + if err := c.checkPermissionUpdateProject(ctx, wm.ResourceOwner, wm.AggregateID); err != nil { + return nil, err + } events := []eventstore.Command{ project.NewProjectAddedEvent( ctx, - //nolint: contextcheck - ProjectAggregateFromWriteModel(&projectWriteModel.WriteModel), - projectAdd.Name, - projectAdd.ProjectRoleAssertion, - projectAdd.ProjectRoleCheck, - projectAdd.HasProjectCheck, - projectAdd.PrivateLabelingSetting), + ProjectAggregateFromWriteModelWithCTX(ctx, &wm.WriteModel), + add.Name, + add.ProjectRoleAssertion, + add.ProjectRoleCheck, + add.HasProjectCheck, + add.PrivateLabelingSetting), } postCommit, err := c.projectCreatedMilestone(ctx, &events) if err != nil { @@ -88,11 +83,11 @@ func (c *Commands) addProjectWithID(ctx context.Context, projectAdd *domain.Proj return nil, err } postCommit(ctx) - err = AppendAndReduce(projectWriteModel, pushedEvents...) + err = AppendAndReduce(wm, pushedEvents...) if err != nil { return nil, err } - return projectWriteModelToProject(projectWriteModel), nil + return writeModelToObjectDetails(&wm.WriteModel), nil } func AddProjectCommand( @@ -177,7 +172,7 @@ func (c *Commands) projectState(ctx context.Context, projectID, resourceOwner st ) } -func (c *Commands) checkProjectExists(ctx context.Context, projectID, resourceOwner string) (err error) { +func (c *Commands) checkProjectExists(ctx context.Context, projectID, resourceOwner string) (_ string, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -185,50 +180,69 @@ func (c *Commands) checkProjectExists(ctx context.Context, projectID, resourceOw return c.checkProjectExistsOld(ctx, projectID, resourceOwner) } - _, state, err := c.projectAggregateByID(ctx, projectID, resourceOwner) + agg, state, err := c.projectAggregateByID(ctx, projectID, resourceOwner) if err != nil || !state.Valid() { - return zerrors.ThrowPreconditionFailed(err, "COMMA-VCnwD", "Errors.Project.NotFound") + return "", zerrors.ThrowPreconditionFailed(err, "COMMA-VCnwD", "Errors.Project.NotFound") + } + return agg.ResourceOwner, nil +} + +type ChangeProject struct { + models.ObjectRoot + + Name *string + ProjectRoleAssertion *bool + ProjectRoleCheck *bool + HasProjectCheck *bool + PrivateLabelingSetting *domain.PrivateLabelingSetting +} + +func (p *ChangeProject) IsValid() error { + if p.AggregateID == "" { + return zerrors.ThrowInvalidArgument(nil, "COMMAND-4m9vS", "Errors.Project.Invalid") + } + if p.Name != nil && *p.Name == "" { + return zerrors.ThrowInvalidArgument(nil, "COMMAND-4m9vS", "Errors.Project.Invalid") } return nil } -func (c *Commands) ChangeProject(ctx context.Context, projectChange *domain.Project, resourceOwner string) (*domain.Project, error) { - if !projectChange.IsValid() || projectChange.AggregateID == "" { - return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-4m9vS", "Errors.Project.Invalid") +func (c *Commands) ChangeProject(ctx context.Context, change *ChangeProject) (_ *domain.ObjectDetails, err error) { + if err := change.IsValid(); err != nil { + return nil, err } - existingProject, err := c.getProjectWriteModelByID(ctx, projectChange.AggregateID, resourceOwner) + existing, err := c.getProjectWriteModelByID(ctx, change.AggregateID, change.ResourceOwner) if err != nil { return nil, err } - if !isProjectStateExists(existingProject.State) { + if !isProjectStateExists(existing.State) { return nil, zerrors.ThrowNotFound(nil, "COMMAND-3M9sd", "Errors.Project.NotFound") } + if err := c.checkPermissionUpdateProject(ctx, existing.ResourceOwner, existing.AggregateID); err != nil { + return nil, err + } - projectAgg := ProjectAggregateFromWriteModel(&existingProject.WriteModel) - changedEvent, hasChanged, err := existingProject.NewChangedEvent( + changedEvent := existing.NewChangedEvent( ctx, - projectAgg, - projectChange.Name, - projectChange.ProjectRoleAssertion, - projectChange.ProjectRoleCheck, - projectChange.HasProjectCheck, - projectChange.PrivateLabelingSetting) + ProjectAggregateFromWriteModelWithCTX(ctx, &existing.WriteModel), + change.Name, + change.ProjectRoleAssertion, + change.ProjectRoleCheck, + change.HasProjectCheck, + change.PrivateLabelingSetting) + if changedEvent == nil { + return writeModelToObjectDetails(&existing.WriteModel), nil + } + err = c.pushAppendAndReduce(ctx, existing, changedEvent) if err != nil { return nil, err } - if !hasChanged { - return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-2M0fs", "Errors.NoChangesFound") - } - err = c.pushAppendAndReduce(ctx, existingProject, changedEvent) - if err != nil { - return nil, err - } - return projectWriteModelToProject(existingProject), nil + return writeModelToObjectDetails(&existing.WriteModel), nil } func (c *Commands) DeactivateProject(ctx context.Context, projectID string, resourceOwner string) (*domain.ObjectDetails, error) { - if projectID == "" || resourceOwner == "" { + if projectID == "" { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-88iF0", "Errors.Project.ProjectIDMissing") } @@ -247,6 +261,9 @@ func (c *Commands) DeactivateProject(ctx context.Context, projectID string, reso if state != domain.ProjectStateActive { return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-mki55", "Errors.Project.NotActive") } + if err := c.checkPermissionUpdateProject(ctx, projectAgg.ResourceOwner, projectAgg.ID); err != nil { + return nil, err + } pushedEvents, err := c.eventstore.Push(ctx, project.NewProjectDeactivatedEvent(ctx, projectAgg)) if err != nil { @@ -261,7 +278,7 @@ func (c *Commands) DeactivateProject(ctx context.Context, projectID string, reso } func (c *Commands) ReactivateProject(ctx context.Context, projectID string, resourceOwner string) (*domain.ObjectDetails, error) { - if projectID == "" || resourceOwner == "" { + if projectID == "" { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-3ihsF", "Errors.Project.ProjectIDMissing") } @@ -280,6 +297,9 @@ func (c *Commands) ReactivateProject(ctx context.Context, projectID string, reso if state != domain.ProjectStateInactive { return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-5M9bs", "Errors.Project.NotInactive") } + if err := c.checkPermissionUpdateProject(ctx, projectAgg.ResourceOwner, projectAgg.ID); err != nil { + return nil, err + } pushedEvents, err := c.eventstore.Push(ctx, project.NewProjectReactivatedEvent(ctx, projectAgg)) if err != nil { @@ -293,6 +313,7 @@ func (c *Commands) ReactivateProject(ctx context.Context, projectID string, reso }, nil } +// Deprecated: use commands.DeleteProject func (c *Commands) RemoveProject(ctx context.Context, projectID, resourceOwner string, cascadingUserGrantIDs ...string) (*domain.ObjectDetails, error) { if projectID == "" || resourceOwner == "" { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-66hM9", "Errors.Project.ProjectIDMissing") @@ -316,15 +337,18 @@ func (c *Commands) RemoveProject(ctx context.Context, projectID, resourceOwner s uniqueConstraints[i] = project.NewRemoveSAMLConfigEntityIDUniqueConstraint(entityID.EntityID) } - projectAgg := ProjectAggregateFromWriteModel(&existingProject.WriteModel) events := []eventstore.Command{ - project.NewProjectRemovedEvent(ctx, projectAgg, existingProject.Name, uniqueConstraints), + project.NewProjectRemovedEvent(ctx, + ProjectAggregateFromWriteModelWithCTX(ctx, &existingProject.WriteModel), + existingProject.Name, + uniqueConstraints, + ), } for _, grantID := range cascadingUserGrantIDs { - event, _, err := c.removeUserGrant(ctx, grantID, "", true) + event, _, err := c.removeUserGrant(ctx, grantID, "", true, false, nil) if err != nil { - logging.LogWithFields("COMMAND-b8Djf", "usergrantid", grantID).WithError(err).Warn("could not cascade remove user grant") + logging.WithFields("id", "COMMAND-b8Djf", "usergrantid", grantID).WithError(err).Warn("could not cascade remove user grant") continue } events = append(events, event) @@ -341,6 +365,53 @@ func (c *Commands) RemoveProject(ctx context.Context, projectID, resourceOwner s return writeModelToObjectDetails(&existingProject.WriteModel), nil } +func (c *Commands) DeleteProject(ctx context.Context, id, resourceOwner string, cascadingUserGrantIDs ...string) (time.Time, error) { + if id == "" { + return time.Time{}, zerrors.ThrowInvalidArgument(nil, "COMMAND-obqos2l3no", "Errors.IDMissing") + } + + existing, err := c.getProjectWriteModelByID(ctx, id, resourceOwner) + if err != nil { + return time.Time{}, err + } + if !isProjectStateExists(existing.State) { + return existing.WriteModel.ChangeDate, nil + } + if err := c.checkPermissionDeleteProject(ctx, existing.ResourceOwner, existing.AggregateID); err != nil { + return time.Time{}, err + } + + samlEntityIDsAgg, err := c.getSAMLEntityIdsWriteModelByProjectID(ctx, id, resourceOwner) + if err != nil { + return time.Time{}, err + } + + uniqueConstraints := make([]*eventstore.UniqueConstraint, len(samlEntityIDsAgg.EntityIDs)) + for i, entityID := range samlEntityIDsAgg.EntityIDs { + uniqueConstraints[i] = project.NewRemoveSAMLConfigEntityIDUniqueConstraint(entityID.EntityID) + } + events := []eventstore.Command{ + project.NewProjectRemovedEvent(ctx, + ProjectAggregateFromWriteModelWithCTX(ctx, &existing.WriteModel), + existing.Name, + uniqueConstraints, + ), + } + for _, grantID := range cascadingUserGrantIDs { + event, _, err := c.removeUserGrant(ctx, grantID, "", true, false, nil) + if err != nil { + logging.WithFields("id", "COMMAND-b8Djf", "usergrantid", grantID).WithError(err).Warn("could not cascade remove user grant") + continue + } + events = append(events, event) + } + + if err := c.pushAppendAndReduce(ctx, existing, events...); err != nil { + return time.Time{}, err + } + return existing.WriteModel.ChangeDate, nil +} + func (c *Commands) getProjectWriteModelByID(ctx context.Context, projectID, resourceOwner string) (_ *ProjectWriteModel, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() diff --git a/internal/command/project_application.go b/internal/command/project_application.go index 0ccf5dc852..465b12e1e1 100644 --- a/internal/command/project_application.go +++ b/internal/command/project_application.go @@ -15,7 +15,7 @@ type AddApp struct { Name string } -func (c *Commands) ChangeApplication(ctx context.Context, projectID string, appChange domain.Application, resourceOwner string) (*domain.ObjectDetails, error) { +func (c *Commands) UpdateApplicationName(ctx context.Context, projectID string, appChange domain.Application, resourceOwner string) (*domain.ObjectDetails, error) { if projectID == "" || appChange.GetAppID() == "" || appChange.GetApplicationName() == "" { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-4m9vS", "Errors.Project.App.Invalid") } @@ -30,6 +30,13 @@ func (c *Commands) ChangeApplication(ctx context.Context, projectID string, appC if existingApp.Name == appChange.GetApplicationName() { return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-2m8vx", "Errors.NoChangesFound") } + if err := c.eventstore.FilterToQueryReducer(ctx, existingApp); err != nil { + return nil, err + } + if err := c.checkPermissionUpdateApplication(ctx, existingApp.ResourceOwner, existingApp.AggregateID); err != nil { + return nil, err + } + projectAgg := ProjectAggregateFromWriteModel(&existingApp.WriteModel) pushedEvents, err := c.eventstore.Push( ctx, @@ -59,6 +66,13 @@ func (c *Commands) DeactivateApplication(ctx context.Context, projectID, appID, if existingApp.State != domain.AppStateActive { return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-dsh35", "Errors.Project.App.NotActive") } + if err := c.eventstore.FilterToQueryReducer(ctx, existingApp); err != nil { + return nil, err + } + if err := c.checkPermissionUpdateApplication(ctx, existingApp.ResourceOwner, existingApp.AggregateID); err != nil { + return nil, err + } + projectAgg := ProjectAggregateFromWriteModel(&existingApp.WriteModel) pushedEvents, err := c.eventstore.Push(ctx, project.NewApplicationDeactivatedEvent(ctx, projectAgg, appID)) if err != nil { @@ -86,6 +100,11 @@ func (c *Commands) ReactivateApplication(ctx context.Context, projectID, appID, if existingApp.State != domain.AppStateInactive { return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-1n8cM", "Errors.Project.App.NotInactive") } + + if err := c.checkPermissionUpdateApplication(ctx, existingApp.ResourceOwner, existingApp.AggregateID); err != nil { + return nil, err + } + projectAgg := ProjectAggregateFromWriteModel(&existingApp.WriteModel) pushedEvents, err := c.eventstore.Push(ctx, project.NewApplicationReactivatedEvent(ctx, projectAgg, appID)) @@ -111,6 +130,13 @@ func (c *Commands) RemoveApplication(ctx context.Context, projectID, appID, reso if existingApp.State == domain.AppStateUnspecified || existingApp.State == domain.AppStateRemoved { return nil, zerrors.ThrowNotFound(nil, "COMMAND-0po9s", "Errors.Project.App.NotExisting") } + if err := c.eventstore.FilterToQueryReducer(ctx, existingApp); err != nil { + return nil, err + } + if err := c.checkPermissionDeleteApp(ctx, existingApp.ResourceOwner, existingApp.AggregateID); err != nil { + return nil, err + } + projectAgg := ProjectAggregateFromWriteModel(&existingApp.WriteModel) entityID := "" diff --git a/internal/command/project_application_api.go b/internal/command/project_application_api.go index 21c3bc5ee7..82e7d0bde8 100644 --- a/internal/command/project_application_api.go +++ b/internal/command/project_application_api.go @@ -79,7 +79,7 @@ func (c *Commands) AddAPIApplicationWithID(ctx context.Context, apiApp *domain.A return nil, zerrors.ThrowPreconditionFailed(nil, "PROJECT-mabu12", "Errors.Project.App.AlreadyExisting") } - if err := c.checkProjectExists(ctx, apiApp.AggregateID, resourceOwner); err != nil { + if _, err := c.checkProjectExists(ctx, apiApp.AggregateID, resourceOwner); err != nil { return nil, err } return c.addAPIApplicationWithID(ctx, apiApp, resourceOwner, appID) @@ -90,16 +90,24 @@ func (c *Commands) AddAPIApplication(ctx context.Context, apiApp *domain.APIApp, return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-5m9E", "Errors.Project.App.Invalid") } - if err := c.checkProjectExists(ctx, apiApp.AggregateID, resourceOwner); err != nil { + projectResOwner, err := c.checkProjectExists(ctx, apiApp.AggregateID, resourceOwner) + if err != nil { return nil, err } + if resourceOwner == "" { + resourceOwner = projectResOwner + } + if !apiApp.IsValid() { return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-Bff2g", "Errors.Project.App.Invalid") } - appID, err := c.idGenerator.Next() - if err != nil { - return nil, err + appID := apiApp.AppID + if appID == "" { + appID, err = c.idGenerator.Next() + if err != nil { + return nil, err + } } return c.addAPIApplicationWithID(ctx, apiApp, resourceOwner, appID) @@ -112,6 +120,13 @@ func (c *Commands) addAPIApplicationWithID(ctx context.Context, apiApp *domain.A apiApp.AppID = appID addedApplication := NewAPIApplicationWriteModel(apiApp.AggregateID, resourceOwner) + if err := c.eventstore.FilterToQueryReducer(ctx, addedApplication); err != nil { + return nil, err + } + if err := c.checkPermissionUpdateApplication(ctx, addedApplication.ResourceOwner, addedApplication.AggregateID); err != nil { + return nil, err + } + projectAgg := ProjectAggregateFromWriteModel(&addedApplication.WriteModel) events := []eventstore.Command{ @@ -150,7 +165,7 @@ func (c *Commands) addAPIApplicationWithID(ctx context.Context, apiApp *domain.A return result, nil } -func (c *Commands) ChangeAPIApplication(ctx context.Context, apiApp *domain.APIApp, resourceOwner string) (*domain.APIApp, error) { +func (c *Commands) UpdateAPIApplication(ctx context.Context, apiApp *domain.APIApp, resourceOwner string) (*domain.APIApp, error) { if apiApp.AppID == "" || apiApp.AggregateID == "" { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-1m900", "Errors.Project.App.APIConfigInvalid") } @@ -165,6 +180,13 @@ func (c *Commands) ChangeAPIApplication(ctx context.Context, apiApp *domain.APIA if !existingAPI.IsAPI() { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-Gnwt3", "Errors.Project.App.IsNotAPI") } + if err := c.eventstore.FilterToQueryReducer(ctx, existingAPI); err != nil { + return nil, err + } + if err := c.checkPermissionUpdateApplication(ctx, existingAPI.ResourceOwner, existingAPI.AggregateID); err != nil { + return nil, err + } + projectAgg := ProjectAggregateFromWriteModel(&existingAPI.WriteModel) changedEvent, hasChanged, err := existingAPI.NewChangedEvent( ctx, @@ -205,6 +227,11 @@ func (c *Commands) ChangeAPIApplicationSecret(ctx context.Context, projectID, ap if !existingAPI.IsAPI() { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-aeH4", "Errors.Project.App.IsNotAPI") } + + if err := c.checkPermissionUpdateApplication(ctx, existingAPI.ResourceOwner, existingAPI.AggregateID); err != nil { + return nil, err + } + encodedHash, plain, err := c.newHashedSecret(ctx, c.eventstore.Filter) //nolint:staticcheck if err != nil { return nil, err @@ -226,37 +253,6 @@ func (c *Commands) ChangeAPIApplicationSecret(ctx context.Context, projectID, ap return result, err } -func (c *Commands) VerifyAPIClientSecret(ctx context.Context, projectID, appID, secret string) (err error) { - ctx, span := tracing.NewSpan(ctx) - defer func() { span.EndWithError(err) }() - - app, err := c.getAPIAppWriteModel(ctx, projectID, appID, "") - if err != nil { - return err - } - if !app.State.Exists() { - return zerrors.ThrowPreconditionFailed(nil, "COMMAND-DFnbf", "Errors.Project.App.NotExisting") - } - if !app.IsAPI() { - return zerrors.ThrowInvalidArgument(nil, "COMMAND-Bf3fw", "Errors.Project.App.IsNotAPI") - } - if app.HashedSecret == "" { - return zerrors.ThrowPreconditionFailed(nil, "COMMAND-D3t5g", "Errors.Project.App.APIConfigInvalid") - } - - projectAgg := ProjectAggregateFromWriteModel(&app.WriteModel) - ctx, spanPasswordComparison := tracing.NewNamedSpan(ctx, "passwap.Verify") - updated, err := c.secretHasher.Verify(app.HashedSecret, secret) - spanPasswordComparison.EndWithError(err) - if err != nil { - return zerrors.ThrowInvalidArgument(err, "COMMAND-SADfg", "Errors.Project.App.ClientSecretInvalid") - } - if updated != "" { - c.apiUpdateSecret(ctx, projectAgg, app.AppID, updated) - } - return nil -} - func (c *Commands) APIUpdateSecret(ctx context.Context, appID, projectID, resourceOwner, updated string) { agg := project_repo.NewAggregate(projectID, resourceOwner) c.apiUpdateSecret(ctx, &agg.Aggregate, appID, updated) diff --git a/internal/command/project_application_api_test.go b/internal/command/project_application_api_test.go index 2702c00b39..53448e1c5e 100644 --- a/internal/command/project_application_api_test.go +++ b/internal/command/project_application_api_test.go @@ -2,16 +2,11 @@ package command import ( "context" - "io" "testing" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/zitadel/passwap" - "github.com/zitadel/passwap/bcrypt" "github.com/zitadel/zitadel/internal/command/preparation" - "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore/v1/models" @@ -147,6 +142,7 @@ func TestAddAPIConfig(t *testing.T) { } func TestCommandSide_AddAPIApplication(t *testing.T) { + t.Parallel() type fields struct { eventstore func(t *testing.T) *eventstore.Eventstore idGenerator id.Generator @@ -243,6 +239,7 @@ func TestCommandSide_AddAPIApplication(t *testing.T) { domain.PrivateLabelingSettingUnspecified), ), ), + expectFilter(), expectPush( project.NewApplicationAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -297,6 +294,7 @@ func TestCommandSide_AddAPIApplication(t *testing.T) { domain.PrivateLabelingSettingUnspecified), ), ), + expectFilter(), expectPush( project.NewApplicationAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -351,6 +349,7 @@ func TestCommandSide_AddAPIApplication(t *testing.T) { domain.PrivateLabelingSettingUnspecified), ), ), + expectFilter(), expectPush( project.NewApplicationAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -395,6 +394,8 @@ func TestCommandSide_AddAPIApplication(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() + r := &Commands{ eventstore: tt.fields.eventstore(t), idGenerator: tt.fields.idGenerator, @@ -402,6 +403,7 @@ func TestCommandSide_AddAPIApplication(t *testing.T) { defaultSecretGenerators: &SecretGenerators{ ClientSecret: emptyConfig, }, + checkPermission: newMockPermissionCheckAllowed(), } got, err := r.AddAPIApplication(tt.args.ctx, tt.args.apiApp, tt.args.resourceOwner) if tt.res.err == nil { @@ -418,6 +420,8 @@ func TestCommandSide_AddAPIApplication(t *testing.T) { } func TestCommandSide_ChangeAPIApplication(t *testing.T) { + t.Parallel() + type fields struct { eventstore func(t *testing.T) *eventstore.Eventstore } @@ -521,6 +525,7 @@ func TestCommandSide_ChangeAPIApplication(t *testing.T) { domain.APIAuthMethodTypePrivateKeyJWT), ), ), + expectFilter(), ), }, args: args{ @@ -560,6 +565,7 @@ func TestCommandSide_ChangeAPIApplication(t *testing.T) { domain.APIAuthMethodTypeBasic), ), ), + expectFilter(), expectPush( newAPIAppChangedEvent(context.Background(), "app1", @@ -598,14 +604,17 @@ func TestCommandSide_ChangeAPIApplication(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() + r := &Commands{ eventstore: tt.fields.eventstore(t), newHashedSecret: mockHashedSecret("secret"), defaultSecretGenerators: &SecretGenerators{ ClientSecret: emptyConfig, }, + checkPermission: newMockPermissionCheckAllowed(), } - got, err := r.ChangeAPIApplication(tt.args.ctx, tt.args.apiApp, tt.args.resourceOwner) + got, err := r.UpdateAPIApplication(tt.args.ctx, tt.args.apiApp, tt.args.resourceOwner) if tt.res.err == nil { assert.NoError(t, err) } @@ -620,6 +629,8 @@ func TestCommandSide_ChangeAPIApplication(t *testing.T) { } func TestCommandSide_ChangeAPIApplicationSecret(t *testing.T) { + t.Parallel() + type fields struct { eventstore func(*testing.T) *eventstore.Eventstore } @@ -739,12 +750,15 @@ func TestCommandSide_ChangeAPIApplicationSecret(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() + r := &Commands{ eventstore: tt.fields.eventstore(t), newHashedSecret: mockHashedSecret("secret"), defaultSecretGenerators: &SecretGenerators{ ClientSecret: emptyConfig, }, + checkPermission: newMockPermissionCheckAllowed(), } got, err := r.ChangeAPIApplicationSecret(tt.args.ctx, tt.args.projectID, tt.args.appID, tt.args.resourceOwner) if tt.res.err == nil { @@ -771,99 +785,3 @@ func newAPIAppChangedEvent(ctx context.Context, appID, projectID, resourceOwner ) return event } - -func TestCommands_VerifyAPIClientSecret(t *testing.T) { - hasher := &crypto.Hasher{ - Swapper: passwap.NewSwapper(bcrypt.New(bcrypt.MinCost)), - } - hashedSecret, err := hasher.Hash("secret") - require.NoError(t, err) - agg := project.NewAggregate("projectID", "orgID") - - tests := []struct { - name string - secret string - eventstore func(*testing.T) *eventstore.Eventstore - wantErr error - }{ - { - name: "filter error", - eventstore: expectEventstore( - expectFilterError(io.ErrClosedPipe), - ), - wantErr: io.ErrClosedPipe, - }, - { - name: "app not exists", - eventstore: expectEventstore( - expectFilter(), - ), - wantErr: zerrors.ThrowPreconditionFailed(nil, "COMMAND-DFnbf", "Errors.Project.App.NotExisting"), - }, - { - name: "wrong app type", - eventstore: expectEventstore( - expectFilter( - eventFromEventPusher( - project.NewApplicationAddedEvent(context.Background(), &agg.Aggregate, "appID", "appName"), - ), - ), - ), - wantErr: zerrors.ThrowInvalidArgument(nil, "COMMAND-Bf3fw", "Errors.Project.App.IsNotAPI"), - }, - { - name: "no secret set", - eventstore: expectEventstore( - expectFilter( - eventFromEventPusher( - project.NewApplicationAddedEvent(context.Background(), &agg.Aggregate, "appID", "appName"), - ), - eventFromEventPusher( - project.NewAPIConfigAddedEvent(context.Background(), &agg.Aggregate, "appID", "clientID", "", domain.APIAuthMethodTypePrivateKeyJWT), - ), - ), - ), - wantErr: zerrors.ThrowPreconditionFailed(nil, "COMMAND-D3t5g", "Errors.Project.App.APIConfigInvalid"), - }, - { - name: "check succeeded", - secret: "secret", - eventstore: expectEventstore( - expectFilter( - eventFromEventPusher( - project.NewApplicationAddedEvent(context.Background(), &agg.Aggregate, "appID", "appName"), - ), - eventFromEventPusher( - project.NewAPIConfigAddedEvent(context.Background(), &agg.Aggregate, "appID", "clientID", hashedSecret, domain.APIAuthMethodTypePrivateKeyJWT), - ), - ), - ), - }, - { - name: "check failed", - secret: "wrong!", - eventstore: expectEventstore( - expectFilter( - eventFromEventPusher( - project.NewApplicationAddedEvent(context.Background(), &agg.Aggregate, "appID", "appName"), - ), - eventFromEventPusher( - project.NewAPIConfigAddedEvent(context.Background(), &agg.Aggregate, "appID", "clientID", hashedSecret, domain.APIAuthMethodTypePrivateKeyJWT), - ), - ), - ), - wantErr: zerrors.ThrowInvalidArgument(err, "COMMAND-SADfg", "Errors.Project.App.ClientSecretInvalid"), - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - c := &Commands{ - eventstore: tt.eventstore(t), - secretHasher: hasher, - } - err := c.VerifyAPIClientSecret(context.Background(), "projectID", "appID", tt.secret) - c.jobs.Wait() - require.ErrorIs(t, err, tt.wantErr) - }) - } -} diff --git a/internal/command/project_application_key.go b/internal/command/project_application_key.go index 519e9fc30a..47dacdd638 100644 --- a/internal/command/project_application_key.go +++ b/internal/command/project_application_key.go @@ -38,6 +38,11 @@ func (c *Commands) AddApplicationKey(ctx context.Context, key *domain.Applicatio if err != nil { return nil, err } + + if resourceOwner == "" { + resourceOwner = application.ResourceOwner + } + if !application.State.Exists() { return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-sak25", "Errors.Project.App.NotFound") } @@ -59,6 +64,10 @@ func (c *Commands) addApplicationKey(ctx context.Context, key *domain.Applicatio return nil, err } + if err := c.checkPermissionUpdateApplication(ctx, keyWriteModel.ResourceOwner, keyWriteModel.AggregateID); err != nil { + return nil, err + } + if !keyWriteModel.KeysAllowed { return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-Dff54", "Errors.Project.App.AuthMethodNoPrivateKeyJWT") } @@ -110,6 +119,10 @@ func (c *Commands) RemoveApplicationKey(ctx context.Context, projectID, applicat return nil, zerrors.ThrowNotFound(nil, "COMMAND-4m77G", "Errors.Project.App.Key.NotFound") } + if err := c.checkPermissionUpdateApplication(ctx, keyWriteModel.ResourceOwner, keyWriteModel.AggregateID); err != nil { + return nil, err + } + pushedEvents, err := c.eventstore.Push(ctx, project.NewApplicationKeyRemovedEvent(ctx, ProjectAggregateFromWriteModel(&keyWriteModel.WriteModel), keyID)) if err != nil { return nil, err diff --git a/internal/command/project_application_key_test.go b/internal/command/project_application_key_test.go index 9fd46c75f3..3402cab507 100644 --- a/internal/command/project_application_key_test.go +++ b/internal/command/project_application_key_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/zitadel/zitadel/internal/domain" + permissionmock "github.com/zitadel/zitadel/internal/domain/mock" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore/v1/models" "github.com/zitadel/zitadel/internal/id" @@ -17,9 +18,10 @@ import ( func TestCommandSide_AddAPIApplicationKey(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore - idGenerator id.Generator - keySize int + eventstore func(*testing.T) *eventstore.Eventstore + idGenerator id.Generator + keySize int + permissionCheckMock domain.PermissionCheck } type args struct { ctx context.Context @@ -39,9 +41,8 @@ func TestCommandSide_AddAPIApplicationKey(t *testing.T) { { name: "no aggregateid, invalid argument error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + permissionCheckMock: permissionmock.MockPermissionCheckOK(), }, args: args{ ctx: context.Background(), @@ -57,9 +58,8 @@ func TestCommandSide_AddAPIApplicationKey(t *testing.T) { { name: "no appid, invalid argument error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + permissionCheckMock: permissionmock.MockPermissionCheckOK(), }, args: args{ ctx: context.Background(), @@ -77,10 +77,8 @@ func TestCommandSide_AddAPIApplicationKey(t *testing.T) { { name: "app not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, - expectFilter(), - ), + eventstore: expectEventstore(expectFilter()), + permissionCheckMock: permissionmock.MockPermissionCheckOK(), }, args: args{ ctx: context.Background(), @@ -97,10 +95,9 @@ func TestCommandSide_AddAPIApplicationKey(t *testing.T) { }, }, { - name: "create key not allowed, precondition error", + name: "create key not allowed, precondition error 1", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewApplicationAddedEvent(context.Background(), @@ -121,7 +118,8 @@ func TestCommandSide_AddAPIApplicationKey(t *testing.T) { ), ), ), - idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "key1"), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "key1"), + permissionCheckMock: permissionmock.MockPermissionCheckOK(), }, args: args{ ctx: context.Background(), @@ -138,10 +136,9 @@ func TestCommandSide_AddAPIApplicationKey(t *testing.T) { }, }, { - name: "create key not allowed, precondition error", + name: "permission check failed", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewApplicationAddedEvent(context.Background(), @@ -162,8 +159,9 @@ func TestCommandSide_AddAPIApplicationKey(t *testing.T) { ), ), ), - idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "key1"), - keySize: 10, + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "key1"), + keySize: 10, + permissionCheckMock: permissionmock.MockPermissionCheckErr(zerrors.ThrowPermissionDenied(nil, "mock.err", "mock permission check failed")), }, args: args{ ctx: context.Background(), @@ -175,6 +173,47 @@ func TestCommandSide_AddAPIApplicationKey(t *testing.T) { }, resourceOwner: "org1", }, + res: res{ + err: zerrors.IsPermissionDenied, + }, + }, + { + name: "create key not allowed, precondition error 2", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewApplicationAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "app1", + "app", + ), + ), + ), + expectFilter( + eventFromEventPusher( + project.NewAPIConfigAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "app1", + "client1@project", + "secret", + domain.APIAuthMethodTypeBasic), + ), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "key1"), + keySize: 10, + permissionCheckMock: permissionmock.MockPermissionCheckOK(), + }, + args: args{ + ctx: context.Background(), + key: &domain.ApplicationKey{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + ApplicationID: "app1", + }, + }, res: res{ err: zerrors.IsPreconditionFailed, }, @@ -183,9 +222,10 @@ func TestCommandSide_AddAPIApplicationKey(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), idGenerator: tt.fields.idGenerator, applicationKeySize: tt.fields.keySize, + checkPermission: tt.fields.permissionCheckMock, } got, err := r.AddApplicationKey(tt.args.ctx, tt.args.key, tt.args.resourceOwner) if tt.res.err == nil { diff --git a/internal/command/project_application_oidc.go b/internal/command/project_application_oidc.go index fccb0efe06..7f33b6a3cf 100644 --- a/internal/command/project_application_oidc.go +++ b/internal/command/project_application_oidc.go @@ -5,6 +5,8 @@ import ( "strings" "time" + "github.com/muhlemmer/gu" + http_util "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/command/preparation" "github.com/zitadel/zitadel/internal/domain" @@ -120,6 +122,7 @@ func (c *Commands) AddOIDCAppCommand(app *addOIDCApp) preparation.Validation { } } +// TODO: Combine with AddOIDCApplication and addOIDCApplicationWithID func (c *Commands) AddOIDCApplicationWithID(ctx context.Context, oidcApp *domain.OIDCApp, resourceOwner, appID string) (_ *domain.OIDCApp, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -132,7 +135,7 @@ func (c *Commands) AddOIDCApplicationWithID(ctx context.Context, oidcApp *domain return nil, zerrors.ThrowPreconditionFailed(nil, "PROJECT-lxowmp", "Errors.Project.App.AlreadyExisting") } - if err := c.checkProjectExists(ctx, oidcApp.AggregateID, resourceOwner); err != nil { + if _, err := c.checkProjectExists(ctx, oidcApp.AggregateID, resourceOwner); err != nil { return nil, err } return c.addOIDCApplicationWithID(ctx, oidcApp, resourceOwner, appID) @@ -142,9 +145,15 @@ func (c *Commands) AddOIDCApplication(ctx context.Context, oidcApp *domain.OIDCA if oidcApp == nil || oidcApp.AggregateID == "" { return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-34Fm0", "Errors.Project.App.Invalid") } - if err := c.checkProjectExists(ctx, oidcApp.AggregateID, resourceOwner); err != nil { + + projectResOwner, err := c.checkProjectExists(ctx, oidcApp.AggregateID, resourceOwner) + if err != nil { return nil, err } + if resourceOwner == "" { + resourceOwner = projectResOwner + } + if oidcApp.AppName == "" || !oidcApp.IsValid() { return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-1n8df", "Errors.Project.App.Invalid") } @@ -162,6 +171,13 @@ func (c *Commands) addOIDCApplicationWithID(ctx context.Context, oidcApp *domain defer func() { span.EndWithError(err) }() addedApplication := NewOIDCApplicationWriteModel(oidcApp.AggregateID, resourceOwner) + if err := c.eventstore.FilterToQueryReducer(ctx, addedApplication); err != nil { + return nil, err + } + if err := c.checkPermissionUpdateApplication(ctx, addedApplication.ResourceOwner, addedApplication.AggregateID); err != nil { + return nil, err + } + projectAgg := ProjectAggregateFromWriteModel(&addedApplication.WriteModel) oidcApp.AppID = appID @@ -183,27 +199,27 @@ func (c *Commands) addOIDCApplicationWithID(ctx context.Context, oidcApp *domain } events = append(events, project_repo.NewOIDCConfigAddedEvent(ctx, projectAgg, - oidcApp.OIDCVersion, + gu.Value(oidcApp.OIDCVersion), oidcApp.AppID, oidcApp.ClientID, oidcApp.EncodedHash, trimStringSliceWhiteSpaces(oidcApp.RedirectUris), oidcApp.ResponseTypes, oidcApp.GrantTypes, - oidcApp.ApplicationType, - oidcApp.AuthMethodType, + gu.Value(oidcApp.ApplicationType), + gu.Value(oidcApp.AuthMethodType), trimStringSliceWhiteSpaces(oidcApp.PostLogoutRedirectUris), - oidcApp.DevMode, - oidcApp.AccessTokenType, - oidcApp.AccessTokenRoleAssertion, - oidcApp.IDTokenRoleAssertion, - oidcApp.IDTokenUserinfoAssertion, - oidcApp.ClockSkew, + gu.Value(oidcApp.DevMode), + gu.Value(oidcApp.AccessTokenType), + gu.Value(oidcApp.AccessTokenRoleAssertion), + gu.Value(oidcApp.IDTokenRoleAssertion), + gu.Value(oidcApp.IDTokenUserinfoAssertion), + gu.Value(oidcApp.ClockSkew), trimStringSliceWhiteSpaces(oidcApp.AdditionalOrigins), - oidcApp.SkipNativeAppSuccessPage, - strings.TrimSpace(oidcApp.BackChannelLogoutURI), - oidcApp.LoginVersion, - strings.TrimSpace(oidcApp.LoginBaseURI), + gu.Value(oidcApp.SkipNativeAppSuccessPage), + strings.TrimSpace(gu.Value(oidcApp.BackChannelLogoutURI)), + gu.Value(oidcApp.LoginVersion), + strings.TrimSpace(gu.Value(oidcApp.LoginBaseURI)), )) addedApplication.AppID = oidcApp.AppID @@ -226,7 +242,7 @@ func (c *Commands) addOIDCApplicationWithID(ctx context.Context, oidcApp *domain return result, nil } -func (c *Commands) ChangeOIDCApplication(ctx context.Context, oidc *domain.OIDCApp, resourceOwner string) (*domain.OIDCApp, error) { +func (c *Commands) UpdateOIDCApplication(ctx context.Context, oidc *domain.OIDCApp, resourceOwner string) (*domain.OIDCApp, error) { if !oidc.IsValid() || oidc.AppID == "" || oidc.AggregateID == "" { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-5m9fs", "Errors.Project.App.OIDCConfigInvalid") } @@ -241,7 +257,23 @@ func (c *Commands) ChangeOIDCApplication(ctx context.Context, oidc *domain.OIDCA if !existingOIDC.IsOIDC() { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-GBr34", "Errors.Project.App.IsNotOIDC") } + if err := c.eventstore.FilterToQueryReducer(ctx, existingOIDC); err != nil { + return nil, err + } + if err := c.checkPermissionUpdateApplication(ctx, existingOIDC.ResourceOwner, existingOIDC.AggregateID); err != nil { + return nil, err + } + projectAgg := ProjectAggregateFromWriteModel(&existingOIDC.WriteModel) + var backChannelLogout, loginBaseURI *string + if oidc.BackChannelLogoutURI != nil { + backChannelLogout = gu.Ptr(strings.TrimSpace(*oidc.BackChannelLogoutURI)) + } + + if oidc.LoginBaseURI != nil { + loginBaseURI = gu.Ptr(strings.TrimSpace(*oidc.LoginBaseURI)) + } + changedEvent, hasChanged, err := existingOIDC.NewChangedEvent( ctx, projectAgg, @@ -261,9 +293,9 @@ func (c *Commands) ChangeOIDCApplication(ctx context.Context, oidc *domain.OIDCA oidc.ClockSkew, trimStringSliceWhiteSpaces(oidc.AdditionalOrigins), oidc.SkipNativeAppSuccessPage, - strings.TrimSpace(oidc.BackChannelLogoutURI), + backChannelLogout, oidc.LoginVersion, - strings.TrimSpace(oidc.LoginBaseURI), + loginBaseURI, ) if err != nil { return nil, err @@ -301,6 +333,11 @@ func (c *Commands) ChangeOIDCApplicationSecret(ctx context.Context, projectID, a if !existingOIDC.IsOIDC() { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-Ghrh3", "Errors.Project.App.IsNotOIDC") } + + if err := c.checkPermissionUpdateApplication(ctx, existingOIDC.ResourceOwner, existingOIDC.AggregateID); err != nil { + return nil, err + } + encodedHash, plain, err := c.newHashedSecret(ctx, c.eventstore.Filter) //nolint:staticcheck if err != nil { return nil, err @@ -322,37 +359,6 @@ func (c *Commands) ChangeOIDCApplicationSecret(ctx context.Context, projectID, a return result, err } -func (c *Commands) VerifyOIDCClientSecret(ctx context.Context, projectID, appID, secret string) (err error) { - ctx, span := tracing.NewSpan(ctx) - defer func() { span.EndWithError(err) }() - - app, err := c.getOIDCAppWriteModel(ctx, projectID, appID, "") - if err != nil { - return err - } - if !app.State.Exists() { - return zerrors.ThrowPreconditionFailed(nil, "COMMAND-D8hba", "Errors.Project.App.NotExisting") - } - if !app.IsOIDC() { - return zerrors.ThrowInvalidArgument(nil, "COMMAND-BHgn2", "Errors.Project.App.IsNotOIDC") - } - if app.HashedSecret == "" { - return zerrors.ThrowPreconditionFailed(nil, "COMMAND-D6hba", "Errors.Project.App.OIDCConfigInvalid") - } - - projectAgg := ProjectAggregateFromWriteModel(&app.WriteModel) - ctx, spanPasswordComparison := tracing.NewNamedSpan(ctx, "passwap.Verify") - updated, err := c.secretHasher.Verify(app.HashedSecret, secret) - spanPasswordComparison.EndWithError(err) - if err != nil { - return zerrors.ThrowInvalidArgument(err, "COMMAND-Bz542", "Errors.Project.App.ClientSecretInvalid") - } - if updated != "" { - c.oidcUpdateSecret(ctx, projectAgg, appID, updated) - } - return nil -} - func (c *Commands) OIDCUpdateSecret(ctx context.Context, appID, projectID, resourceOwner, updated string) { agg := project_repo.NewAggregate(projectID, resourceOwner) c.oidcUpdateSecret(ctx, &agg.Aggregate, appID, updated) diff --git a/internal/command/project_application_oidc_model.go b/internal/command/project_application_oidc_model.go index 603ebdcda2..375bb26f5e 100644 --- a/internal/command/project_application_oidc_model.go +++ b/internal/command/project_application_oidc_model.go @@ -258,77 +258,77 @@ func (wm *OIDCApplicationWriteModel) NewChangedEvent( postLogoutRedirectURIs []string, responseTypes []domain.OIDCResponseType, grantTypes []domain.OIDCGrantType, - appType domain.OIDCApplicationType, - authMethodType domain.OIDCAuthMethodType, - oidcVersion domain.OIDCVersion, - accessTokenType domain.OIDCTokenType, + appType *domain.OIDCApplicationType, + authMethodType *domain.OIDCAuthMethodType, + oidcVersion *domain.OIDCVersion, + accessTokenType *domain.OIDCTokenType, devMode, accessTokenRoleAssertion, idTokenRoleAssertion, - idTokenUserinfoAssertion bool, - clockSkew time.Duration, + idTokenUserinfoAssertion *bool, + clockSkew *time.Duration, additionalOrigins []string, - skipNativeAppSuccessPage bool, - backChannelLogoutURI string, - loginVersion domain.LoginVersion, - loginBaseURI string, + skipNativeAppSuccessPage *bool, + backChannelLogoutURI *string, + loginVersion *domain.LoginVersion, + loginBaseURI *string, ) (*project.OIDCConfigChangedEvent, bool, error) { changes := make([]project.OIDCConfigChanges, 0) var err error - if !slices.Equal(wm.RedirectUris, redirectURIS) { + if redirectURIS != nil && !slices.Equal(wm.RedirectUris, redirectURIS) { changes = append(changes, project.ChangeRedirectURIs(redirectURIS)) } - if !slices.Equal(wm.ResponseTypes, responseTypes) { + if responseTypes != nil && !slices.Equal(wm.ResponseTypes, responseTypes) { changes = append(changes, project.ChangeResponseTypes(responseTypes)) } - if !slices.Equal(wm.GrantTypes, grantTypes) { + if grantTypes != nil && !slices.Equal(wm.GrantTypes, grantTypes) { changes = append(changes, project.ChangeGrantTypes(grantTypes)) } - if wm.ApplicationType != appType { - changes = append(changes, project.ChangeApplicationType(appType)) + if appType != nil && wm.ApplicationType != *appType { + changes = append(changes, project.ChangeApplicationType(*appType)) } - if wm.AuthMethodType != authMethodType { - changes = append(changes, project.ChangeAuthMethodType(authMethodType)) + if authMethodType != nil && wm.AuthMethodType != *authMethodType { + changes = append(changes, project.ChangeAuthMethodType(*authMethodType)) } - if !slices.Equal(wm.PostLogoutRedirectUris, postLogoutRedirectURIs) { + if postLogoutRedirectURIs != nil && !slices.Equal(wm.PostLogoutRedirectUris, postLogoutRedirectURIs) { changes = append(changes, project.ChangePostLogoutRedirectURIs(postLogoutRedirectURIs)) } - if wm.OIDCVersion != oidcVersion { - changes = append(changes, project.ChangeVersion(oidcVersion)) + if oidcVersion != nil && wm.OIDCVersion != *oidcVersion { + changes = append(changes, project.ChangeVersion(*oidcVersion)) } - if wm.DevMode != devMode { - changes = append(changes, project.ChangeDevMode(devMode)) + if devMode != nil && wm.DevMode != *devMode { + changes = append(changes, project.ChangeDevMode(*devMode)) } - if wm.AccessTokenType != accessTokenType { - changes = append(changes, project.ChangeAccessTokenType(accessTokenType)) + if accessTokenType != nil && wm.AccessTokenType != *accessTokenType { + changes = append(changes, project.ChangeAccessTokenType(*accessTokenType)) } - if wm.AccessTokenRoleAssertion != accessTokenRoleAssertion { - changes = append(changes, project.ChangeAccessTokenRoleAssertion(accessTokenRoleAssertion)) + if accessTokenRoleAssertion != nil && wm.AccessTokenRoleAssertion != *accessTokenRoleAssertion { + changes = append(changes, project.ChangeAccessTokenRoleAssertion(*accessTokenRoleAssertion)) } - if wm.IDTokenRoleAssertion != idTokenRoleAssertion { - changes = append(changes, project.ChangeIDTokenRoleAssertion(idTokenRoleAssertion)) + if idTokenRoleAssertion != nil && wm.IDTokenRoleAssertion != *idTokenRoleAssertion { + changes = append(changes, project.ChangeIDTokenRoleAssertion(*idTokenRoleAssertion)) } - if wm.IDTokenUserinfoAssertion != idTokenUserinfoAssertion { - changes = append(changes, project.ChangeIDTokenUserinfoAssertion(idTokenUserinfoAssertion)) + if idTokenUserinfoAssertion != nil && wm.IDTokenUserinfoAssertion != *idTokenUserinfoAssertion { + changes = append(changes, project.ChangeIDTokenUserinfoAssertion(*idTokenUserinfoAssertion)) } - if wm.ClockSkew != clockSkew { - changes = append(changes, project.ChangeClockSkew(clockSkew)) + if clockSkew != nil && wm.ClockSkew != *clockSkew { + changes = append(changes, project.ChangeClockSkew(*clockSkew)) } - if !slices.Equal(wm.AdditionalOrigins, additionalOrigins) { + if additionalOrigins != nil && !slices.Equal(wm.AdditionalOrigins, additionalOrigins) { changes = append(changes, project.ChangeAdditionalOrigins(additionalOrigins)) } - if wm.SkipNativeAppSuccessPage != skipNativeAppSuccessPage { - changes = append(changes, project.ChangeSkipNativeAppSuccessPage(skipNativeAppSuccessPage)) + if skipNativeAppSuccessPage != nil && wm.SkipNativeAppSuccessPage != *skipNativeAppSuccessPage { + changes = append(changes, project.ChangeSkipNativeAppSuccessPage(*skipNativeAppSuccessPage)) } - if wm.BackChannelLogoutURI != backChannelLogoutURI { - changes = append(changes, project.ChangeBackChannelLogoutURI(backChannelLogoutURI)) + if backChannelLogoutURI != nil && wm.BackChannelLogoutURI != *backChannelLogoutURI { + changes = append(changes, project.ChangeBackChannelLogoutURI(*backChannelLogoutURI)) } - if wm.LoginVersion != loginVersion { - changes = append(changes, project.ChangeOIDCLoginVersion(loginVersion)) + if loginVersion != nil && wm.LoginVersion != *loginVersion { + changes = append(changes, project.ChangeOIDCLoginVersion(*loginVersion)) } - if wm.LoginBaseURI != loginBaseURI { - changes = append(changes, project.ChangeOIDCLoginBaseURI(loginBaseURI)) + if loginBaseURI != nil && wm.LoginBaseURI != *loginBaseURI { + changes = append(changes, project.ChangeOIDCLoginBaseURI(*loginBaseURI)) } if len(changes) == 0 { diff --git a/internal/command/project_application_oidc_test.go b/internal/command/project_application_oidc_test.go index 4b9f5bf94f..d728ffca45 100644 --- a/internal/command/project_application_oidc_test.go +++ b/internal/command/project_application_oidc_test.go @@ -2,18 +2,14 @@ package command import ( "context" - "io" "testing" "time" + "github.com/muhlemmer/gu" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/zitadel/passwap" - "github.com/zitadel/passwap/bcrypt" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/command/preparation" - "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore/v1/models" @@ -406,6 +402,8 @@ func TestAddOIDCApp(t *testing.T) { } func TestCommandSide_AddOIDCApplication(t *testing.T) { + t.Parallel() + type fields struct { eventstore func(t *testing.T) *eventstore.Eventstore idGenerator id.Generator @@ -502,6 +500,7 @@ func TestCommandSide_AddOIDCApplication(t *testing.T) { domain.PrivateLabelingSettingUnspecified), ), ), + expectFilter(), expectPush( project.NewApplicationAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -543,24 +542,24 @@ func TestCommandSide_AddOIDCApplication(t *testing.T) { AggregateID: "project1", }, AppName: "app", - AuthMethodType: domain.OIDCAuthMethodTypePost, - OIDCVersion: domain.OIDCVersionV1, + AuthMethodType: gu.Ptr(domain.OIDCAuthMethodTypePost), + OIDCVersion: gu.Ptr(domain.OIDCVersionV1), RedirectUris: []string{" https://test.ch "}, ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}, - ApplicationType: domain.OIDCApplicationTypeWeb, + ApplicationType: gu.Ptr(domain.OIDCApplicationTypeWeb), PostLogoutRedirectUris: []string{" https://test.ch/logout "}, - DevMode: true, - AccessTokenType: domain.OIDCTokenTypeBearer, - AccessTokenRoleAssertion: true, - IDTokenRoleAssertion: true, - IDTokenUserinfoAssertion: true, - ClockSkew: time.Second * 1, + DevMode: gu.Ptr(true), + AccessTokenType: gu.Ptr(domain.OIDCTokenTypeBearer), + AccessTokenRoleAssertion: gu.Ptr(true), + IDTokenRoleAssertion: gu.Ptr(true), + IDTokenUserinfoAssertion: gu.Ptr(true), + ClockSkew: gu.Ptr(time.Second * 1), AdditionalOrigins: []string{" https://sub.test.ch "}, - SkipNativeAppSuccessPage: true, - BackChannelLogoutURI: " https://test.ch/backchannel ", - LoginVersion: domain.LoginVersion2, - LoginBaseURI: " https://login.test.ch ", + SkipNativeAppSuccessPage: gu.Ptr(true), + BackChannelLogoutURI: gu.Ptr(" https://test.ch/backchannel "), + LoginVersion: gu.Ptr(domain.LoginVersion2), + LoginBaseURI: gu.Ptr(" https://login.test.ch "), }, resourceOwner: "org1", }, @@ -574,24 +573,24 @@ func TestCommandSide_AddOIDCApplication(t *testing.T) { AppName: "app", ClientID: "client1", ClientSecretString: "secret", - AuthMethodType: domain.OIDCAuthMethodTypePost, - OIDCVersion: domain.OIDCVersionV1, + AuthMethodType: gu.Ptr(domain.OIDCAuthMethodTypePost), + OIDCVersion: gu.Ptr(domain.OIDCVersionV1), RedirectUris: []string{"https://test.ch"}, ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}, - ApplicationType: domain.OIDCApplicationTypeWeb, + ApplicationType: gu.Ptr(domain.OIDCApplicationTypeWeb), PostLogoutRedirectUris: []string{"https://test.ch/logout"}, - DevMode: true, - AccessTokenType: domain.OIDCTokenTypeBearer, - AccessTokenRoleAssertion: true, - IDTokenRoleAssertion: true, - IDTokenUserinfoAssertion: true, - ClockSkew: time.Second * 1, + DevMode: gu.Ptr(true), + AccessTokenType: gu.Ptr(domain.OIDCTokenTypeBearer), + AccessTokenRoleAssertion: gu.Ptr(true), + IDTokenRoleAssertion: gu.Ptr(true), + IDTokenUserinfoAssertion: gu.Ptr(true), + ClockSkew: gu.Ptr(time.Second * 1), AdditionalOrigins: []string{"https://sub.test.ch"}, - SkipNativeAppSuccessPage: true, - BackChannelLogoutURI: "https://test.ch/backchannel", - LoginVersion: domain.LoginVersion2, - LoginBaseURI: "https://login.test.ch", + SkipNativeAppSuccessPage: gu.Ptr(true), + BackChannelLogoutURI: gu.Ptr("https://test.ch/backchannel"), + LoginVersion: gu.Ptr(domain.LoginVersion2), + LoginBaseURI: gu.Ptr("https://login.test.ch"), State: domain.AppStateActive, Compliance: &domain.Compliance{}, }, @@ -609,6 +608,7 @@ func TestCommandSide_AddOIDCApplication(t *testing.T) { domain.PrivateLabelingSettingUnspecified), ), ), + expectFilter(), expectPush( project.NewApplicationAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -650,24 +650,24 @@ func TestCommandSide_AddOIDCApplication(t *testing.T) { AggregateID: "project1", }, AppName: "app", - AuthMethodType: domain.OIDCAuthMethodTypePost, - OIDCVersion: domain.OIDCVersionV1, + AuthMethodType: gu.Ptr(domain.OIDCAuthMethodTypePost), + OIDCVersion: gu.Ptr(domain.OIDCVersionV1), RedirectUris: []string{"https://test.ch"}, ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}, - ApplicationType: domain.OIDCApplicationTypeWeb, + ApplicationType: gu.Ptr(domain.OIDCApplicationTypeWeb), PostLogoutRedirectUris: []string{"https://test.ch/logout"}, - DevMode: true, - AccessTokenType: domain.OIDCTokenTypeBearer, - AccessTokenRoleAssertion: true, - IDTokenRoleAssertion: true, - IDTokenUserinfoAssertion: true, - ClockSkew: time.Second * 1, + DevMode: gu.Ptr(true), + AccessTokenType: gu.Ptr(domain.OIDCTokenTypeBearer), + AccessTokenRoleAssertion: gu.Ptr(true), + IDTokenRoleAssertion: gu.Ptr(true), + IDTokenUserinfoAssertion: gu.Ptr(true), + ClockSkew: gu.Ptr(time.Second * 1), AdditionalOrigins: []string{"https://sub.test.ch"}, - SkipNativeAppSuccessPage: true, - BackChannelLogoutURI: "https://test.ch/backchannel", - LoginVersion: domain.LoginVersion2, - LoginBaseURI: "https://login.test.ch", + SkipNativeAppSuccessPage: gu.Ptr(true), + BackChannelLogoutURI: gu.Ptr("https://test.ch/backchannel"), + LoginVersion: gu.Ptr(domain.LoginVersion2), + LoginBaseURI: gu.Ptr("https://login.test.ch"), }, resourceOwner: "org1", }, @@ -681,24 +681,24 @@ func TestCommandSide_AddOIDCApplication(t *testing.T) { AppName: "app", ClientID: "client1", ClientSecretString: "secret", - AuthMethodType: domain.OIDCAuthMethodTypePost, - OIDCVersion: domain.OIDCVersionV1, + AuthMethodType: gu.Ptr(domain.OIDCAuthMethodTypePost), + OIDCVersion: gu.Ptr(domain.OIDCVersionV1), RedirectUris: []string{"https://test.ch"}, ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}, - ApplicationType: domain.OIDCApplicationTypeWeb, + ApplicationType: gu.Ptr(domain.OIDCApplicationTypeWeb), PostLogoutRedirectUris: []string{"https://test.ch/logout"}, - DevMode: true, - AccessTokenType: domain.OIDCTokenTypeBearer, - AccessTokenRoleAssertion: true, - IDTokenRoleAssertion: true, - IDTokenUserinfoAssertion: true, - ClockSkew: time.Second * 1, + DevMode: gu.Ptr(true), + AccessTokenType: gu.Ptr(domain.OIDCTokenTypeBearer), + AccessTokenRoleAssertion: gu.Ptr(true), + IDTokenRoleAssertion: gu.Ptr(true), + IDTokenUserinfoAssertion: gu.Ptr(true), + ClockSkew: gu.Ptr(time.Second * 1), AdditionalOrigins: []string{"https://sub.test.ch"}, - SkipNativeAppSuccessPage: true, - BackChannelLogoutURI: "https://test.ch/backchannel", - LoginVersion: domain.LoginVersion2, - LoginBaseURI: "https://login.test.ch", + SkipNativeAppSuccessPage: gu.Ptr(true), + BackChannelLogoutURI: gu.Ptr("https://test.ch/backchannel"), + LoginVersion: gu.Ptr(domain.LoginVersion2), + LoginBaseURI: gu.Ptr("https://login.test.ch"), State: domain.AppStateActive, Compliance: &domain.Compliance{}, }, @@ -707,6 +707,7 @@ func TestCommandSide_AddOIDCApplication(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() c := &Commands{ eventstore: tt.fields.eventstore(t), idGenerator: tt.fields.idGenerator, @@ -714,6 +715,7 @@ func TestCommandSide_AddOIDCApplication(t *testing.T) { defaultSecretGenerators: &SecretGenerators{ ClientSecret: emptyConfig, }, + checkPermission: newMockPermissionCheckAllowed(), } c.setMilestonesCompletedForTest("instanceID") got, err := c.AddOIDCApplication(tt.args.ctx, tt.args.oidcApp, tt.args.resourceOwner) @@ -731,6 +733,7 @@ func TestCommandSide_AddOIDCApplication(t *testing.T) { } func TestCommandSide_ChangeOIDCApplication(t *testing.T) { + t.Parallel() type fields struct { eventstore func(*testing.T) *eventstore.Eventstore } @@ -780,7 +783,7 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) { AggregateID: "project1", }, AppID: "", - AuthMethodType: domain.OIDCAuthMethodTypePost, + AuthMethodType: gu.Ptr(domain.OIDCAuthMethodTypePost), GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}, ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, }, @@ -802,7 +805,7 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) { AggregateID: "", }, AppID: "appid", - AuthMethodType: domain.OIDCAuthMethodTypePost, + AuthMethodType: gu.Ptr(domain.OIDCAuthMethodTypePost), GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}, ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, }, @@ -826,7 +829,7 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) { AggregateID: "project1", }, AppID: "app1", - AuthMethodType: domain.OIDCAuthMethodTypePost, + AuthMethodType: gu.Ptr(domain.OIDCAuthMethodTypePost), GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}, ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, }, @@ -875,6 +878,7 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) { ), ), ), + expectFilter(), ), }, args: args{ @@ -885,24 +889,24 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) { }, AppID: "app1", AppName: "app", - AuthMethodType: domain.OIDCAuthMethodTypePost, - OIDCVersion: domain.OIDCVersionV1, + AuthMethodType: gu.Ptr(domain.OIDCAuthMethodTypePost), + OIDCVersion: gu.Ptr(domain.OIDCVersionV1), RedirectUris: []string{"https://test.ch"}, ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}, - ApplicationType: domain.OIDCApplicationTypeWeb, + ApplicationType: gu.Ptr(domain.OIDCApplicationTypeWeb), PostLogoutRedirectUris: []string{"https://test.ch/logout"}, - DevMode: true, - AccessTokenType: domain.OIDCTokenTypeBearer, - AccessTokenRoleAssertion: true, - IDTokenRoleAssertion: true, - IDTokenUserinfoAssertion: true, - ClockSkew: time.Second * 1, + DevMode: gu.Ptr(true), + AccessTokenType: gu.Ptr(domain.OIDCTokenTypeBearer), + AccessTokenRoleAssertion: gu.Ptr(true), + IDTokenRoleAssertion: gu.Ptr(true), + IDTokenUserinfoAssertion: gu.Ptr(true), + ClockSkew: gu.Ptr(time.Second * 1), AdditionalOrigins: []string{"https://sub.test.ch"}, - SkipNativeAppSuccessPage: true, - BackChannelLogoutURI: "https://test.ch/backchannel", - LoginVersion: domain.LoginVersion2, - LoginBaseURI: "https://login.test.ch", + SkipNativeAppSuccessPage: gu.Ptr(true), + BackChannelLogoutURI: gu.Ptr("https://test.ch/backchannel"), + LoginVersion: gu.Ptr(domain.LoginVersion2), + LoginBaseURI: gu.Ptr("https://login.test.ch"), }, resourceOwner: "org1", }, @@ -949,6 +953,7 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) { ), ), ), + expectFilter(), ), }, args: args{ @@ -959,24 +964,24 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) { }, AppID: "app1", AppName: "app", - AuthMethodType: domain.OIDCAuthMethodTypePost, - OIDCVersion: domain.OIDCVersionV1, + AuthMethodType: gu.Ptr(domain.OIDCAuthMethodTypePost), + OIDCVersion: gu.Ptr(domain.OIDCVersionV1), RedirectUris: []string{"https://test.ch "}, ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}, - ApplicationType: domain.OIDCApplicationTypeWeb, + ApplicationType: gu.Ptr(domain.OIDCApplicationTypeWeb), PostLogoutRedirectUris: []string{" https://test.ch/logout"}, - DevMode: true, - AccessTokenType: domain.OIDCTokenTypeBearer, - AccessTokenRoleAssertion: true, - IDTokenRoleAssertion: true, - IDTokenUserinfoAssertion: true, - ClockSkew: time.Second * 1, + DevMode: gu.Ptr(true), + AccessTokenType: gu.Ptr(domain.OIDCTokenTypeBearer), + AccessTokenRoleAssertion: gu.Ptr(true), + IDTokenRoleAssertion: gu.Ptr(true), + IDTokenUserinfoAssertion: gu.Ptr(true), + ClockSkew: gu.Ptr(time.Second * 1), AdditionalOrigins: []string{" https://sub.test.ch "}, - SkipNativeAppSuccessPage: true, - BackChannelLogoutURI: " https://test.ch/backchannel ", - LoginVersion: domain.LoginVersion2, - LoginBaseURI: " https://login.test.ch ", + SkipNativeAppSuccessPage: gu.Ptr(true), + BackChannelLogoutURI: gu.Ptr(" https://test.ch/backchannel "), + LoginVersion: gu.Ptr(domain.LoginVersion2), + LoginBaseURI: gu.Ptr(" https://login.test.ch "), }, resourceOwner: "org1", }, @@ -985,7 +990,7 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) { }, }, { - name: "change oidc app, ok", + name: "partial change oidc app, ok", fields: fields{ eventstore: expectEventstore( expectFilter( @@ -1023,6 +1028,7 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) { ), ), ), + expectFilter(), expectPush( newOIDCAppChangedEvent(context.Background(), "app1", @@ -1037,26 +1043,11 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) { ObjectRoot: models.ObjectRoot{ AggregateID: "project1", }, - AppID: "app1", - AppName: "app", - AuthMethodType: domain.OIDCAuthMethodTypePost, - OIDCVersion: domain.OIDCVersionV1, - RedirectUris: []string{" https://test-change.ch "}, - ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, - GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}, - ApplicationType: domain.OIDCApplicationTypeWeb, - PostLogoutRedirectUris: []string{" https://test-change.ch/logout "}, - DevMode: true, - AccessTokenType: domain.OIDCTokenTypeJWT, - AccessTokenRoleAssertion: false, - IDTokenRoleAssertion: false, - IDTokenUserinfoAssertion: false, - ClockSkew: time.Second * 2, - AdditionalOrigins: []string{"https://sub.test.ch"}, - SkipNativeAppSuccessPage: true, - BackChannelLogoutURI: "https://test.ch/backchannel", - LoginVersion: domain.LoginVersion2, - LoginBaseURI: "https://login.test.ch", + AppID: "app1", + AppName: "app", + AuthMethodType: gu.Ptr(domain.OIDCAuthMethodTypeBasic), + GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}, + ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, }, resourceOwner: "org1", }, @@ -1069,24 +1060,24 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) { AppID: "app1", ClientID: "client1@project", AppName: "app", - AuthMethodType: domain.OIDCAuthMethodTypePost, - OIDCVersion: domain.OIDCVersionV1, - RedirectUris: []string{"https://test-change.ch"}, + AuthMethodType: gu.Ptr(domain.OIDCAuthMethodTypeBasic), + OIDCVersion: gu.Ptr(domain.OIDCVersionV1), + RedirectUris: []string{"https://test.ch"}, ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}, - ApplicationType: domain.OIDCApplicationTypeWeb, - PostLogoutRedirectUris: []string{"https://test-change.ch/logout"}, - DevMode: true, - AccessTokenType: domain.OIDCTokenTypeJWT, - AccessTokenRoleAssertion: false, - IDTokenRoleAssertion: false, - IDTokenUserinfoAssertion: false, - ClockSkew: time.Second * 2, + ApplicationType: gu.Ptr(domain.OIDCApplicationTypeWeb), + PostLogoutRedirectUris: []string{"https://test.ch/logout"}, + DevMode: gu.Ptr(false), + AccessTokenType: gu.Ptr(domain.OIDCTokenTypeBearer), + AccessTokenRoleAssertion: gu.Ptr(true), + IDTokenRoleAssertion: gu.Ptr(true), + IDTokenUserinfoAssertion: gu.Ptr(true), + ClockSkew: gu.Ptr(time.Second * 1), AdditionalOrigins: []string{"https://sub.test.ch"}, - SkipNativeAppSuccessPage: true, - BackChannelLogoutURI: "https://test.ch/backchannel", - LoginVersion: domain.LoginVersion2, - LoginBaseURI: "https://login.test.ch", + SkipNativeAppSuccessPage: gu.Ptr(true), + BackChannelLogoutURI: gu.Ptr("https://test.ch/backchannel"), + LoginVersion: gu.Ptr(domain.LoginVersion1), + LoginBaseURI: gu.Ptr(""), Compliance: &domain.Compliance{}, State: domain.AppStateActive, }, @@ -1095,10 +1086,12 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + // t.Parallel() r := &Commands{ - eventstore: tt.fields.eventstore(t), + eventstore: tt.fields.eventstore(t), + checkPermission: newMockPermissionCheckAllowed(), } - got, err := r.ChangeOIDCApplication(tt.args.ctx, tt.args.oidcApp, tt.args.resourceOwner) + got, err := r.UpdateOIDCApplication(tt.args.ctx, tt.args.oidcApp, tt.args.resourceOwner) if tt.res.err == nil { assert.NoError(t, err) } @@ -1113,6 +1106,8 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) { } func TestCommandSide_ChangeOIDCApplicationSecret(t *testing.T) { + t.Parallel() + type fields struct { eventstore func(*testing.T) *eventstore.Eventstore } @@ -1242,36 +1237,40 @@ func TestCommandSide_ChangeOIDCApplicationSecret(t *testing.T) { AppName: "app", ClientID: "client1@project", ClientSecretString: "secret", - AuthMethodType: domain.OIDCAuthMethodTypePost, - OIDCVersion: domain.OIDCVersionV1, + AuthMethodType: gu.Ptr(domain.OIDCAuthMethodTypePost), + OIDCVersion: gu.Ptr(domain.OIDCVersionV1), RedirectUris: []string{"https://test.ch"}, ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}, - ApplicationType: domain.OIDCApplicationTypeWeb, + ApplicationType: gu.Ptr(domain.OIDCApplicationTypeWeb), PostLogoutRedirectUris: []string{"https://test.ch/logout"}, - DevMode: true, - AccessTokenType: domain.OIDCTokenTypeBearer, - AccessTokenRoleAssertion: true, - IDTokenRoleAssertion: true, - IDTokenUserinfoAssertion: true, - ClockSkew: time.Second * 1, + DevMode: gu.Ptr(true), + AccessTokenType: gu.Ptr(domain.OIDCTokenTypeBearer), + AccessTokenRoleAssertion: gu.Ptr(true), + IDTokenRoleAssertion: gu.Ptr(true), + IDTokenUserinfoAssertion: gu.Ptr(true), + ClockSkew: gu.Ptr(time.Second * 1), AdditionalOrigins: []string{"https://sub.test.ch"}, - SkipNativeAppSuccessPage: false, - BackChannelLogoutURI: "", - LoginVersion: domain.LoginVersionUnspecified, + SkipNativeAppSuccessPage: gu.Ptr(false), + BackChannelLogoutURI: gu.Ptr(""), + LoginVersion: gu.Ptr(domain.LoginVersionUnspecified), + LoginBaseURI: gu.Ptr(""), State: domain.AppStateActive, }, }, }, } for _, tt := range tests { - t.Run(tt.name, func(*testing.T) { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + r := &Commands{ eventstore: tt.fields.eventstore(t), newHashedSecret: mockHashedSecret("secret"), defaultSecretGenerators: &SecretGenerators{ ClientSecret: emptyConfig, }, + checkPermission: newMockPermissionCheckAllowed(), } got, err := r.ChangeOIDCApplicationSecret(tt.args.ctx, tt.args.projectID, tt.args.appID, tt.args.resourceOwner) if tt.res.err == nil { @@ -1289,16 +1288,7 @@ func TestCommandSide_ChangeOIDCApplicationSecret(t *testing.T) { func newOIDCAppChangedEvent(ctx context.Context, appID, projectID, resourceOwner string) *project.OIDCConfigChangedEvent { changes := []project.OIDCConfigChanges{ - project.ChangeRedirectURIs([]string{"https://test-change.ch"}), - project.ChangePostLogoutRedirectURIs([]string{"https://test-change.ch/logout"}), - project.ChangeDevMode(true), - project.ChangeAccessTokenType(domain.OIDCTokenTypeJWT), - project.ChangeAccessTokenRoleAssertion(false), - project.ChangeIDTokenRoleAssertion(false), - project.ChangeIDTokenUserinfoAssertion(false), - project.ChangeClockSkew(time.Second * 2), - project.ChangeOIDCLoginVersion(domain.LoginVersion2), - project.ChangeOIDCLoginBaseURI("https://login.test.ch"), + project.ChangeAuthMethodType(domain.OIDCAuthMethodTypeBasic), } event, _ := project.NewOIDCConfigChangedEvent(ctx, &project.NewAggregate(projectID, resourceOwner).Aggregate, @@ -1307,168 +1297,3 @@ func newOIDCAppChangedEvent(ctx context.Context, appID, projectID, resourceOwner ) return event } - -func TestCommands_VerifyOIDCClientSecret(t *testing.T) { - hasher := &crypto.Hasher{ - Swapper: passwap.NewSwapper(bcrypt.New(bcrypt.MinCost)), - } - hashedSecret, err := hasher.Hash("secret") - require.NoError(t, err) - agg := project.NewAggregate("projectID", "orgID") - - tests := []struct { - name string - secret string - eventstore func(*testing.T) *eventstore.Eventstore - wantErr error - }{ - { - name: "filter error", - eventstore: expectEventstore( - expectFilterError(io.ErrClosedPipe), - ), - wantErr: io.ErrClosedPipe, - }, - { - name: "app not exists", - eventstore: expectEventstore( - expectFilter(), - ), - wantErr: zerrors.ThrowPreconditionFailed(nil, "COMMAND-D8hba", "Errors.Project.App.NotExisting"), - }, - { - name: "wrong app type", - eventstore: expectEventstore( - expectFilter( - eventFromEventPusher( - project.NewApplicationAddedEvent(context.Background(), &agg.Aggregate, "appID", "appName"), - ), - ), - ), - wantErr: zerrors.ThrowInvalidArgument(nil, "COMMAND-BHgn2", "Errors.Project.App.IsNotOIDC"), - }, - { - name: "no secret set", - eventstore: expectEventstore( - expectFilter( - eventFromEventPusher( - project.NewApplicationAddedEvent(context.Background(), &agg.Aggregate, "appID", "appName"), - ), - eventFromEventPusher( - project.NewOIDCConfigAddedEvent(context.Background(), - &agg.Aggregate, - domain.OIDCVersionV1, - "appID", - "client1@project", - "", - []string{"https://test.ch"}, - []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, - []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}, - domain.OIDCApplicationTypeWeb, - domain.OIDCAuthMethodTypePost, - []string{"https://test.ch/logout"}, - true, - domain.OIDCTokenTypeBearer, - true, - true, - true, - time.Second*1, - []string{"https://sub.test.ch"}, - false, - "", - domain.LoginVersionUnspecified, - "", - ), - ), - ), - ), - wantErr: zerrors.ThrowPreconditionFailed(nil, "COMMAND-D6hba", "Errors.Project.App.OIDCConfigInvalid"), - }, - { - name: "check succeeded", - secret: "secret", - eventstore: expectEventstore( - expectFilter( - eventFromEventPusher( - project.NewApplicationAddedEvent(context.Background(), &agg.Aggregate, "appID", "appName"), - ), - eventFromEventPusher( - project.NewOIDCConfigAddedEvent(context.Background(), - &agg.Aggregate, - domain.OIDCVersionV1, - "appID", - "client1@project", - hashedSecret, - []string{"https://test.ch"}, - []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, - []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}, - domain.OIDCApplicationTypeWeb, - domain.OIDCAuthMethodTypePost, - []string{"https://test.ch/logout"}, - true, - domain.OIDCTokenTypeBearer, - true, - true, - true, - time.Second*1, - []string{"https://sub.test.ch"}, - false, - "", - domain.LoginVersionUnspecified, - "", - ), - ), - ), - ), - }, - { - name: "check failed", - secret: "wrong!", - eventstore: expectEventstore( - expectFilter( - eventFromEventPusher( - project.NewApplicationAddedEvent(context.Background(), &agg.Aggregate, "appID", "appName"), - ), - eventFromEventPusher( - project.NewOIDCConfigAddedEvent(context.Background(), - &agg.Aggregate, - domain.OIDCVersionV1, - "appID", - "client1@project", - hashedSecret, - []string{"https://test.ch"}, - []domain.OIDCResponseType{domain.OIDCResponseTypeCode}, - []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode}, - domain.OIDCApplicationTypeWeb, - domain.OIDCAuthMethodTypePost, - []string{"https://test.ch/logout"}, - true, - domain.OIDCTokenTypeBearer, - true, - true, - true, - time.Second*1, - []string{"https://sub.test.ch"}, - false, - "", - domain.LoginVersionUnspecified, - "", - ), - ), - ), - ), - wantErr: zerrors.ThrowInvalidArgument(err, "COMMAND-Bz542", "Errors.Project.App.ClientSecretInvalid"), - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - c := &Commands{ - eventstore: tt.eventstore(t), - secretHasher: hasher, - } - err := c.VerifyOIDCClientSecret(context.Background(), "projectID", "appID", tt.secret) - c.jobs.Wait() - require.ErrorIs(t, err, tt.wantErr) - }) - } -} diff --git a/internal/command/project_application_saml.go b/internal/command/project_application_saml.go index b14bed0758..9b1dc9e97a 100644 --- a/internal/command/project_application_saml.go +++ b/internal/command/project_application_saml.go @@ -3,6 +3,7 @@ package command import ( "context" + "github.com/muhlemmer/gu" "github.com/zitadel/saml/pkg/provider/xml" "github.com/zitadel/zitadel/internal/domain" @@ -16,10 +17,22 @@ func (c *Commands) AddSAMLApplication(ctx context.Context, application *domain.S return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-35Fn0", "Errors.Project.App.Invalid") } - if err := c.checkProjectExists(ctx, application.AggregateID, resourceOwner); err != nil { + projectResOwner, err := c.checkProjectExists(ctx, application.AggregateID, resourceOwner) + if err != nil { return nil, err } + if resourceOwner == "" { + resourceOwner = projectResOwner + } + addedApplication := NewSAMLApplicationWriteModel(application.AggregateID, resourceOwner) + if err := c.eventstore.FilterToQueryReducer(ctx, addedApplication); err != nil { + return nil, err + } + if err := c.checkPermissionUpdateApplication(ctx, addedApplication.ResourceOwner, addedApplication.AggregateID); err != nil { + return nil, err + } + projectAgg := ProjectAggregateFromWriteModel(&addedApplication.WriteModel) events, err := c.addSAMLApplication(ctx, projectAgg, application) if err != nil { @@ -49,12 +62,8 @@ func (c *Commands) addSAMLApplication(ctx context.Context, projectAgg *eventstor return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-1n9df", "Errors.Project.App.Invalid") } - if samlApp.Metadata == nil && samlApp.MetadataURL == "" { - return nil, zerrors.ThrowInvalidArgument(nil, "SAML-podix9", "Errors.Project.App.SAMLMetadataMissing") - } - - if samlApp.MetadataURL != "" { - data, err := xml.ReadMetadataFromURL(c.httpClient, samlApp.MetadataURL) + if samlApp.MetadataURL != nil && *samlApp.MetadataURL != "" { + data, err := xml.ReadMetadataFromURL(c.httpClient, *samlApp.MetadataURL) if err != nil { return nil, zerrors.ThrowInvalidArgument(err, "SAML-wmqlo1", "Errors.Project.App.SAMLMetadataMissing") } @@ -78,14 +87,14 @@ func (c *Commands) addSAMLApplication(ctx context.Context, projectAgg *eventstor samlApp.AppID, string(entity.EntityID), samlApp.Metadata, - samlApp.MetadataURL, - samlApp.LoginVersion, - samlApp.LoginBaseURI, + gu.Value(samlApp.MetadataURL), + gu.Value(samlApp.LoginVersion), + gu.Value(samlApp.LoginBaseURI), ), }, nil } -func (c *Commands) ChangeSAMLApplication(ctx context.Context, samlApp *domain.SAMLApp, resourceOwner string) (*domain.SAMLApp, error) { +func (c *Commands) UpdateSAMLApplication(ctx context.Context, samlApp *domain.SAMLApp, resourceOwner string) (*domain.SAMLApp, error) { if !samlApp.IsValid() || samlApp.AppID == "" || samlApp.AggregateID == "" { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-5n9fs", "Errors.Project.App.SAMLConfigInvalid") } @@ -100,10 +109,15 @@ func (c *Commands) ChangeSAMLApplication(ctx context.Context, samlApp *domain.SA if !existingSAML.IsSAML() { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-GBr35", "Errors.Project.App.IsNotSAML") } + + if err := c.checkPermissionUpdateApplication(ctx, existingSAML.ResourceOwner, existingSAML.AggregateID); err != nil { + return nil, err + } + projectAgg := ProjectAggregateFromWriteModel(&existingSAML.WriteModel) - if samlApp.MetadataURL != "" { - data, err := xml.ReadMetadataFromURL(c.httpClient, samlApp.MetadataURL) + if samlApp.MetadataURL != nil && *samlApp.MetadataURL != "" { + data, err := xml.ReadMetadataFromURL(c.httpClient, *samlApp.MetadataURL) if err != nil { return nil, zerrors.ThrowInvalidArgument(err, "SAML-J3kg3", "Errors.Project.App.SAMLMetadataMissing") } diff --git a/internal/command/project_application_saml_model.go b/internal/command/project_application_saml_model.go index f219039b58..f3097914f3 100644 --- a/internal/command/project_application_saml_model.go +++ b/internal/command/project_application_saml_model.go @@ -2,7 +2,7 @@ package command import ( "context" - "reflect" + "slices" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" @@ -170,26 +170,26 @@ func (wm *SAMLApplicationWriteModel) NewChangedEvent( appID string, entityID string, metadata []byte, - metadataURL string, - loginVersion domain.LoginVersion, - loginBaseURI string, + metadataURL *string, + loginVersion *domain.LoginVersion, + loginBaseURI *string, ) (*project.SAMLConfigChangedEvent, bool, error) { changes := make([]project.SAMLConfigChanges, 0) var err error - if !reflect.DeepEqual(wm.Metadata, metadata) { + if metadata != nil && !slices.Equal(wm.Metadata, metadata) { changes = append(changes, project.ChangeMetadata(metadata)) } - if wm.MetadataURL != metadataURL { - changes = append(changes, project.ChangeMetadataURL(metadataURL)) + if metadataURL != nil && wm.MetadataURL != *metadataURL { + changes = append(changes, project.ChangeMetadataURL(*metadataURL)) } if wm.EntityID != entityID { changes = append(changes, project.ChangeEntityID(entityID)) } - if wm.LoginVersion != loginVersion { - changes = append(changes, project.ChangeSAMLLoginVersion(loginVersion)) + if loginVersion != nil && wm.LoginVersion != *loginVersion { + changes = append(changes, project.ChangeSAMLLoginVersion(*loginVersion)) } - if wm.LoginBaseURI != loginBaseURI { - changes = append(changes, project.ChangeSAMLLoginBaseURI(loginBaseURI)) + if loginBaseURI != nil && wm.LoginBaseURI != *loginBaseURI { + changes = append(changes, project.ChangeSAMLLoginBaseURI(*loginBaseURI)) } if len(changes) == 0 { diff --git a/internal/command/project_application_saml_test.go b/internal/command/project_application_saml_test.go index c6f6f7cf21..5d18d9587c 100644 --- a/internal/command/project_application_saml_test.go +++ b/internal/command/project_application_saml_test.go @@ -7,6 +7,7 @@ import ( "net/http" "testing" + "github.com/muhlemmer/gu" "github.com/stretchr/testify/assert" "github.com/zitadel/zitadel/internal/api/authz" @@ -49,6 +50,8 @@ var testMetadataChangedEntityID = []byte(` `) func TestCommandSide_AddSAMLApplication(t *testing.T) { + t.Parallel() + type fields struct { eventstore func(t *testing.T) *eventstore.Eventstore idGenerator id.Generator @@ -117,6 +120,7 @@ func TestCommandSide_AddSAMLApplication(t *testing.T) { domain.PrivateLabelingSettingUnspecified), ), ), + expectFilter(), ), }, args: args{ @@ -134,6 +138,37 @@ func TestCommandSide_AddSAMLApplication(t *testing.T) { err: zerrors.IsErrorInvalidArgument, }, }, + { + name: "empty metas, invalid argument error", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project", true, true, true, + domain.PrivateLabelingSettingUnspecified), + ), + ), + expectFilter(), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instanceID"), + samlApp: &domain.SAMLApp{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + AppName: "app", + EntityID: "https://test.com/saml/metadata", + }, + resourceOwner: "org1", + }, + res: res{ + err: zerrors.IsErrorInvalidArgument, + }, + }, { name: "create saml app, metadata not parsable", fields: fields{ @@ -146,6 +181,7 @@ func TestCommandSide_AddSAMLApplication(t *testing.T) { domain.PrivateLabelingSettingUnspecified), ), ), + expectFilter(), ), idGenerator: id_mock.NewIDGeneratorExpectIDs(t), }, @@ -158,7 +194,7 @@ func TestCommandSide_AddSAMLApplication(t *testing.T) { AppName: "app", EntityID: "https://test.com/saml/metadata", Metadata: []byte("test metadata"), - MetadataURL: "", + MetadataURL: gu.Ptr(""), }, resourceOwner: "org1", }, @@ -178,6 +214,7 @@ func TestCommandSide_AddSAMLApplication(t *testing.T) { domain.PrivateLabelingSettingUnspecified), ), ), + expectFilter(), expectPush( project.NewApplicationAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -206,7 +243,7 @@ func TestCommandSide_AddSAMLApplication(t *testing.T) { AppName: "app", EntityID: "https://test.com/saml/metadata", Metadata: testMetadata, - MetadataURL: "", + MetadataURL: gu.Ptr(""), }, resourceOwner: "org1", }, @@ -216,12 +253,14 @@ func TestCommandSide_AddSAMLApplication(t *testing.T) { AggregateID: "project1", ResourceOwner: "org1", }, - AppID: "app1", - AppName: "app", - EntityID: "https://test.com/saml/metadata", - Metadata: testMetadata, - MetadataURL: "", - State: domain.AppStateActive, + AppID: "app1", + AppName: "app", + EntityID: "https://test.com/saml/metadata", + Metadata: testMetadata, + MetadataURL: gu.Ptr(""), + State: domain.AppStateActive, + LoginVersion: gu.Ptr(domain.LoginVersionUnspecified), + LoginBaseURI: gu.Ptr(""), }, }, }, @@ -237,6 +276,7 @@ func TestCommandSide_AddSAMLApplication(t *testing.T) { domain.PrivateLabelingSettingUnspecified), ), ), + expectFilter(), expectPush( project.NewApplicationAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -265,9 +305,9 @@ func TestCommandSide_AddSAMLApplication(t *testing.T) { AppName: "app", EntityID: "https://test.com/saml/metadata", Metadata: testMetadata, - MetadataURL: "", - LoginVersion: domain.LoginVersion2, - LoginBaseURI: "https://test.com/login", + MetadataURL: gu.Ptr(""), + LoginVersion: gu.Ptr(domain.LoginVersion2), + LoginBaseURI: gu.Ptr("https://test.com/login"), }, resourceOwner: "org1", }, @@ -281,10 +321,10 @@ func TestCommandSide_AddSAMLApplication(t *testing.T) { AppName: "app", EntityID: "https://test.com/saml/metadata", Metadata: testMetadata, - MetadataURL: "", + MetadataURL: gu.Ptr(""), State: domain.AppStateActive, - LoginVersion: domain.LoginVersion2, - LoginBaseURI: "https://test.com/login", + LoginVersion: gu.Ptr(domain.LoginVersion2), + LoginBaseURI: gu.Ptr("https://test.com/login"), }, }, }, @@ -300,6 +340,7 @@ func TestCommandSide_AddSAMLApplication(t *testing.T) { domain.PrivateLabelingSettingUnspecified), ), ), + expectFilter(), expectPush( project.NewApplicationAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -329,7 +370,7 @@ func TestCommandSide_AddSAMLApplication(t *testing.T) { AppName: "app", EntityID: "https://test.com/saml/metadata", Metadata: nil, - MetadataURL: "http://localhost:8080/saml/metadata", + MetadataURL: gu.Ptr("http://localhost:8080/saml/metadata"), }, resourceOwner: "org1", }, @@ -339,12 +380,14 @@ func TestCommandSide_AddSAMLApplication(t *testing.T) { AggregateID: "project1", ResourceOwner: "org1", }, - AppID: "app1", - AppName: "app", - EntityID: "https://test.com/saml/metadata", - Metadata: testMetadata, - MetadataURL: "http://localhost:8080/saml/metadata", - State: domain.AppStateActive, + AppID: "app1", + AppName: "app", + EntityID: "https://test.com/saml/metadata", + Metadata: testMetadata, + MetadataURL: gu.Ptr("http://localhost:8080/saml/metadata"), + State: domain.AppStateActive, + LoginVersion: gu.Ptr(domain.LoginVersionUnspecified), + LoginBaseURI: gu.Ptr(""), }, }, }, @@ -360,6 +403,7 @@ func TestCommandSide_AddSAMLApplication(t *testing.T) { domain.PrivateLabelingSettingUnspecified), ), ), + expectFilter(), ), idGenerator: id_mock.NewIDGeneratorExpectIDs(t), httpClient: newTestClient(http.StatusNotFound, nil), @@ -373,7 +417,7 @@ func TestCommandSide_AddSAMLApplication(t *testing.T) { AppName: "app", EntityID: "https://test.com/saml/metadata", Metadata: nil, - MetadataURL: "http://localhost:8080/saml/metadata", + MetadataURL: gu.Ptr("http://localhost:8080/saml/metadata"), }, resourceOwner: "org1", }, @@ -385,10 +429,13 @@ func TestCommandSide_AddSAMLApplication(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() + c := &Commands{ - eventstore: tt.fields.eventstore(t), - idGenerator: tt.fields.idGenerator, - httpClient: tt.fields.httpClient, + eventstore: tt.fields.eventstore(t), + idGenerator: tt.fields.idGenerator, + httpClient: tt.fields.httpClient, + checkPermission: newMockPermissionCheckAllowed(), } c.setMilestonesCompletedForTest("instanceID") got, err := c.AddSAMLApplication(tt.args.ctx, tt.args.samlApp, tt.args.resourceOwner) @@ -406,6 +453,8 @@ func TestCommandSide_AddSAMLApplication(t *testing.T) { } func TestCommandSide_ChangeSAMLApplication(t *testing.T) { + t.Parallel() + type fields struct { eventstore func(t *testing.T) *eventstore.Eventstore httpClient *http.Client @@ -544,7 +593,7 @@ func TestCommandSide_ChangeSAMLApplication(t *testing.T) { AppID: "app1", EntityID: "https://test.com/saml/metadata", Metadata: nil, - MetadataURL: "http://localhost:8080/saml/metadata", + MetadataURL: gu.Ptr("http://localhost:8080/saml/metadata"), }, resourceOwner: "org1", }, @@ -590,7 +639,7 @@ func TestCommandSide_ChangeSAMLApplication(t *testing.T) { AppID: "app1", EntityID: "https://test.com/saml/metadata", Metadata: testMetadata, - MetadataURL: "", + MetadataURL: gu.Ptr(""), }, resourceOwner: "org1", }, @@ -646,7 +695,7 @@ func TestCommandSide_ChangeSAMLApplication(t *testing.T) { AppName: "app", EntityID: "https://test2.com/saml/metadata", Metadata: nil, - MetadataURL: "http://localhost:8080/saml/metadata", + MetadataURL: gu.Ptr("http://localhost:8080/saml/metadata"), }, resourceOwner: "org1", }, @@ -656,17 +705,19 @@ func TestCommandSide_ChangeSAMLApplication(t *testing.T) { AggregateID: "project1", ResourceOwner: "org1", }, - AppID: "app1", - AppName: "app", - EntityID: "https://test2.com/saml/metadata", - Metadata: testMetadataChangedEntityID, - MetadataURL: "http://localhost:8080/saml/metadata", - State: domain.AppStateActive, + AppID: "app1", + AppName: "app", + EntityID: "https://test2.com/saml/metadata", + Metadata: testMetadataChangedEntityID, + MetadataURL: gu.Ptr("http://localhost:8080/saml/metadata"), + State: domain.AppStateActive, + LoginVersion: gu.Ptr(domain.LoginVersionUnspecified), + LoginBaseURI: gu.Ptr(""), }, }, }, { - name: "change saml app, ok, metadata", + name: "partial change saml app, ok, metadata", fields: fields{ eventstore: expectEventstore( expectFilter( @@ -713,7 +764,7 @@ func TestCommandSide_ChangeSAMLApplication(t *testing.T) { AppName: "app", EntityID: "https://test2.com/saml/metadata", Metadata: testMetadataChangedEntityID, - MetadataURL: "", + MetadataURL: gu.Ptr(""), }, resourceOwner: "org1", }, @@ -723,15 +774,18 @@ func TestCommandSide_ChangeSAMLApplication(t *testing.T) { AggregateID: "project1", ResourceOwner: "org1", }, - AppID: "app1", - AppName: "app", - EntityID: "https://test2.com/saml/metadata", - Metadata: testMetadataChangedEntityID, - MetadataURL: "", - State: domain.AppStateActive, + AppID: "app1", + AppName: "app", + EntityID: "https://test2.com/saml/metadata", + Metadata: testMetadataChangedEntityID, + MetadataURL: gu.Ptr(""), + State: domain.AppStateActive, + LoginVersion: gu.Ptr(domain.LoginVersionUnspecified), + LoginBaseURI: gu.Ptr(""), }, }, - }, { + }, + { name: "change saml app, ok, loginversion", fields: fields{ eventstore: expectEventstore( @@ -781,9 +835,9 @@ func TestCommandSide_ChangeSAMLApplication(t *testing.T) { AppName: "app", EntityID: "https://test2.com/saml/metadata", Metadata: testMetadataChangedEntityID, - MetadataURL: "", - LoginVersion: domain.LoginVersion2, - LoginBaseURI: "https://test.com/login", + MetadataURL: gu.Ptr(""), + LoginVersion: gu.Ptr(domain.LoginVersion2), + LoginBaseURI: gu.Ptr("https://test.com/login"), }, resourceOwner: "org1", }, @@ -797,10 +851,10 @@ func TestCommandSide_ChangeSAMLApplication(t *testing.T) { AppName: "app", EntityID: "https://test2.com/saml/metadata", Metadata: testMetadataChangedEntityID, - MetadataURL: "", + MetadataURL: gu.Ptr(""), State: domain.AppStateActive, - LoginVersion: domain.LoginVersion2, - LoginBaseURI: "https://test.com/login", + LoginVersion: gu.Ptr(domain.LoginVersion2), + LoginBaseURI: gu.Ptr("https://test.com/login"), }, }, }, @@ -808,11 +862,14 @@ func TestCommandSide_ChangeSAMLApplication(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() + r := &Commands{ - eventstore: tt.fields.eventstore(t), - httpClient: tt.fields.httpClient, + eventstore: tt.fields.eventstore(t), + httpClient: tt.fields.httpClient, + checkPermission: newMockPermissionCheckAllowed(), } - got, err := r.ChangeSAMLApplication(tt.args.ctx, tt.args.samlApp, tt.args.resourceOwner) + got, err := r.UpdateSAMLApplication(tt.args.ctx, tt.args.samlApp, tt.args.resourceOwner) if tt.res.err == nil { assert.NoError(t, err) } diff --git a/internal/command/project_application_test.go b/internal/command/project_application_test.go index 050a41d29f..a67e6886ed 100644 --- a/internal/command/project_application_test.go +++ b/internal/command/project_application_test.go @@ -8,13 +8,16 @@ import ( "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/eventstore/repository/mock" "github.com/zitadel/zitadel/internal/repository/project" "github.com/zitadel/zitadel/internal/zerrors" ) func TestCommandSide_ChangeApplication(t *testing.T) { + t.Parallel() + type fields struct { - eventstore *eventstore.Eventstore + eventstore func(*testing.T) *eventstore.Eventstore } type args struct { ctx context.Context @@ -35,9 +38,7 @@ func TestCommandSide_ChangeApplication(t *testing.T) { { name: "invalid app missing projectid, invalid argument error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(func(mockRepository *mock.MockRepository) {}), }, args: args{ ctx: context.Background(), @@ -55,9 +56,7 @@ func TestCommandSide_ChangeApplication(t *testing.T) { { name: "invalid app missing appid, invalid argument error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(func(mockRepository *mock.MockRepository) {}), }, args: args{ ctx: context.Background(), @@ -74,9 +73,7 @@ func TestCommandSide_ChangeApplication(t *testing.T) { { name: "invalid app missing name, invalid argument error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(func(mockRepository *mock.MockRepository) {}), }, args: args{ ctx: context.Background(), @@ -94,10 +91,7 @@ func TestCommandSide_ChangeApplication(t *testing.T) { { name: "app not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, - expectFilter(), - ), + eventstore: expectEventstore(expectFilter()), }, args: args{ ctx: context.Background(), @@ -115,8 +109,7 @@ func TestCommandSide_ChangeApplication(t *testing.T) { { name: "app name not changed, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -142,8 +135,14 @@ func TestCommandSide_ChangeApplication(t *testing.T) { { name: "app changed, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "app1", + "app", + )), + ), expectFilter( eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -179,10 +178,13 @@ func TestCommandSide_ChangeApplication(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() + r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), + checkPermission: newMockPermissionCheckAllowed(), } - got, err := r.ChangeApplication(tt.args.ctx, tt.args.projectID, tt.args.app, tt.args.resourceOwner) + got, err := r.UpdateApplicationName(tt.args.ctx, tt.args.projectID, tt.args.app, tt.args.resourceOwner) if tt.res.err == nil { assert.NoError(t, err) } @@ -197,8 +199,10 @@ func TestCommandSide_ChangeApplication(t *testing.T) { } func TestCommandSide_DeactivateApplication(t *testing.T) { + t.Parallel() + type fields struct { - eventstore *eventstore.Eventstore + eventstore func(*testing.T) *eventstore.Eventstore } type args struct { ctx context.Context @@ -219,9 +223,7 @@ func TestCommandSide_DeactivateApplication(t *testing.T) { { name: "missing projectid, invalid argument error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(func(mockRepository *mock.MockRepository) {}), }, args: args{ ctx: context.Background(), @@ -236,9 +238,7 @@ func TestCommandSide_DeactivateApplication(t *testing.T) { { name: "missing appid, invalid argument error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(func(mockRepository *mock.MockRepository) {}), }, args: args{ ctx: context.Background(), @@ -253,8 +253,7 @@ func TestCommandSide_DeactivateApplication(t *testing.T) { { name: "app not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -271,8 +270,7 @@ func TestCommandSide_DeactivateApplication(t *testing.T) { { name: "app already inactive, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -299,8 +297,14 @@ func TestCommandSide_DeactivateApplication(t *testing.T) { { name: "app deactivate, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "app1", + "app", + )), + ), expectFilter( eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -331,8 +335,11 @@ func TestCommandSide_DeactivateApplication(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() + r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), + checkPermission: newMockPermissionCheckAllowed(), } got, err := r.DeactivateApplication(tt.args.ctx, tt.args.projectID, tt.args.appID, tt.args.resourceOwner) if tt.res.err == nil { @@ -349,8 +356,10 @@ func TestCommandSide_DeactivateApplication(t *testing.T) { } func TestCommandSide_ReactivateApplication(t *testing.T) { + t.Parallel() + type fields struct { - eventstore *eventstore.Eventstore + eventstore func(*testing.T) *eventstore.Eventstore } type args struct { ctx context.Context @@ -371,9 +380,7 @@ func TestCommandSide_ReactivateApplication(t *testing.T) { { name: "missing projectid, invalid argument error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(func(mockRepository *mock.MockRepository) {}), }, args: args{ ctx: context.Background(), @@ -388,9 +395,7 @@ func TestCommandSide_ReactivateApplication(t *testing.T) { { name: "missing appid, invalid argument error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(func(mockRepository *mock.MockRepository) {}), }, args: args{ ctx: context.Background(), @@ -405,10 +410,7 @@ func TestCommandSide_ReactivateApplication(t *testing.T) { { name: "app not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, - expectFilter(), - ), + eventstore: expectEventstore(expectFilter()), }, args: args{ ctx: context.Background(), @@ -423,8 +425,7 @@ func TestCommandSide_ReactivateApplication(t *testing.T) { { name: "app already active, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -447,8 +448,7 @@ func TestCommandSide_ReactivateApplication(t *testing.T) { { name: "app reactivate, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -483,8 +483,11 @@ func TestCommandSide_ReactivateApplication(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() + r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), + checkPermission: newMockPermissionCheckAllowed(), } got, err := r.ReactivateApplication(tt.args.ctx, tt.args.projectID, tt.args.appID, tt.args.resourceOwner) if tt.res.err == nil { @@ -501,8 +504,10 @@ func TestCommandSide_ReactivateApplication(t *testing.T) { } func TestCommandSide_RemoveApplication(t *testing.T) { + t.Parallel() + type fields struct { - eventstore *eventstore.Eventstore + eventstore func(*testing.T) *eventstore.Eventstore } type args struct { ctx context.Context @@ -523,9 +528,7 @@ func TestCommandSide_RemoveApplication(t *testing.T) { { name: "missing projectid, invalid argument error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(func(mockRepository *mock.MockRepository) {}), }, args: args{ ctx: context.Background(), @@ -540,9 +543,7 @@ func TestCommandSide_RemoveApplication(t *testing.T) { { name: "missing appid, invalid argument error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(func(mockRepository *mock.MockRepository) {}), }, args: args{ ctx: context.Background(), @@ -557,10 +558,7 @@ func TestCommandSide_RemoveApplication(t *testing.T) { { name: "app not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, - expectFilter(), - ), + eventstore: expectEventstore(expectFilter()), }, args: args{ ctx: context.Background(), @@ -575,8 +573,7 @@ func TestCommandSide_RemoveApplication(t *testing.T) { { name: "app remove, entityID, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -584,6 +581,7 @@ func TestCommandSide_RemoveApplication(t *testing.T) { "app", )), ), + expectFilter(), expectFilter( eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -625,8 +623,7 @@ func TestCommandSide_RemoveApplication(t *testing.T) { { name: "app remove, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -636,6 +633,7 @@ func TestCommandSide_RemoveApplication(t *testing.T) { ), // app is not saml, or no saml config available expectFilter(), + expectFilter(), expectPush( project.NewApplicationRemovedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -661,8 +659,11 @@ func TestCommandSide_RemoveApplication(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() + r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), + checkPermission: newMockPermissionCheckAllowed(), } got, err := r.RemoveApplication(tt.args.ctx, tt.args.projectID, tt.args.appID, tt.args.resourceOwner) if tt.res.err == nil { diff --git a/internal/command/project_converter.go b/internal/command/project_converter.go index 01b5a4e63d..e88a1cb75a 100644 --- a/internal/command/project_converter.go +++ b/internal/command/project_converter.go @@ -1,6 +1,8 @@ package command import ( + "github.com/muhlemmer/gu" + "github.com/zitadel/zitadel/internal/domain" ) @@ -35,21 +37,21 @@ func oidcWriteModelToOIDCConfig(writeModel *OIDCApplicationWriteModel) *domain.O RedirectUris: writeModel.RedirectUris, ResponseTypes: writeModel.ResponseTypes, GrantTypes: writeModel.GrantTypes, - ApplicationType: writeModel.ApplicationType, - AuthMethodType: writeModel.AuthMethodType, + ApplicationType: gu.Ptr(writeModel.ApplicationType), + AuthMethodType: gu.Ptr(writeModel.AuthMethodType), PostLogoutRedirectUris: writeModel.PostLogoutRedirectUris, - OIDCVersion: writeModel.OIDCVersion, - DevMode: writeModel.DevMode, - AccessTokenType: writeModel.AccessTokenType, - AccessTokenRoleAssertion: writeModel.AccessTokenRoleAssertion, - IDTokenRoleAssertion: writeModel.IDTokenRoleAssertion, - IDTokenUserinfoAssertion: writeModel.IDTokenUserinfoAssertion, - ClockSkew: writeModel.ClockSkew, + OIDCVersion: gu.Ptr(writeModel.OIDCVersion), + DevMode: gu.Ptr(writeModel.DevMode), + AccessTokenType: gu.Ptr(writeModel.AccessTokenType), + AccessTokenRoleAssertion: gu.Ptr(writeModel.AccessTokenRoleAssertion), + IDTokenRoleAssertion: gu.Ptr(writeModel.IDTokenRoleAssertion), + IDTokenUserinfoAssertion: gu.Ptr(writeModel.IDTokenUserinfoAssertion), + ClockSkew: gu.Ptr(writeModel.ClockSkew), AdditionalOrigins: writeModel.AdditionalOrigins, - SkipNativeAppSuccessPage: writeModel.SkipNativeAppSuccessPage, - BackChannelLogoutURI: writeModel.BackChannelLogoutURI, - LoginVersion: writeModel.LoginVersion, - LoginBaseURI: writeModel.LoginBaseURI, + SkipNativeAppSuccessPage: gu.Ptr(writeModel.SkipNativeAppSuccessPage), + BackChannelLogoutURI: gu.Ptr(writeModel.BackChannelLogoutURI), + LoginVersion: gu.Ptr(writeModel.LoginVersion), + LoginBaseURI: gu.Ptr(writeModel.LoginBaseURI), } } @@ -60,10 +62,10 @@ func samlWriteModelToSAMLConfig(writeModel *SAMLApplicationWriteModel) *domain.S AppName: writeModel.AppName, State: writeModel.State, Metadata: writeModel.Metadata, - MetadataURL: writeModel.MetadataURL, + MetadataURL: gu.Ptr(writeModel.MetadataURL), EntityID: writeModel.EntityID, - LoginVersion: writeModel.LoginVersion, - LoginBaseURI: writeModel.LoginBaseURI, + LoginVersion: gu.Ptr(writeModel.LoginVersion), + LoginBaseURI: gu.Ptr(writeModel.LoginBaseURI), } } @@ -78,15 +80,6 @@ func apiWriteModelToAPIConfig(writeModel *APIApplicationWriteModel) *domain.APIA } } -func roleWriteModelToRole(writeModel *ProjectRoleWriteModel) *domain.ProjectRole { - return &domain.ProjectRole{ - ObjectRoot: writeModelToObjectRoot(writeModel.WriteModel), - Key: writeModel.Key, - DisplayName: writeModel.DisplayName, - Group: writeModel.Group, - } -} - func memberWriteModelToProjectGrantMember(writeModel *ProjectGrantMemberWriteModel) *domain.ProjectGrantMember { return &domain.ProjectGrantMember{ ObjectRoot: writeModelToObjectRoot(writeModel.WriteModel), diff --git a/internal/command/project_grant.go b/internal/command/project_grant.go index 82d5dcab38..2a0e83a6e8 100644 --- a/internal/command/project_grant.go +++ b/internal/command/project_grant.go @@ -9,6 +9,7 @@ import ( "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" + es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models" "github.com/zitadel/zitadel/internal/feature" "github.com/zitadel/zitadel/internal/repository/org" "github.com/zitadel/zitadel/internal/repository/project" @@ -16,69 +17,112 @@ import ( "github.com/zitadel/zitadel/internal/zerrors" ) -func (c *Commands) AddProjectGrantWithID(ctx context.Context, grant *domain.ProjectGrant, grantID string, resourceOwner string) (_ *domain.ProjectGrant, err error) { +type AddProjectGrant struct { + es_models.ObjectRoot + + GrantID string + GrantedOrgID string + RoleKeys []string +} + +func (p *AddProjectGrant) IsValid() error { + if p.AggregateID == "" { + return zerrors.ThrowInvalidArgument(nil, "COMMAND-FYRnWEzBzV", "Errors.Project.Grant.Invalid") + } + if p.GrantedOrgID == "" { + return zerrors.ThrowInvalidArgument(nil, "COMMAND-PPhHpWGRAE", "Errors.Project.Grant.Invalid") + } + return nil +} + +func (c *Commands) AddProjectGrant(ctx context.Context, grant *AddProjectGrant) (_ *domain.ObjectDetails, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - return c.addProjectGrantWithID(ctx, grant, grantID, resourceOwner) -} - -func (c *Commands) AddProjectGrant(ctx context.Context, grant *domain.ProjectGrant, resourceOwner string) (_ *domain.ProjectGrant, err error) { - if !grant.IsValid() { - return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-3b8fs", "Errors.Project.Grant.Invalid") - } - err = c.checkProjectGrantPreCondition(ctx, grant, resourceOwner) - if err != nil { + if err := grant.IsValid(); err != nil { return nil, err } - grantID, err := c.idGenerator.Next() - if err != nil { - return nil, err - } - - return c.addProjectGrantWithID(ctx, grant, grantID, resourceOwner) -} - -func (c *Commands) addProjectGrantWithID(ctx context.Context, grant *domain.ProjectGrant, grantID string, resourceOwner string) (_ *domain.ProjectGrant, err error) { - grant.GrantID = grantID - - addedGrant := NewProjectGrantWriteModel(grant.GrantID, grant.AggregateID, resourceOwner) - projectAgg := ProjectAggregateFromWriteModel(&addedGrant.WriteModel) - pushedEvents, err := c.eventstore.Push( - ctx, - project.NewGrantAddedEvent(ctx, projectAgg, grant.GrantID, grant.GrantedOrgID, grant.RoleKeys)) - if err != nil { - return nil, err - } - err = AppendAndReduce(addedGrant, pushedEvents...) - if err != nil { - return nil, err - } - return projectGrantWriteModelToProjectGrant(addedGrant), nil -} - -func (c *Commands) ChangeProjectGrant(ctx context.Context, grant *domain.ProjectGrant, resourceOwner string, cascadeUserGrantIDs ...string) (_ *domain.ProjectGrant, err error) { if grant.GrantID == "" { + grant.GrantID, err = c.idGenerator.Next() + if err != nil { + return nil, err + } + } + + projectResourceOwner, err := c.checkProjectGrantPreCondition(ctx, grant.AggregateID, grant.GrantedOrgID, grant.ResourceOwner, grant.RoleKeys) + if err != nil { + return nil, err + } + // if there is no resourceowner provided then use the resourceowner of the project + if grant.ResourceOwner == "" { + grant.ResourceOwner = projectResourceOwner + } + if err := c.checkPermissionUpdateProjectGrant(ctx, grant.ResourceOwner, grant.AggregateID, grant.GrantID); err != nil { + return nil, err + } + + wm := NewProjectGrantWriteModel(grant.GrantID, grant.GrantedOrgID, grant.AggregateID, grant.ResourceOwner) + // error if provided resourceowner is not equal to the resourceowner of the project or the project grant is for the same organization + if projectResourceOwner != wm.ResourceOwner || wm.ResourceOwner == grant.GrantedOrgID { + return nil, zerrors.ThrowPreconditionFailed(nil, "PROJECT-ckUpbvboAH", "Errors.Project.Grant.Invalid") + } + if err := c.pushAppendAndReduce(ctx, + wm, + project.NewGrantAddedEvent(ctx, + ProjectAggregateFromWriteModelWithCTX(ctx, &wm.WriteModel), + grant.GrantID, + grant.GrantedOrgID, + grant.RoleKeys), + ); err != nil { + return nil, err + } + return writeModelToObjectDetails(&wm.WriteModel), nil +} + +type ChangeProjectGrant struct { + es_models.ObjectRoot + + GrantID string + GrantedOrgID string + RoleKeys []string +} + +func (c *Commands) ChangeProjectGrant(ctx context.Context, grant *ChangeProjectGrant, cascadeUserGrantIDs ...string) (_ *domain.ObjectDetails, err error) { + if grant.GrantID == "" && grant.GrantedOrgID == "" { return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-1j83s", "Errors.IDMissing") } - existingGrant, err := c.projectGrantWriteModelByID(ctx, grant.GrantID, grant.AggregateID, resourceOwner) + existingGrant, err := c.projectGrantWriteModelByID(ctx, grant.GrantID, grant.GrantedOrgID, grant.AggregateID, grant.ResourceOwner) if err != nil { return nil, err } - grant.GrantedOrgID = existingGrant.GrantedOrgID - err = c.checkProjectGrantPreCondition(ctx, grant, resourceOwner) - if err != nil { - return nil, err + if !existingGrant.State.Exists() { + return nil, zerrors.ThrowNotFound(nil, "PROJECT-D8JxR", "Errors.Project.Grant.NotFound") } - projectAgg := ProjectAggregateFromWriteModel(&existingGrant.WriteModel) + if err := c.checkPermissionUpdateProjectGrant(ctx, existingGrant.ResourceOwner, existingGrant.AggregateID, existingGrant.GrantID); err != nil { + return nil, err + } + projectResourceOwner, err := c.checkProjectGrantPreCondition(ctx, existingGrant.AggregateID, existingGrant.GrantedOrgID, existingGrant.ResourceOwner, grant.RoleKeys) + if err != nil { + return nil, err + } + // error if provided resourceowner is not equal to the resourceowner of the project + if existingGrant.ResourceOwner != projectResourceOwner { + return nil, zerrors.ThrowPreconditionFailed(nil, "PROJECT-q1BhA68RBC", "Errors.Project.Grant.Invalid") + } + + // return if there are no changes to the project grant roles if reflect.DeepEqual(existingGrant.RoleKeys, grant.RoleKeys) { - return nil, zerrors.ThrowPreconditionFailed(nil, "PROJECT-0o0pL", "Errors.NoChangesFoundc") + return writeModelToObjectDetails(&existingGrant.WriteModel), nil } events := []eventstore.Command{ - project.NewGrantChangedEvent(ctx, projectAgg, grant.GrantID, grant.RoleKeys), + project.NewGrantChangedEvent(ctx, + ProjectAggregateFromWriteModelWithCTX(ctx, &existingGrant.WriteModel), + existingGrant.GrantID, + grant.RoleKeys, + ), } removedRoles := domain.GetRemovedRoles(existingGrant.RoleKeys, grant.RoleKeys) @@ -91,7 +135,7 @@ func (c *Commands) ChangeProjectGrant(ctx context.Context, grant *domain.Project if err != nil { return nil, err } - return projectGrantWriteModelToProjectGrant(existingGrant), nil + return writeModelToObjectDetails(&existingGrant.WriteModel), nil } for _, userGrantID := range cascadeUserGrantIDs { @@ -109,16 +153,16 @@ func (c *Commands) ChangeProjectGrant(ctx context.Context, grant *domain.Project if err != nil { return nil, err } - return projectGrantWriteModelToProjectGrant(existingGrant), nil + return writeModelToObjectDetails(&existingGrant.WriteModel), nil } func (c *Commands) removeRoleFromProjectGrant(ctx context.Context, projectAgg *eventstore.Aggregate, projectID, projectGrantID, roleKey string, cascade bool) (_ eventstore.Command, _ *ProjectGrantWriteModel, err error) { - existingProjectGrant, err := c.projectGrantWriteModelByID(ctx, projectGrantID, projectID, "") + existingProjectGrant, err := c.projectGrantWriteModelByID(ctx, projectGrantID, "", projectID, "") if err != nil { return nil, nil, err } - if existingProjectGrant.State == domain.ProjectGrantStateUnspecified || existingProjectGrant.State == domain.ProjectGrantStateRemoved { - return nil, nil, zerrors.ThrowNotFound(nil, "COMMAND-3M9sd", "Errors.Project.Grant.NotFound") + if !existingProjectGrant.State.Exists() { + return nil, nil, zerrors.ThrowNotFound(nil, "PROJECT-D8JxR", "Errors.Project.Grant.NotFound") } keyExists := false for i, key := range existingProjectGrant.RoleKeys { @@ -133,7 +177,7 @@ func (c *Commands) removeRoleFromProjectGrant(ctx context.Context, projectAgg *e if !keyExists { return nil, nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-5m8g9", "Errors.Project.Grant.RoleKeyNotFound") } - changedProjectGrant := NewProjectGrantWriteModel(projectGrantID, projectID, existingProjectGrant.ResourceOwner) + changedProjectGrant := NewProjectGrantWriteModel(projectGrantID, projectID, "", existingProjectGrant.ResourceOwner) if cascade { return project.NewGrantCascadeChangedEvent(ctx, projectAgg, projectGrantID, existingProjectGrant.RoleKeys), changedProjectGrant, nil @@ -142,26 +186,44 @@ func (c *Commands) removeRoleFromProjectGrant(ctx context.Context, projectAgg *e return project.NewGrantChangedEvent(ctx, projectAgg, projectGrantID, existingProjectGrant.RoleKeys), changedProjectGrant, nil } -func (c *Commands) DeactivateProjectGrant(ctx context.Context, projectID, grantID, resourceOwner string) (details *domain.ObjectDetails, err error) { - if grantID == "" || projectID == "" { +func (c *Commands) DeactivateProjectGrant(ctx context.Context, projectID, grantID, grantedOrgID, resourceOwner string) (details *domain.ObjectDetails, err error) { + if (grantID == "" && grantedOrgID == "") || projectID == "" { return details, zerrors.ThrowInvalidArgument(nil, "PROJECT-p0s4V", "Errors.IDMissing") } - err = c.checkProjectExists(ctx, projectID, resourceOwner) + projectResourceOwner, err := c.checkProjectExists(ctx, projectID, resourceOwner) if err != nil { return nil, err } - existingGrant, err := c.projectGrantWriteModelByID(ctx, grantID, projectID, resourceOwner) + existingGrant, err := c.projectGrantWriteModelByID(ctx, grantID, grantedOrgID, projectID, resourceOwner) if err != nil { return details, err } + if !existingGrant.State.Exists() { + return nil, zerrors.ThrowNotFound(nil, "PROJECT-D8JxR", "Errors.Project.Grant.NotFound") + } + // error if provided resourceowner is not equal to the resourceowner of the project + if projectResourceOwner != existingGrant.ResourceOwner { + return nil, zerrors.ThrowPreconditionFailed(nil, "PROJECT-0l10S9OmZV", "Errors.Project.Grant.Invalid") + } + // return if project grant is already inactive + if existingGrant.State == domain.ProjectGrantStateInactive { + return writeModelToObjectDetails(&existingGrant.WriteModel), nil + } + // error if project grant is neither active nor inactive if existingGrant.State != domain.ProjectGrantStateActive { return details, zerrors.ThrowPreconditionFailed(nil, "PROJECT-47fu8", "Errors.Project.Grant.NotActive") } - projectAgg := ProjectAggregateFromWriteModel(&existingGrant.WriteModel) - - pushedEvents, err := c.eventstore.Push(ctx, project.NewGrantDeactivateEvent(ctx, projectAgg, grantID)) + if err := c.checkPermissionUpdateProjectGrant(ctx, existingGrant.ResourceOwner, existingGrant.AggregateID, existingGrant.GrantID); err != nil { + return nil, err + } + pushedEvents, err := c.eventstore.Push(ctx, + project.NewGrantDeactivateEvent(ctx, + ProjectAggregateFromWriteModelWithCTX(ctx, &existingGrant.WriteModel), + existingGrant.GrantID, + ), + ) if err != nil { return nil, err } @@ -172,25 +234,55 @@ func (c *Commands) DeactivateProjectGrant(ctx context.Context, projectID, grantI return writeModelToObjectDetails(&existingGrant.WriteModel), nil } -func (c *Commands) ReactivateProjectGrant(ctx context.Context, projectID, grantID, resourceOwner string) (details *domain.ObjectDetails, err error) { - if grantID == "" || projectID == "" { +func (c *Commands) checkProjectGrantExists(ctx context.Context, grantID, grantedOrgID, projectID, resourceOwner string) (string, string, error) { + existingGrant, err := c.projectGrantWriteModelByID(ctx, grantID, grantedOrgID, projectID, resourceOwner) + if err != nil { + return "", "", err + } + if !existingGrant.State.Exists() { + return "", "", zerrors.ThrowNotFound(nil, "PROJECT-D8JxR", "Errors.Project.Grant.NotFound") + } + return existingGrant.GrantedOrgID, existingGrant.ResourceOwner, nil +} + +func (c *Commands) ReactivateProjectGrant(ctx context.Context, projectID, grantID, grantedOrgID, resourceOwner string) (details *domain.ObjectDetails, err error) { + if (grantID == "" && grantedOrgID == "") || projectID == "" { return details, zerrors.ThrowInvalidArgument(nil, "PROJECT-p0s4V", "Errors.IDMissing") } - err = c.checkProjectExists(ctx, projectID, resourceOwner) + projectResourceOwner, err := c.checkProjectExists(ctx, projectID, resourceOwner) if err != nil { return nil, err } - existingGrant, err := c.projectGrantWriteModelByID(ctx, grantID, projectID, resourceOwner) + existingGrant, err := c.projectGrantWriteModelByID(ctx, grantID, grantedOrgID, projectID, resourceOwner) if err != nil { return details, err } + if !existingGrant.State.Exists() { + return nil, zerrors.ThrowNotFound(nil, "PROJECT-D8JxR", "Errors.Project.Grant.NotFound") + } + // error if provided resourceowner is not equal to the resourceowner of the project + if projectResourceOwner != existingGrant.ResourceOwner { + return nil, zerrors.ThrowPreconditionFailed(nil, "PROJECT-byscAarAST", "Errors.Project.Grant.Invalid") + } + // return if project grant is already active + if existingGrant.State == domain.ProjectGrantStateActive { + return writeModelToObjectDetails(&existingGrant.WriteModel), nil + } + // error if project grant is neither active nor inactive if existingGrant.State != domain.ProjectGrantStateInactive { return details, zerrors.ThrowPreconditionFailed(nil, "PROJECT-47fu8", "Errors.Project.Grant.NotInactive") } - projectAgg := ProjectAggregateFromWriteModel(&existingGrant.WriteModel) - pushedEvents, err := c.eventstore.Push(ctx, project.NewGrantReactivatedEvent(ctx, projectAgg, grantID)) + if err := c.checkPermissionUpdateProjectGrant(ctx, existingGrant.ResourceOwner, existingGrant.AggregateID, existingGrant.GrantID); err != nil { + return nil, err + } + pushedEvents, err := c.eventstore.Push(ctx, + project.NewGrantReactivatedEvent(ctx, + ProjectAggregateFromWriteModelWithCTX(ctx, &existingGrant.WriteModel), + existingGrant.GrantID, + ), + ) if err != nil { return nil, err } @@ -201,25 +293,37 @@ func (c *Commands) ReactivateProjectGrant(ctx context.Context, projectID, grantI return writeModelToObjectDetails(&existingGrant.WriteModel), nil } +// Deprecated: use commands.DeleteProjectGrant func (c *Commands) RemoveProjectGrant(ctx context.Context, projectID, grantID, resourceOwner string, cascadeUserGrantIDs ...string) (details *domain.ObjectDetails, err error) { if grantID == "" || projectID == "" { return details, zerrors.ThrowInvalidArgument(nil, "PROJECT-1m9fJ", "Errors.IDMissing") } - existingGrant, err := c.projectGrantWriteModelByID(ctx, grantID, projectID, resourceOwner) + existingGrant, err := c.projectGrantWriteModelByID(ctx, grantID, "", projectID, resourceOwner) if err != nil { return details, err } + if !existingGrant.State.Exists() { + return nil, zerrors.ThrowNotFound(nil, "PROJECT-D8JxR", "Errors.Project.Grant.NotFound") + } + if err := c.checkPermissionDeleteProjectGrant(ctx, existingGrant.ResourceOwner, existingGrant.AggregateID, existingGrant.GrantID); err != nil { + return nil, err + } events := make([]eventstore.Command, 0) - projectAgg := ProjectAggregateFromWriteModel(&existingGrant.WriteModel) - events = append(events, project.NewGrantRemovedEvent(ctx, projectAgg, grantID, existingGrant.GrantedOrgID)) + events = append(events, project.NewGrantRemovedEvent(ctx, + ProjectAggregateFromWriteModelWithCTX(ctx, &existingGrant.WriteModel), + existingGrant.GrantID, + existingGrant.GrantedOrgID, + )) for _, userGrantID := range cascadeUserGrantIDs { - event, _, err := c.removeUserGrant(ctx, userGrantID, "", true) + event, _, err := c.removeUserGrant(ctx, userGrantID, "", true, true, nil) if err != nil { - logging.LogWithFields("COMMAND-3m8sG", "usergrantid", grantID).WithError(err).Warn("could not cascade remove user grant") + logging.WithFields("id", "COMMAND-3m8sG", "usergrantid", grantID).WithError(err).Warn("could not cascade remove user grant") continue } - events = append(events, event) + if event != nil { + events = append(events, event) + } } pushedEvents, err := c.eventstore.Push(ctx, events...) if err != nil { @@ -232,72 +336,117 @@ func (c *Commands) RemoveProjectGrant(ctx context.Context, projectID, grantID, r return writeModelToObjectDetails(&existingGrant.WriteModel), nil } -func (c *Commands) projectGrantWriteModelByID(ctx context.Context, grantID, projectID, resourceOwner string) (member *ProjectGrantWriteModel, err error) { +func (c *Commands) DeleteProjectGrant(ctx context.Context, projectID, grantID, grantedOrgID, resourceOwner string, cascadeUserGrantIDs ...string) (details *domain.ObjectDetails, err error) { + if (grantID == "" && grantedOrgID == "") || projectID == "" { + return details, zerrors.ThrowInvalidArgument(nil, "PROJECT-1m9fJ", "Errors.IDMissing") + } + existingGrant, err := c.projectGrantWriteModelByID(ctx, grantID, grantedOrgID, projectID, resourceOwner) + if err != nil { + return details, err + } + // return if project grant does not exist, or was removed already + if !existingGrant.State.Exists() { + return writeModelToObjectDetails(&existingGrant.WriteModel), nil + } + if err := c.checkPermissionDeleteProjectGrant(ctx, existingGrant.ResourceOwner, existingGrant.AggregateID, existingGrant.GrantID); err != nil { + return nil, err + } + events := make([]eventstore.Command, 0) + events = append(events, project.NewGrantRemovedEvent(ctx, + ProjectAggregateFromWriteModelWithCTX(ctx, &existingGrant.WriteModel), + existingGrant.GrantID, + existingGrant.GrantedOrgID, + ), + ) + + for _, userGrantID := range cascadeUserGrantIDs { + event, _, err := c.removeUserGrant(ctx, userGrantID, "", true, true, nil) + if err != nil { + logging.WithFields("id", "COMMAND-3m8sG", "usergrantid", grantID).WithError(err).Warn("could not cascade remove user grant") + continue + } + if event != nil { + events = append(events, event) + } + } + pushedEvents, err := c.eventstore.Push(ctx, events...) + if err != nil { + return nil, err + } + err = AppendAndReduce(existingGrant, pushedEvents...) + if err != nil { + return nil, err + } + return writeModelToObjectDetails(&existingGrant.WriteModel), nil +} + +func (c *Commands) projectGrantWriteModelByID(ctx context.Context, grantID, grantedOrgID, projectID, resourceOwner string) (member *ProjectGrantWriteModel, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - writeModel := NewProjectGrantWriteModel(grantID, projectID, resourceOwner) + writeModel := NewProjectGrantWriteModel(grantID, grantedOrgID, projectID, resourceOwner) err = c.eventstore.FilterToQueryReducer(ctx, writeModel) if err != nil { return nil, err } - - if writeModel.State == domain.ProjectGrantStateUnspecified || writeModel.State == domain.ProjectGrantStateRemoved { - return nil, zerrors.ThrowNotFound(nil, "PROJECT-D8JxR", "Errors.Project.Grant.NotFound") - } - return writeModel, nil } -func (c *Commands) checkProjectGrantPreCondition(ctx context.Context, projectGrant *domain.ProjectGrant, resourceOwner string) error { +func (c *Commands) checkProjectGrantPreCondition(ctx context.Context, projectID, grantedOrgID, resourceOwner string, roles []string) (string, error) { if !authz.GetFeatures(ctx).ShouldUseImprovedPerformance(feature.ImprovedPerformanceTypeProjectGrant) { - return c.checkProjectGrantPreConditionOld(ctx, projectGrant, resourceOwner) + return c.checkProjectGrantPreConditionOld(ctx, projectID, grantedOrgID, resourceOwner, roles) } - existingRoleKeys, err := c.searchProjectGrantState(ctx, projectGrant.AggregateID, projectGrant.GrantedOrgID, resourceOwner) + projectResourceOwner, existingRoleKeys, err := c.searchProjectGrantState(ctx, projectID, grantedOrgID, resourceOwner) if err != nil { - return err + return "", err } - if projectGrant.HasInvalidRoles(existingRoleKeys) { - return zerrors.ThrowPreconditionFailed(err, "COMMAND-6m9gd", "Errors.Project.Role.NotFound") + if domain.HasInvalidRoles(existingRoleKeys, roles) { + return "", zerrors.ThrowPreconditionFailed(err, "COMMAND-6m9gd", "Errors.Project.Role.NotFound") } - return nil + return projectResourceOwner, nil } -func (c *Commands) searchProjectGrantState(ctx context.Context, projectID, grantedOrgID, resourceOwner string) (existingRoleKeys []string, err error) { +func (c *Commands) searchProjectGrantState(ctx context.Context, projectID, grantedOrgID, resourceOwner string) (_ string, existingRoleKeys []string, err error) { + projectStateQuery := map[eventstore.FieldType]any{ + eventstore.FieldTypeAggregateType: project.AggregateType, + eventstore.FieldTypeAggregateID: projectID, + eventstore.FieldTypeFieldName: project.ProjectStateSearchField, + eventstore.FieldTypeObjectType: project.ProjectSearchType, + } + grantedOrgQuery := map[eventstore.FieldType]any{ + eventstore.FieldTypeAggregateType: org.AggregateType, + eventstore.FieldTypeAggregateID: grantedOrgID, + eventstore.FieldTypeFieldName: org.OrgStateSearchField, + eventstore.FieldTypeObjectType: org.OrgSearchType, + } + roleQuery := map[eventstore.FieldType]any{ + eventstore.FieldTypeAggregateType: project.AggregateType, + eventstore.FieldTypeAggregateID: projectID, + eventstore.FieldTypeFieldName: project.ProjectRoleKeySearchField, + eventstore.FieldTypeObjectType: project.ProjectRoleSearchType, + } + + // as resourceowner is not always provided, it has to be separately + if resourceOwner != "" { + projectStateQuery[eventstore.FieldTypeResourceOwner] = resourceOwner + roleQuery[eventstore.FieldTypeResourceOwner] = resourceOwner + } + results, err := c.eventstore.Search( ctx, - // project state query - map[eventstore.FieldType]any{ - eventstore.FieldTypeResourceOwner: resourceOwner, - eventstore.FieldTypeAggregateType: project.AggregateType, - eventstore.FieldTypeAggregateID: projectID, - eventstore.FieldTypeFieldName: project.ProjectStateSearchField, - eventstore.FieldTypeObjectType: project.ProjectSearchType, - }, - // granted org query - map[eventstore.FieldType]any{ - eventstore.FieldTypeAggregateType: org.AggregateType, - eventstore.FieldTypeAggregateID: grantedOrgID, - eventstore.FieldTypeFieldName: org.OrgStateSearchField, - eventstore.FieldTypeObjectType: org.OrgSearchType, - }, - // role query - map[eventstore.FieldType]any{ - eventstore.FieldTypeResourceOwner: resourceOwner, - eventstore.FieldTypeAggregateType: project.AggregateType, - eventstore.FieldTypeAggregateID: projectID, - eventstore.FieldTypeFieldName: project.ProjectRoleKeySearchField, - eventstore.FieldTypeObjectType: project.ProjectRoleSearchType, - }, + projectStateQuery, + grantedOrgQuery, + roleQuery, ) if err != nil { - return nil, err + return "", nil, err } var ( - existsProject bool - existsGrantedOrg bool + existsProject bool + existingProjectResourceOwner string + existsGrantedOrg bool ) for _, result := range results { @@ -306,31 +455,32 @@ func (c *Commands) searchProjectGrantState(ctx context.Context, projectID, grant var role string err := result.Value.Unmarshal(&role) if err != nil { - return nil, err + return "", nil, err } existingRoleKeys = append(existingRoleKeys, role) case org.OrgSearchType: var state domain.OrgState err := result.Value.Unmarshal(&state) if err != nil { - return nil, err + return "", nil, err } existsGrantedOrg = state.Valid() && state != domain.OrgStateRemoved case project.ProjectSearchType: var state domain.ProjectState err := result.Value.Unmarshal(&state) if err != nil { - return nil, err + return "", nil, err } existsProject = state.Valid() && state != domain.ProjectStateRemoved + existingProjectResourceOwner = result.Aggregate.ResourceOwner } } if !existsProject { - return nil, zerrors.ThrowPreconditionFailed(err, "COMMAND-m9gsd", "Errors.Project.NotFound") + return "", nil, zerrors.ThrowPreconditionFailed(err, "COMMAND-m9gsd", "Errors.Project.NotFound") } if !existsGrantedOrg { - return nil, zerrors.ThrowPreconditionFailed(err, "COMMAND-3m9gg", "Errors.Org.NotFound") + return "", nil, zerrors.ThrowPreconditionFailed(err, "COMMAND-3m9gg", "Errors.Org.NotFound") } - return existingRoleKeys, nil + return existingProjectResourceOwner, existingRoleKeys, nil } diff --git a/internal/command/project_grant_member.go b/internal/command/project_grant_member.go index f7ea887475..611b897c61 100644 --- a/internal/command/project_grant_member.go +++ b/internal/command/project_grant_member.go @@ -2,8 +2,9 @@ package command import ( "context" - "reflect" + "slices" + "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/repository/project" @@ -11,25 +12,66 @@ import ( "github.com/zitadel/zitadel/internal/zerrors" ) -func (c *Commands) AddProjectGrantMember(ctx context.Context, member *domain.ProjectGrantMember) (_ *domain.ProjectGrantMember, err error) { +type AddProjectGrantMember struct { + ResourceOwner string + UserID string + GrantID string + ProjectID string + Roles []string +} + +func (i *AddProjectGrantMember) IsValid(zitadelRoles []authz.RoleMapping) error { + if i.ProjectID == "" || i.GrantID == "" || i.UserID == "" || len(i.Roles) == 0 { + return zerrors.ThrowInvalidArgument(nil, "PROJECT-8fi7G", "Errors.Project.Grant.Member.Invalid") + } + if len(domain.CheckForInvalidRoles(i.Roles, domain.ProjectGrantRolePrefix, zitadelRoles)) > 0 { + return zerrors.ThrowInvalidArgument(nil, "PROJECT-m9gKK", "Errors.Project.Grant.Member.Invalid") + } + return nil +} + +func (c *Commands) AddProjectGrantMember(ctx context.Context, member *AddProjectGrantMember) (_ *domain.ObjectDetails, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - if !member.IsValid() { - return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-8fi7G", "Errors.Project.Grant.Member.Invalid") + if err := member.IsValid(c.zitadelRoles); err != nil { + return nil, err } - if len(domain.CheckForInvalidRoles(member.Roles, domain.ProjectGrantRolePrefix, c.zitadelRoles)) > 0 { - return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-m9gKK", "Errors.Project.Grant.Member.Invalid") - } - err = c.checkUserExists(ctx, member.UserID, "") + _, err = c.checkUserExists(ctx, member.UserID, "") if err != nil { return nil, err } - addedMember := NewProjectGrantMemberWriteModel(member.AggregateID, member.UserID, member.GrantID) - projectAgg := ProjectAggregateFromWriteModel(&addedMember.WriteModel) + grantedOrgID, projectGrantResourceOwner, err := c.checkProjectGrantExists(ctx, member.GrantID, "", member.ProjectID, "") + if err != nil { + return nil, err + } + if member.ResourceOwner == "" { + member.ResourceOwner = projectGrantResourceOwner + } + addedMember, err := c.projectGrantMemberWriteModelByID(ctx, member.ProjectID, member.UserID, member.GrantID, member.ResourceOwner) + if err != nil { + return nil, err + } + // TODO: change e2e tests to use correct resourceowner, wrong resource owner is corrected through aggregate + // error if provided resourceowner is not equal to the resourceowner of the project grant + //if projectGrantResourceOwner != addedMember.ResourceOwner { + // return nil, zerrors.ThrowPreconditionFailed(nil, "PROJECT-0l10S9OmZV", "Errors.Project.Grant.Invalid") + //} + if addedMember.State.Exists() { + return nil, zerrors.ThrowNotFound(nil, "PROJECT-37fug", "Errors.AlreadyExists") + } + if err := c.checkPermissionUpdateProjectGrantMember(ctx, grantedOrgID, addedMember.GrantID); err != nil { + return nil, err + } + pushedEvents, err := c.eventstore.Push( ctx, - project.NewProjectGrantMemberAddedEvent(ctx, projectAgg, member.UserID, member.GrantID, member.Roles...)) + project.NewProjectGrantMemberAddedEvent(ctx, + ProjectAggregateFromWriteModelWithCTX(ctx, &addedMember.WriteModel), + member.UserID, + member.GrantID, + member.Roles..., + )) if err != nil { return nil, err } @@ -38,30 +80,58 @@ func (c *Commands) AddProjectGrantMember(ctx context.Context, member *domain.Pro return nil, err } - return memberWriteModelToProjectGrantMember(addedMember), nil + return writeModelToObjectDetails(&addedMember.WriteModel), nil +} + +type ChangeProjectGrantMember struct { + UserID string + GrantID string + ProjectID string + Roles []string +} + +func (i *ChangeProjectGrantMember) IsValid(zitadelRoles []authz.RoleMapping) error { + if i.ProjectID == "" || i.GrantID == "" || i.UserID == "" || len(i.Roles) == 0 { + return zerrors.ThrowInvalidArgument(nil, "PROJECT-109fs", "Errors.Project.Grant.Member.Invalid") + } + if len(domain.CheckForInvalidRoles(i.Roles, domain.ProjectGrantRolePrefix, zitadelRoles)) > 0 { + return zerrors.ThrowInvalidArgument(nil, "PROJECT-m0sDf", "Errors.Project.Grant.Member.Invalid") + } + return nil } // ChangeProjectGrantMember updates an existing member -func (c *Commands) ChangeProjectGrantMember(ctx context.Context, member *domain.ProjectGrantMember) (*domain.ProjectGrantMember, error) { - if !member.IsValid() { - return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-109fs", "Errors.Project.Member.Invalid") +func (c *Commands) ChangeProjectGrantMember(ctx context.Context, member *ChangeProjectGrantMember) (*domain.ObjectDetails, error) { + if err := member.IsValid(c.zitadelRoles); err != nil { + return nil, err } - if len(domain.CheckForInvalidRoles(member.Roles, domain.ProjectGrantRolePrefix, c.zitadelRoles)) > 0 { - return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-m0sDf", "Errors.Project.Member.Invalid") - } - - existingMember, err := c.projectGrantMemberWriteModelByID(ctx, member.AggregateID, member.UserID, member.GrantID) + existingGrant, err := c.projectGrantWriteModelByID(ctx, member.GrantID, "", member.ProjectID, "") if err != nil { return nil, err } - - if reflect.DeepEqual(existingMember.Roles, member.Roles) { - return nil, zerrors.ThrowPreconditionFailed(nil, "PROJECT-2n8vx", "Errors.Project.Member.RolesNotChanged") + existingMember, err := c.projectGrantMemberWriteModelByID(ctx, member.ProjectID, member.UserID, member.GrantID, existingGrant.ResourceOwner) + if err != nil { + return nil, err } - projectAgg := ProjectAggregateFromWriteModel(&existingMember.WriteModel) + if !existingMember.State.Exists() { + return nil, zerrors.ThrowNotFound(nil, "PROJECT-37fug", "Errors.NotFound") + } + + if err := c.checkPermissionUpdateProjectGrantMember(ctx, existingGrant.GrantedOrgID, existingMember.GrantID); err != nil { + return nil, err + } + if slices.Compare(existingMember.Roles, member.Roles) == 0 { + return writeModelToObjectDetails(&existingMember.WriteModel), nil + } + pushedEvents, err := c.eventstore.Push( ctx, - project.NewProjectGrantMemberChangedEvent(ctx, projectAgg, member.UserID, member.GrantID, member.Roles...)) + project.NewProjectGrantMemberChangedEvent(ctx, + ProjectAggregateFromWriteModelWithCTX(ctx, &existingMember.WriteModel), + member.UserID, + member.GrantID, + member.Roles..., + )) if err != nil { return nil, err } @@ -70,29 +140,43 @@ func (c *Commands) ChangeProjectGrantMember(ctx context.Context, member *domain. return nil, err } - return memberWriteModelToProjectGrantMember(existingMember), nil + return writeModelToObjectDetails(&existingMember.WriteModel), nil } func (c *Commands) RemoveProjectGrantMember(ctx context.Context, projectID, userID, grantID string) (*domain.ObjectDetails, error) { if projectID == "" || userID == "" || grantID == "" { return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-66mHd", "Errors.Project.Member.Invalid") } - m, err := c.projectGrantMemberWriteModelByID(ctx, projectID, userID, grantID) + existingGrant, err := c.projectGrantWriteModelByID(ctx, grantID, "", projectID, "") if err != nil { return nil, err } + existingMember, err := c.projectGrantMemberWriteModelByID(ctx, projectID, userID, grantID, existingGrant.ResourceOwner) + if err != nil { + return nil, err + } + if !existingMember.State.Exists() { + return writeModelToObjectDetails(&existingMember.WriteModel), nil + } + if err := c.checkPermissionDeleteProjectGrantMember(ctx, existingGrant.GrantedOrgID, existingMember.GrantID); err != nil { + return nil, err + } - projectAgg := ProjectAggregateFromWriteModel(&m.WriteModel) - removeEvent := c.removeProjectGrantMember(ctx, projectAgg, userID, grantID, false) + removeEvent := c.removeProjectGrantMember(ctx, + ProjectAggregateFromWriteModelWithCTX(ctx, &existingMember.WriteModel), + userID, + grantID, + false, + ) pushedEvents, err := c.eventstore.Push(ctx, removeEvent) if err != nil { return nil, err } - err = AppendAndReduce(m, pushedEvents...) + err = AppendAndReduce(existingMember, pushedEvents...) if err != nil { return nil, err } - return writeModelToObjectDetails(&m.WriteModel), nil + return writeModelToObjectDetails(&existingMember.WriteModel), nil } func (c *Commands) removeProjectGrantMember(ctx context.Context, projectAgg *eventstore.Aggregate, userID, grantID string, cascade bool) eventstore.Command { @@ -107,19 +191,15 @@ func (c *Commands) removeProjectGrantMember(ctx context.Context, projectAgg *eve } } -func (c *Commands) projectGrantMemberWriteModelByID(ctx context.Context, projectID, userID, grantID string) (member *ProjectGrantMemberWriteModel, err error) { +func (c *Commands) projectGrantMemberWriteModelByID(ctx context.Context, projectID, userID, grantID, resourceOwner string) (member *ProjectGrantMemberWriteModel, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - writeModel := NewProjectGrantMemberWriteModel(projectID, userID, grantID) + writeModel := NewProjectGrantMemberWriteModel(projectID, userID, grantID, resourceOwner) err = c.eventstore.FilterToQueryReducer(ctx, writeModel) if err != nil { return nil, err } - if writeModel.State == domain.MemberStateUnspecified || writeModel.State == domain.MemberStateRemoved { - return nil, zerrors.ThrowNotFound(nil, "PROJECT-37fug", "Errors.NotFound") - } - return writeModel, nil } diff --git a/internal/command/project_grant_member_model.go b/internal/command/project_grant_member_model.go index 6c089ba1c8..1d9ab02478 100644 --- a/internal/command/project_grant_member_model.go +++ b/internal/command/project_grant_member_model.go @@ -16,10 +16,11 @@ type ProjectGrantMemberWriteModel struct { State domain.MemberState } -func NewProjectGrantMemberWriteModel(projectID, userID, grantID string) *ProjectGrantMemberWriteModel { +func NewProjectGrantMemberWriteModel(projectID, userID, grantID, resourceOwner string) *ProjectGrantMemberWriteModel { return &ProjectGrantMemberWriteModel{ WriteModel: eventstore.WriteModel{ - AggregateID: projectID, + AggregateID: projectID, + ResourceOwner: resourceOwner, }, UserID: userID, GrantID: grantID, @@ -66,6 +67,7 @@ func (wm *ProjectGrantMemberWriteModel) Reduce() error { case *project.GrantMemberAddedEvent: wm.Roles = e.Roles wm.State = domain.MemberStateActive + wm.ResourceOwner = e.Aggregate().ResourceOwner case *project.GrantMemberChangedEvent: wm.Roles = e.Roles case *project.GrantMemberRemovedEvent: @@ -80,7 +82,8 @@ func (wm *ProjectGrantMemberWriteModel) Reduce() error { } func (wm *ProjectGrantMemberWriteModel) Query() *eventstore.SearchQueryBuilder { - return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). + query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). + ResourceOwner(wm.ResourceOwner). AddQuery(). AggregateTypes(project.AggregateType). AggregateIDs(wm.AggregateID). @@ -92,4 +95,5 @@ func (wm *ProjectGrantMemberWriteModel) Query() *eventstore.SearchQueryBuilder { project.GrantRemovedType, project.ProjectRemovedType). Builder() + return query } diff --git a/internal/command/project_grant_member_test.go b/internal/command/project_grant_member_test.go index 189d1b9911..3cd8ecdd82 100644 --- a/internal/command/project_grant_member_test.go +++ b/internal/command/project_grant_member_test.go @@ -10,7 +10,6 @@ import ( "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" - "github.com/zitadel/zitadel/internal/eventstore/v1/models" "github.com/zitadel/zitadel/internal/repository/project" "github.com/zitadel/zitadel/internal/repository/user" "github.com/zitadel/zitadel/internal/zerrors" @@ -18,15 +17,15 @@ import ( func TestCommandSide_AddProjectGrantMember(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore - zitadelRoles []authz.RoleMapping + eventstore func(t *testing.T) *eventstore.Eventstore + zitadelRoles []authz.RoleMapping + checkPermission domain.PermissionCheck } type args struct { - ctx context.Context - member *domain.ProjectGrantMember + member *AddProjectGrantMember } type res struct { - want *domain.ProjectGrantMember + want *domain.ObjectDetails err func(error) bool } tests := []struct { @@ -38,16 +37,12 @@ func TestCommandSide_AddProjectGrantMember(t *testing.T) { { name: "invalid member, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - ctx: context.Background(), - member: &domain.ProjectGrantMember{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", - }, + member: &AddProjectGrantMember{ + ProjectID: "project1", }, }, res: res{ @@ -57,19 +52,15 @@ func TestCommandSide_AddProjectGrantMember(t *testing.T) { { name: "invalid roles, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - ctx: context.Background(), - member: &domain.ProjectGrantMember{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", - }, - GrantID: "projectgrant1", - UserID: "user1", - Roles: []string{"PROJECT_GRANT_OWNER"}, + member: &AddProjectGrantMember{ + ProjectID: "project1", + GrantID: "projectgrant1", + UserID: "user1", + Roles: []string{"PROJECT_GRANT_OWNER"}, }, }, res: res{ @@ -79,10 +70,10 @@ func TestCommandSide_AddProjectGrantMember(t *testing.T) { { name: "user not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), zitadelRoles: []authz.RoleMapping{ { Role: "PROJECT_GRANT_OWNER", @@ -90,14 +81,11 @@ func TestCommandSide_AddProjectGrantMember(t *testing.T) { }, }, args: args{ - ctx: context.Background(), - member: &domain.ProjectGrantMember{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", - }, - GrantID: "projectgrant1", - UserID: "user1", - Roles: []string{"PROJECT_GRANT_OWNER"}, + member: &AddProjectGrantMember{ + ProjectID: "project1", + GrantID: "projectgrant1", + UserID: "user1", + Roles: []string{"PROJECT_GRANT_OWNER"}, }, }, res: res{ @@ -107,8 +95,7 @@ func TestCommandSide_AddProjectGrantMember(t *testing.T) { { name: "member add uniqueconstraint err, already exists", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -125,15 +112,28 @@ func TestCommandSide_AddProjectGrantMember(t *testing.T) { ), ), ), + expectFilter( + eventFromEventPusher( + eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + []string{"key1"}, + ), + ), + ), + ), + expectFilter(), expectPushFailed(zerrors.ThrowAlreadyExists(nil, "ERROR", "internal"), project.NewProjectGrantMemberAddedEvent(context.Background(), - &project.NewAggregate("project1", "").Aggregate, + &project.NewAggregate("project1", "org1").Aggregate, "user1", "projectgrant1", []string{"PROJECT_GRANT_OWNER"}..., ), ), ), + checkPermission: newMockPermissionCheckAllowed(), zitadelRoles: []authz.RoleMapping{ { Role: "PROJECT_GRANT_OWNER", @@ -141,14 +141,11 @@ func TestCommandSide_AddProjectGrantMember(t *testing.T) { }, }, args: args{ - ctx: context.Background(), - member: &domain.ProjectGrantMember{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", - }, - GrantID: "projectgrant1", - UserID: "user1", - Roles: []string{"PROJECT_GRANT_OWNER"}, + member: &AddProjectGrantMember{ + ProjectID: "project1", + GrantID: "projectgrant1", + UserID: "user1", + Roles: []string{"PROJECT_GRANT_OWNER"}, }, }, res: res{ @@ -158,8 +155,7 @@ func TestCommandSide_AddProjectGrantMember(t *testing.T) { { name: "member add, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -176,15 +172,28 @@ func TestCommandSide_AddProjectGrantMember(t *testing.T) { ), ), ), + expectFilter( + eventFromEventPusher( + eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + []string{"key1"}, + ), + ), + ), + ), + expectFilter(), expectPush( project.NewProjectGrantMemberAddedEvent(context.Background(), - &project.NewAggregate("project1", "").Aggregate, + &project.NewAggregate("project1", "org1").Aggregate, "user1", "projectgrant1", []string{"PROJECT_GRANT_OWNER"}..., ), ), ), + checkPermission: newMockPermissionCheckAllowed(), zitadelRoles: []authz.RoleMapping{ { Role: "PROJECT_GRANT_OWNER", @@ -192,35 +201,81 @@ func TestCommandSide_AddProjectGrantMember(t *testing.T) { }, }, args: args{ - ctx: context.Background(), - member: &domain.ProjectGrantMember{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", - }, - UserID: "user1", - GrantID: "projectgrant1", - Roles: []string{"PROJECT_GRANT_OWNER"}, + member: &AddProjectGrantMember{ + ProjectID: "project1", + GrantID: "projectgrant1", + UserID: "user1", + Roles: []string{"PROJECT_GRANT_OWNER"}, }, }, res: res{ - want: &domain.ProjectGrantMember{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", - }, - GrantID: "projectgrant1", - UserID: "user1", - Roles: []string{"PROJECT_GRANT_OWNER"}, + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + ID: "project1", }, }, }, + { + name: "member add, no permission", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username1", + "firstname1", + "lastname1", + "nickname1", + "displayname1", + language.German, + domain.GenderMale, + "email1", + true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + []string{"key1"}, + ), + ), + ), + ), + expectFilter(), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + zitadelRoles: []authz.RoleMapping{ + { + Role: "PROJECT_GRANT_OWNER", + }, + }, + }, + args: args{ + member: &AddProjectGrantMember{ + ProjectID: "project1", + GrantID: "projectgrant1", + UserID: "user1", + Roles: []string{"PROJECT_GRANT_OWNER"}, + }, + }, + res: res{ + err: zerrors.IsPermissionDenied, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, - zitadelRoles: tt.fields.zitadelRoles, + eventstore: tt.fields.eventstore(t), + zitadelRoles: tt.fields.zitadelRoles, + checkPermission: tt.fields.checkPermission, } - got, err := r.AddProjectGrantMember(tt.args.ctx, tt.args.member) + got, err := r.AddProjectGrantMember(context.Background(), tt.args.member) if tt.res.err == nil { assert.NoError(t, err) } @@ -236,15 +291,15 @@ func TestCommandSide_AddProjectGrantMember(t *testing.T) { func TestCommandSide_ChangeProjectGrantMember(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore - zitadelRoles []authz.RoleMapping + eventstore func(t *testing.T) *eventstore.Eventstore + zitadelRoles []authz.RoleMapping + checkPermission domain.PermissionCheck } type args struct { - ctx context.Context - member *domain.ProjectGrantMember + member *ChangeProjectGrantMember } type res struct { - want *domain.ProjectGrantMember + want *domain.ObjectDetails err func(error) bool } tests := []struct { @@ -256,16 +311,12 @@ func TestCommandSide_ChangeProjectGrantMember(t *testing.T) { { name: "invalid member, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - ctx: context.Background(), - member: &domain.ProjectGrantMember{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", - }, + member: &ChangeProjectGrantMember{ + ProjectID: "project1", }, }, res: res{ @@ -275,19 +326,15 @@ func TestCommandSide_ChangeProjectGrantMember(t *testing.T) { { name: "invalid roles, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - ctx: context.Background(), - member: &domain.ProjectGrantMember{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", - }, - GrantID: "projectgrant1", - UserID: "user1", - Roles: []string{"PROJECT_OWNER"}, + member: &ChangeProjectGrantMember{ + ProjectID: "project1", + GrantID: "projectgrant1", + UserID: "user1", + Roles: []string{"PROJECT_OWNER"}, }, }, res: res{ @@ -297,10 +344,20 @@ func TestCommandSide_ChangeProjectGrantMember(t *testing.T) { { name: "member not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "org2", + []string{"rol1", "role2"}, + ), + ), + ), expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), zitadelRoles: []authz.RoleMapping{ { Role: "PROJECT_GRANT_OWNER", @@ -308,14 +365,11 @@ func TestCommandSide_ChangeProjectGrantMember(t *testing.T) { }, }, args: args{ - ctx: context.Background(), - member: &domain.ProjectGrantMember{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", - }, - GrantID: "projectgrant1", - UserID: "user1", - Roles: []string{"PROJECT_GRANT_OWNER"}, + member: &ChangeProjectGrantMember{ + ProjectID: "project1", + GrantID: "projectgrant1", + UserID: "user1", + Roles: []string{"PROJECT_GRANT_OWNER"}, }, }, res: res{ @@ -325,8 +379,17 @@ func TestCommandSide_ChangeProjectGrantMember(t *testing.T) { { name: "member not changed, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "org2", + []string{"rol1", "role2"}, + ), + ), + ), expectFilter( eventFromEventPusher( project.NewProjectGrantMemberAddedEvent(context.Background(), @@ -338,6 +401,7 @@ func TestCommandSide_ChangeProjectGrantMember(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), zitadelRoles: []authz.RoleMapping{ { Role: "PROJECT_GRANT_OWNER", @@ -345,25 +409,33 @@ func TestCommandSide_ChangeProjectGrantMember(t *testing.T) { }, }, args: args{ - ctx: context.Background(), - member: &domain.ProjectGrantMember{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", - }, - GrantID: "projectgrant1", - UserID: "user1", - Roles: []string{"PROJECT_GRANT_OWNER"}, + member: &ChangeProjectGrantMember{ + ProjectID: "project1", + GrantID: "projectgrant1", + UserID: "user1", + Roles: []string{"PROJECT_GRANT_OWNER"}, }, }, res: res{ - err: zerrors.IsPreconditionFailed, + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, }, }, { name: "member change, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "org2", + []string{"rol1", "role2"}, + ), + ), + ), expectFilter( eventFromEventPusher( project.NewProjectGrantMemberAddedEvent(context.Background(), @@ -383,6 +455,7 @@ func TestCommandSide_ChangeProjectGrantMember(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), zitadelRoles: []authz.RoleMapping{ { Role: "PROJECT_GRANT_OWNER", @@ -393,36 +466,75 @@ func TestCommandSide_ChangeProjectGrantMember(t *testing.T) { }, }, args: args{ - ctx: context.Background(), - member: &domain.ProjectGrantMember{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", - }, - GrantID: "projectgrant1", - UserID: "user1", - Roles: []string{"PROJECT_GRANT_OWNER", "PROJECT_GRANT_VIEWER"}, + member: &ChangeProjectGrantMember{ + ProjectID: "project1", + GrantID: "projectgrant1", + UserID: "user1", + Roles: []string{"PROJECT_GRANT_OWNER", "PROJECT_GRANT_VIEWER"}, }, }, res: res{ - want: &domain.ProjectGrantMember{ - ObjectRoot: models.ObjectRoot{ - ResourceOwner: "org1", - AggregateID: "project1", - }, - GrantID: "projectgrant1", - UserID: "user1", - Roles: []string{"PROJECT_GRANT_OWNER", "PROJECT_GRANT_VIEWER"}, + want: &domain.ObjectDetails{ + ResourceOwner: "org1", }, }, }, + { + name: "member change, no permission", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "org2", + []string{"rol1", "role2"}, + ), + ), + ), + expectFilter( + eventFromEventPusher( + project.NewProjectGrantMemberAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "user1", + "projectgrant1", + []string{"PROJECT_GRANT_OWNER"}..., + ), + ), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + zitadelRoles: []authz.RoleMapping{ + { + Role: "PROJECT_GRANT_OWNER", + }, + { + Role: "PROJECT_GRANT_VIEWER", + }, + }, + }, + args: args{ + member: &ChangeProjectGrantMember{ + ProjectID: "project1", + GrantID: "projectgrant1", + UserID: "user1", + Roles: []string{"PROJECT_GRANT_OWNER", "PROJECT_GRANT_VIEWER"}, + }, + }, + res: res{ + err: zerrors.IsPermissionDenied, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, - zitadelRoles: tt.fields.zitadelRoles, + eventstore: tt.fields.eventstore(t), + zitadelRoles: tt.fields.zitadelRoles, + checkPermission: tt.fields.checkPermission, } - got, err := r.ChangeProjectGrantMember(tt.args.ctx, tt.args.member) + got, err := r.ChangeProjectGrantMember(context.Background(), tt.args.member) if tt.res.err == nil { assert.NoError(t, err) } @@ -430,7 +542,7 @@ func TestCommandSide_ChangeProjectGrantMember(t *testing.T) { t.Errorf("got wrong err: %v ", err) } if tt.res.err == nil { - assert.Equal(t, tt.res.want, got) + assertObjectDetails(t, tt.res.want, got) } }) } @@ -438,7 +550,8 @@ func TestCommandSide_ChangeProjectGrantMember(t *testing.T) { func TestCommandSide_RemoveProjectGrantMember(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck } type args struct { ctx context.Context @@ -459,9 +572,8 @@ func TestCommandSide_RemoveProjectGrantMember(t *testing.T) { { name: "invalid member projectid missing, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -476,9 +588,8 @@ func TestCommandSide_RemoveProjectGrantMember(t *testing.T) { { name: "invalid member userid missing, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -493,9 +604,8 @@ func TestCommandSide_RemoveProjectGrantMember(t *testing.T) { { name: "invalid member grantid missing, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -508,12 +618,22 @@ func TestCommandSide_RemoveProjectGrantMember(t *testing.T) { }, }, { - name: "member not existing, not found err", + name: "member not existing, not found ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "org2", + []string{"rol1", "role2"}, + ), + ), + ), expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -522,14 +642,25 @@ func TestCommandSide_RemoveProjectGrantMember(t *testing.T) { grantID: "projectgrant1", }, res: res{ - err: zerrors.IsNotFound, + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, }, }, { name: "member remove, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "org2", + []string{"rol1", "role2"}, + ), + ), + ), expectFilter( eventFromEventPusher( project.NewProjectGrantMemberAddedEvent(context.Background(), @@ -548,6 +679,7 @@ func TestCommandSide_RemoveProjectGrantMember(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -561,11 +693,49 @@ func TestCommandSide_RemoveProjectGrantMember(t *testing.T) { }, }, }, + { + name: "member remove, no permission", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "org2", + []string{"rol1", "role2"}, + ), + ), + ), + expectFilter( + eventFromEventPusher( + project.NewProjectGrantMemberAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "user1", + "projectgrant1", + []string{"PROJECT_OWNER"}..., + ), + ), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + userID: "user1", + grantID: "projectgrant1", + }, + res: res{ + err: zerrors.IsPermissionDenied, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, } got, err := r.RemoveProjectGrantMember(tt.args.ctx, tt.args.projectID, tt.args.userID, tt.args.grantID) if tt.res.err == nil { diff --git a/internal/command/project_grant_model.go b/internal/command/project_grant_model.go index c658b00b69..15950d4f3d 100644 --- a/internal/command/project_grant_model.go +++ b/internal/command/project_grant_model.go @@ -16,13 +16,14 @@ type ProjectGrantWriteModel struct { State domain.ProjectGrantState } -func NewProjectGrantWriteModel(grantID, projectID, resourceOwner string) *ProjectGrantWriteModel { +func NewProjectGrantWriteModel(grantID, grantedOrgID, projectID, resourceOwner string) *ProjectGrantWriteModel { return &ProjectGrantWriteModel{ WriteModel: eventstore.WriteModel{ AggregateID: projectID, ResourceOwner: resourceOwner, }, - GrantID: grantID, + GrantID: grantID, + GrantedOrgID: grantedOrgID, } } @@ -30,27 +31,28 @@ func (wm *ProjectGrantWriteModel) AppendEvents(events ...eventstore.Event) { for _, event := range events { switch e := event.(type) { case *project.GrantAddedEvent: - if e.GrantID == wm.GrantID { + if (wm.GrantID != "" && e.GrantID == wm.GrantID) || + (wm.GrantedOrgID != "" && e.GrantedOrgID == wm.GrantedOrgID) { wm.WriteModel.AppendEvents(e) } case *project.GrantChangedEvent: - if e.GrantID == wm.GrantID { + if wm.GrantID != "" && e.GrantID == wm.GrantID { wm.WriteModel.AppendEvents(e) } case *project.GrantCascadeChangedEvent: - if e.GrantID == wm.GrantID { + if wm.GrantID != "" && e.GrantID == wm.GrantID { wm.WriteModel.AppendEvents(e) } case *project.GrantDeactivateEvent: - if e.GrantID == wm.GrantID { + if wm.GrantID != "" && e.GrantID == wm.GrantID { wm.WriteModel.AppendEvents(e) } case *project.GrantReactivatedEvent: - if e.GrantID == wm.GrantID { + if wm.GrantID != "" && e.GrantID == wm.GrantID { wm.WriteModel.AppendEvents(e) } case *project.GrantRemovedEvent: - if e.GrantID == wm.GrantID { + if wm.GrantID != "" && e.GrantID == wm.GrantID { wm.WriteModel.AppendEvents(e) } case *project.ProjectRemovedEvent: @@ -114,18 +116,20 @@ func (wm *ProjectGrantWriteModel) Query() *eventstore.SearchQueryBuilder { type ProjectGrantPreConditionReadModel struct { eventstore.WriteModel - ProjectID string - GrantedOrgID string - ProjectExists bool - GrantedOrgExists bool - ExistingRoleKeys []string + ProjectResourceOwner string + ProjectID string + GrantedOrgID string + ProjectExists bool + GrantedOrgExists bool + ExistingRoleKeys []string } func NewProjectGrantPreConditionReadModel(projectID, grantedOrgID, resourceOwner string) *ProjectGrantPreConditionReadModel { return &ProjectGrantPreConditionReadModel{ - WriteModel: eventstore.WriteModel{ResourceOwner: resourceOwner}, - ProjectID: projectID, - GrantedOrgID: grantedOrgID, + WriteModel: eventstore.WriteModel{}, + ProjectResourceOwner: resourceOwner, + ProjectID: projectID, + GrantedOrgID: grantedOrgID, } } @@ -133,22 +137,26 @@ func (wm *ProjectGrantPreConditionReadModel) Reduce() error { for _, event := range wm.Events { switch e := event.(type) { case *project.ProjectAddedEvent: - if e.Aggregate().ResourceOwner != wm.ResourceOwner { + if wm.ProjectResourceOwner == "" { + wm.ProjectResourceOwner = e.Aggregate().ResourceOwner + } + if wm.ProjectResourceOwner != e.Aggregate().ResourceOwner { continue } wm.ProjectExists = true case *project.ProjectRemovedEvent: - if e.Aggregate().ResourceOwner != wm.ResourceOwner { + if wm.ProjectResourceOwner != e.Aggregate().ResourceOwner { continue } + wm.ProjectResourceOwner = "" wm.ProjectExists = false case *project.RoleAddedEvent: - if e.Aggregate().ResourceOwner != wm.ResourceOwner { + if e.Aggregate().ResourceOwner != wm.ProjectResourceOwner { continue } wm.ExistingRoleKeys = append(wm.ExistingRoleKeys, e.Key) case *project.RoleRemovedEvent: - if e.Aggregate().ResourceOwner != wm.ResourceOwner { + if e.Aggregate().ResourceOwner != wm.ProjectResourceOwner { continue } for i, key := range wm.ExistingRoleKeys { @@ -171,12 +179,6 @@ func (wm *ProjectGrantPreConditionReadModel) Reduce() error { func (wm *ProjectGrantPreConditionReadModel) Query() *eventstore.SearchQueryBuilder { query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). AddQuery(). - AggregateTypes(org.AggregateType). - AggregateIDs(wm.GrantedOrgID). - EventTypes( - org.OrgAddedEventType, - org.OrgRemovedEventType). - Or(). AggregateTypes(project.AggregateType). AggregateIDs(wm.ProjectID). EventTypes( @@ -184,6 +186,12 @@ func (wm *ProjectGrantPreConditionReadModel) Query() *eventstore.SearchQueryBuil project.ProjectRemovedType, project.RoleAddedType, project.RoleRemovedType). + Or(). + AggregateTypes(org.AggregateType). + AggregateIDs(wm.GrantedOrgID). + EventTypes( + org.OrgAddedEventType, + org.OrgRemovedEventType). Builder() return query diff --git a/internal/command/project_grant_test.go b/internal/command/project_grant_test.go index e39835e2f4..7a3bb98e7d 100644 --- a/internal/command/project_grant_test.go +++ b/internal/command/project_grant_test.go @@ -19,16 +19,16 @@ import ( func TestCommandSide_AddProjectGrant(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore - idGenerator id.Generator + eventstore func(t *testing.T) *eventstore.Eventstore + idGenerator id.Generator + checkPermission domain.PermissionCheck } type args struct { - ctx context.Context - projectGrant *domain.ProjectGrant - resourceOwner string + ctx context.Context + projectGrant *AddProjectGrant } type res struct { - want *domain.ProjectGrant + want *domain.ObjectDetails err func(error) bool } tests := []struct { @@ -40,18 +40,18 @@ func TestCommandSide_AddProjectGrant(t *testing.T) { { name: "invalid usergrant, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - projectGrant: &domain.ProjectGrant{ + projectGrant: &AddProjectGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", + AggregateID: "project1", + ResourceOwner: "org1", }, + GrantID: "grant1", }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsErrorInvalidArgument, @@ -60,20 +60,21 @@ func TestCommandSide_AddProjectGrant(t *testing.T) { { name: "project not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - projectGrant: &domain.ProjectGrant{ + projectGrant: &AddProjectGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", + AggregateID: "project1", + ResourceOwner: "org1", }, + GrantID: "grant1", GrantedOrgID: "grantedorg1", }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsPreconditionFailed, @@ -82,8 +83,7 @@ func TestCommandSide_AddProjectGrant(t *testing.T) { { name: "project not existing in org, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -108,16 +108,18 @@ func TestCommandSide_AddProjectGrant(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - projectGrant: &domain.ProjectGrant{ + projectGrant: &AddProjectGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", + AggregateID: "project1", + ResourceOwner: "org1", }, + GrantID: "grant1", GrantedOrgID: "grantedorg1", }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsPreconditionFailed, @@ -126,8 +128,7 @@ func TestCommandSide_AddProjectGrant(t *testing.T) { { name: "granted org not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -138,16 +139,18 @@ func TestCommandSide_AddProjectGrant(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - projectGrant: &domain.ProjectGrant{ + projectGrant: &AddProjectGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", + AggregateID: "project1", + ResourceOwner: "org1", }, + GrantID: "grant1", GrantedOrgID: "grantedorg1", }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsPreconditionFailed, @@ -156,8 +159,7 @@ func TestCommandSide_AddProjectGrant(t *testing.T) { { name: "project roles not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -174,27 +176,74 @@ func TestCommandSide_AddProjectGrant(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - projectGrant: &domain.ProjectGrant{ + projectGrant: &AddProjectGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", + AggregateID: "project1", + ResourceOwner: "org1", }, + GrantID: "grant1", GrantedOrgID: "grantedorg1", RoleKeys: []string{"key1"}, }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsPreconditionFailed, }, }, { - name: "usergrant for project, ok", + name: "grant for project, same resourceowner", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", true, true, true, + domain.PrivateLabelingSettingUnspecified, + ), + ), + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "granted org", + ), + ), + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "key1", + "key", + "", + ), + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "projectgrant1"), + }, + args: args{ + ctx: context.Background(), + projectGrant: &AddProjectGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + ResourceOwner: "org1", + }, + GrantedOrgID: "org1", + RoleKeys: []string{"key1"}, + }, + }, + res: res{ + err: zerrors.IsPreconditionFailed, + }, + }, + { + name: "grant for project, ok", + fields: fields{ + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -227,21 +276,67 @@ func TestCommandSide_AddProjectGrant(t *testing.T) { ), ), ), - idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "projectgrant1"), + checkPermission: newMockPermissionCheckAllowed(), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "projectgrant1"), }, args: args{ ctx: context.Background(), - projectGrant: &domain.ProjectGrant{ + projectGrant: &AddProjectGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", + AggregateID: "project1", + ResourceOwner: "org1", }, GrantedOrgID: "grantedorg1", RoleKeys: []string{"key1"}, }, - resourceOwner: "org1", }, res: res{ - want: &domain.ProjectGrant{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "grant for project, id, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", true, true, true, + domain.PrivateLabelingSettingUnspecified, + ), + ), + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &org.NewAggregate("grantedorg1").Aggregate, + "granted org", + ), + ), + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "key1", + "key", + "", + ), + ), + ), + expectPush( + project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + []string{"key1"}, + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectGrant: &AddProjectGrant{ ObjectRoot: models.ObjectRoot{ AggregateID: "project1", ResourceOwner: "org1", @@ -249,7 +344,11 @@ func TestCommandSide_AddProjectGrant(t *testing.T) { GrantID: "projectgrant1", GrantedOrgID: "grantedorg1", RoleKeys: []string{"key1"}, - State: domain.ProjectGrantStateActive, + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", }, }, }, @@ -257,10 +356,11 @@ func TestCommandSide_AddProjectGrant(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, - idGenerator: tt.fields.idGenerator, + eventstore: tt.fields.eventstore(t), + idGenerator: tt.fields.idGenerator, + checkPermission: tt.fields.checkPermission, } - got, err := r.AddProjectGrant(tt.args.ctx, tt.args.projectGrant, tt.args.resourceOwner) + got, err := r.AddProjectGrant(tt.args.ctx, tt.args.projectGrant) if tt.res.err == nil { assert.NoError(t, err) } @@ -268,7 +368,8 @@ func TestCommandSide_AddProjectGrant(t *testing.T) { t.Errorf("got wrong err: %v ", err) } if tt.res.err == nil { - assert.Equal(t, tt.res.want, got) + assert.NotEmpty(t, got.ID) + assertObjectDetails(t, tt.res.want, got) } }) } @@ -276,16 +377,16 @@ func TestCommandSide_AddProjectGrant(t *testing.T) { func TestCommandSide_ChangeProjectGrant(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck } type args struct { ctx context.Context - projectGrant *domain.ProjectGrant - resourceOwner string + projectGrant *ChangeProjectGrant cascadeUserGrantIDs []string } type res struct { - want *domain.ProjectGrant + want *domain.ObjectDetails err func(error) bool } tests := []struct { @@ -297,18 +398,17 @@ func TestCommandSide_ChangeProjectGrant(t *testing.T) { { name: "invalid projectgrant, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - projectGrant: &domain.ProjectGrant{ + projectGrant: &ChangeProjectGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", + AggregateID: "project1", + ResourceOwner: "org1", }, }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsErrorInvalidArgument, @@ -317,22 +417,21 @@ func TestCommandSide_ChangeProjectGrant(t *testing.T) { { name: "projectgrant not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - projectGrant: &domain.ProjectGrant{ + projectGrant: &ChangeProjectGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", + AggregateID: "project1", + ResourceOwner: "org1", }, - GrantID: "projectgrant1", - GrantedOrgID: "grantedorg1", - RoleKeys: []string{"key1"}, + GrantID: "projectgrant1", + RoleKeys: []string{"key1"}, }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsNotFound, @@ -341,8 +440,7 @@ func TestCommandSide_ChangeProjectGrant(t *testing.T) { { name: "project not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -353,17 +451,17 @@ func TestCommandSide_ChangeProjectGrant(t *testing.T) { ), expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - projectGrant: &domain.ProjectGrant{ + projectGrant: &ChangeProjectGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", + AggregateID: "project1", + ResourceOwner: "org1", }, - GrantID: "projectgrant1", - GrantedOrgID: "grantedorg1", + GrantID: "projectgrant1", }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsPreconditionFailed, @@ -372,8 +470,7 @@ func TestCommandSide_ChangeProjectGrant(t *testing.T) { { name: "project not existing in org, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -398,18 +495,18 @@ func TestCommandSide_ChangeProjectGrant(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - projectGrant: &domain.ProjectGrant{ + projectGrant: &ChangeProjectGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", + AggregateID: "project1", + ResourceOwner: "org1", }, - GrantID: "projectgrant1", - GrantedOrgID: "grantedorg1", - RoleKeys: []string{"key1"}, + GrantID: "projectgrant1", + RoleKeys: []string{"key1"}, }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsPreconditionFailed, @@ -418,8 +515,7 @@ func TestCommandSide_ChangeProjectGrant(t *testing.T) { { name: "granted org not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -438,17 +534,17 @@ func TestCommandSide_ChangeProjectGrant(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - projectGrant: &domain.ProjectGrant{ + projectGrant: &ChangeProjectGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", + AggregateID: "project1", + ResourceOwner: "org1", }, - GrantID: "projectgrant1", - GrantedOrgID: "grantedorg1", + GrantID: "projectgrant1", }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsPreconditionFailed, @@ -457,8 +553,7 @@ func TestCommandSide_ChangeProjectGrant(t *testing.T) { { name: "project roles not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -483,18 +578,18 @@ func TestCommandSide_ChangeProjectGrant(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - projectGrant: &domain.ProjectGrant{ + projectGrant: &ChangeProjectGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", + AggregateID: "project1", + ResourceOwner: "org1", }, - GrantID: "projectgrant1", - GrantedOrgID: "grantedorg1", - RoleKeys: []string{"key1"}, + GrantID: "projectgrant1", + RoleKeys: []string{"key1"}, }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsPreconditionFailed, @@ -503,8 +598,7 @@ func TestCommandSide_ChangeProjectGrant(t *testing.T) { { name: "projectgrant not changed, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -537,28 +631,29 @@ func TestCommandSide_ChangeProjectGrant(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - projectGrant: &domain.ProjectGrant{ + projectGrant: &ChangeProjectGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", + AggregateID: "project1", + ResourceOwner: "org1", }, - GrantID: "projectgrant1", - GrantedOrgID: "grantedorg1", - RoleKeys: []string{"key1"}, + GrantID: "projectgrant1", + RoleKeys: []string{"key1"}, }, - resourceOwner: "org1", }, res: res{ - err: zerrors.IsPreconditionFailed, + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, }, }, { name: "projectgrant only added roles, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -606,37 +701,99 @@ func TestCommandSide_ChangeProjectGrant(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - projectGrant: &domain.ProjectGrant{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", - }, - GrantID: "projectgrant1", - GrantedOrgID: "grantedorg1", - RoleKeys: []string{"key1", "key2"}, - }, - resourceOwner: "org1", - }, - res: res{ - want: &domain.ProjectGrant{ + projectGrant: &ChangeProjectGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + ResourceOwner: "org1", + }, + GrantID: "projectgrant1", + RoleKeys: []string{"key1", "key2"}, + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "projectgrant only added roles, grantedOrgID, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + []string{"key1"}, + )), + ), + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", true, true, true, + domain.PrivateLabelingSettingUnspecified, + ), + ), + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &org.NewAggregate("grantedorg1").Aggregate, + "granted org", + ), + ), + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "key1", + "key", + "", + ), + ), + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "key2", + "key2", + "", + ), + ), + ), + expectPush( + project.NewGrantChangedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + []string{"key1", "key2"}, + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectGrant: &ChangeProjectGrant{ ObjectRoot: models.ObjectRoot{ AggregateID: "project1", ResourceOwner: "org1", }, - GrantID: "projectgrant1", GrantedOrgID: "grantedorg1", RoleKeys: []string{"key1", "key2"}, - State: domain.ProjectGrantStateActive, + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", }, }, }, { name: "projectgrant remove roles, usergrant not found, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -685,38 +842,30 @@ func TestCommandSide_ChangeProjectGrant(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - projectGrant: &domain.ProjectGrant{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", - }, - GrantID: "projectgrant1", - GrantedOrgID: "grantedorg1", - RoleKeys: []string{"key1"}, - }, - resourceOwner: "org1", - cascadeUserGrantIDs: []string{"usergrant1"}, - }, - res: res{ - want: &domain.ProjectGrant{ + projectGrant: &ChangeProjectGrant{ ObjectRoot: models.ObjectRoot{ AggregateID: "project1", ResourceOwner: "org1", }, - GrantID: "projectgrant1", - GrantedOrgID: "grantedorg1", - RoleKeys: []string{"key1"}, - State: domain.ProjectGrantStateActive, + GrantID: "projectgrant1", + RoleKeys: []string{"key1"}, + }, + cascadeUserGrantIDs: []string{"usergrant1"}, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", }, }, }, { name: "projectgrant remove roles, usergrant not found, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -778,30 +927,23 @@ func TestCommandSide_ChangeProjectGrant(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - projectGrant: &domain.ProjectGrant{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", - }, - GrantID: "projectgrant1", - GrantedOrgID: "grantedorg1", - RoleKeys: []string{"key1"}, - }, - resourceOwner: "org1", - cascadeUserGrantIDs: []string{"usergrant1"}, - }, - res: res{ - want: &domain.ProjectGrant{ + projectGrant: &ChangeProjectGrant{ ObjectRoot: models.ObjectRoot{ AggregateID: "project1", ResourceOwner: "org1", }, - GrantID: "projectgrant1", - GrantedOrgID: "grantedorg1", - RoleKeys: []string{"key1"}, - State: domain.ProjectGrantStateActive, + GrantID: "projectgrant1", + RoleKeys: []string{"key1"}, + }, + cascadeUserGrantIDs: []string{"usergrant1"}, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", }, }, }, @@ -809,9 +951,10 @@ func TestCommandSide_ChangeProjectGrant(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, } - got, err := r.ChangeProjectGrant(tt.args.ctx, tt.args.projectGrant, tt.args.resourceOwner, tt.args.cascadeUserGrantIDs...) + got, err := r.ChangeProjectGrant(tt.args.ctx, tt.args.projectGrant, tt.args.cascadeUserGrantIDs...) if tt.res.err == nil { assert.NoError(t, err) } @@ -819,7 +962,7 @@ func TestCommandSide_ChangeProjectGrant(t *testing.T) { t.Errorf("got wrong err: %v ", err) } if tt.res.err == nil { - assert.Equal(t, tt.res.want, got) + assertObjectDetails(t, tt.res.want, got) } }) } @@ -827,12 +970,14 @@ func TestCommandSide_ChangeProjectGrant(t *testing.T) { func TestCommandSide_DeactivateProjectGrant(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck } type args struct { ctx context.Context projectID string grantID string + grantedOrgID string resourceOwner string } type res struct { @@ -848,9 +993,8 @@ func TestCommandSide_DeactivateProjectGrant(t *testing.T) { { name: "missing projectid, invalid error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -864,9 +1008,8 @@ func TestCommandSide_DeactivateProjectGrant(t *testing.T) { { name: "missing grantid, invalid error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -880,10 +1023,10 @@ func TestCommandSide_DeactivateProjectGrant(t *testing.T) { { name: "project not existing, precondition failed error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -898,8 +1041,7 @@ func TestCommandSide_DeactivateProjectGrant(t *testing.T) { { name: "projectgrant not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -911,6 +1053,7 @@ func TestCommandSide_DeactivateProjectGrant(t *testing.T) { ), expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -923,10 +1066,9 @@ func TestCommandSide_DeactivateProjectGrant(t *testing.T) { }, }, { - name: "projectgrant already deactivated, precondition error", + name: "projectgrant already deactivated, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -949,6 +1091,7 @@ func TestCommandSide_DeactivateProjectGrant(t *testing.T) { )), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -957,14 +1100,15 @@ func TestCommandSide_DeactivateProjectGrant(t *testing.T) { resourceOwner: "org1", }, res: res{ - err: zerrors.IsPreconditionFailed, + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, }, }, { name: "projectgrant deactivate, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -989,6 +1133,7 @@ func TestCommandSide_DeactivateProjectGrant(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -1002,13 +1147,56 @@ func TestCommandSide_DeactivateProjectGrant(t *testing.T) { }, }, }, + { + name: "projectgrant deactivate, grantedOrgID, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", true, true, true, + domain.PrivateLabelingSettingUnspecified, + ), + ), + ), + expectFilter( + eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + []string{"key1"}, + )), + ), + expectPush( + project.NewGrantDeactivateEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + grantedOrgID: "grantedorg1", + resourceOwner: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, } - got, err := r.DeactivateProjectGrant(tt.args.ctx, tt.args.projectID, tt.args.grantID, tt.args.resourceOwner) + got, err := r.DeactivateProjectGrant(tt.args.ctx, tt.args.projectID, tt.args.grantID, tt.args.grantedOrgID, tt.args.resourceOwner) if tt.res.err == nil { assert.NoError(t, err) } @@ -1024,12 +1212,14 @@ func TestCommandSide_DeactivateProjectGrant(t *testing.T) { func TestCommandSide_ReactivateProjectGrant(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck } type args struct { ctx context.Context projectID string grantID string + grantedOrgID string resourceOwner string } type res struct { @@ -1045,9 +1235,8 @@ func TestCommandSide_ReactivateProjectGrant(t *testing.T) { { name: "missing projectid, invalid error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -1061,9 +1250,8 @@ func TestCommandSide_ReactivateProjectGrant(t *testing.T) { { name: "missing grantid, invalid error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -1077,10 +1265,10 @@ func TestCommandSide_ReactivateProjectGrant(t *testing.T) { { name: "project not existing, precondition failed error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -1095,8 +1283,7 @@ func TestCommandSide_ReactivateProjectGrant(t *testing.T) { { name: "projectgrant not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -1108,6 +1295,7 @@ func TestCommandSide_ReactivateProjectGrant(t *testing.T) { ), expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -1122,8 +1310,7 @@ func TestCommandSide_ReactivateProjectGrant(t *testing.T) { { name: "projectgrant not inactive, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -1142,6 +1329,7 @@ func TestCommandSide_ReactivateProjectGrant(t *testing.T) { )), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -1150,14 +1338,15 @@ func TestCommandSide_ReactivateProjectGrant(t *testing.T) { resourceOwner: "org1", }, res: res{ - err: zerrors.IsPreconditionFailed, + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, }, }, { name: "projectgrant reactivate, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -1186,6 +1375,7 @@ func TestCommandSide_ReactivateProjectGrant(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -1199,13 +1389,60 @@ func TestCommandSide_ReactivateProjectGrant(t *testing.T) { }, }, }, + { + name: "projectgrant reactivate, grantedOrgID, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", true, true, true, + domain.PrivateLabelingSettingUnspecified, + ), + ), + ), + expectFilter( + eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + []string{"key1"}, + )), + eventFromEventPusher(project.NewGrantDeactivateEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + )), + ), + expectPush( + project.NewGrantReactivatedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + grantedOrgID: "grantedorg1", + resourceOwner: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, } - got, err := r.ReactivateProjectGrant(tt.args.ctx, tt.args.projectID, tt.args.grantID, tt.args.resourceOwner) + got, err := r.ReactivateProjectGrant(tt.args.ctx, tt.args.projectID, tt.args.grantID, tt.args.grantedOrgID, tt.args.resourceOwner) if tt.res.err == nil { assert.NoError(t, err) } @@ -1221,7 +1458,8 @@ func TestCommandSide_ReactivateProjectGrant(t *testing.T) { func TestCommandSide_RemoveProjectGrant(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck } type args struct { ctx context.Context @@ -1243,9 +1481,8 @@ func TestCommandSide_RemoveProjectGrant(t *testing.T) { { name: "missing projectid, invalid error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -1259,9 +1496,8 @@ func TestCommandSide_RemoveProjectGrant(t *testing.T) { { name: "missing grantid, invalid error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -1275,8 +1511,7 @@ func TestCommandSide_RemoveProjectGrant(t *testing.T) { { name: "project already removed, precondition failed error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -1293,6 +1528,7 @@ func TestCommandSide_RemoveProjectGrant(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -1307,10 +1543,10 @@ func TestCommandSide_RemoveProjectGrant(t *testing.T) { { name: "projectgrant not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -1325,8 +1561,7 @@ func TestCommandSide_RemoveProjectGrant(t *testing.T) { { name: "projectgrant remove, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -1343,6 +1578,7 @@ func TestCommandSide_RemoveProjectGrant(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -1359,8 +1595,7 @@ func TestCommandSide_RemoveProjectGrant(t *testing.T) { { name: "projectgrant remove, cascading usergrant not found, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -1378,6 +1613,7 @@ func TestCommandSide_RemoveProjectGrant(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -1395,8 +1631,7 @@ func TestCommandSide_RemoveProjectGrant(t *testing.T) { { name: "projectgrant remove with cascading usergrants, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), &project.NewAggregate("project1", "org1").Aggregate, @@ -1426,6 +1661,7 @@ func TestCommandSide_RemoveProjectGrant(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -1444,7 +1680,8 @@ func TestCommandSide_RemoveProjectGrant(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, } got, err := r.RemoveProjectGrant(tt.args.ctx, tt.args.projectID, tt.args.grantID, tt.args.resourceOwner, tt.args.cascadeUserGrantIDs...) if tt.res.err == nil { @@ -1459,3 +1696,283 @@ func TestCommandSide_RemoveProjectGrant(t *testing.T) { }) } } + +func TestCommandSide_DeleteProjectGrant(t *testing.T) { + type fields struct { + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck + } + type args struct { + ctx context.Context + projectID string + grantID string + grantedOrgID string + resourceOwner string + cascadeUserGrantIDs []string + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "missing projectid, invalid error", + fields: fields{ + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + grantID: "projectgrant1", + resourceOwner: "org1", + }, + res: res{ + err: zerrors.IsErrorInvalidArgument, + }, + }, + { + name: "missing grantid, invalid error", + fields: fields{ + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + resourceOwner: "org1", + }, + res: res{ + err: zerrors.IsErrorInvalidArgument, + }, + }, + { + name: "project already removed, precondition failed error", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + []string{"key1"}, + )), + eventFromEventPusher( + project.NewProjectRemovedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", + nil, + ), + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + grantID: "projectgrant1", + resourceOwner: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "projectgrant not existing, precondition error", + fields: fields{ + eventstore: expectEventstore( + expectFilter(), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + grantID: "projectgrant1", + resourceOwner: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "projectgrant remove, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + []string{"key1"}, + )), + ), + expectPush( + project.NewGrantRemovedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + grantID: "projectgrant1", + resourceOwner: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "projectgrant remove, grantedOrgID, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + []string{"key1"}, + )), + ), + expectPush( + project.NewGrantRemovedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + grantedOrgID: "grantedorg1", + resourceOwner: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "projectgrant remove, cascading usergrant not found, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + []string{"key1"}, + )), + ), + expectFilter(), + expectPush( + project.NewGrantRemovedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + grantID: "projectgrant1", + resourceOwner: "org1", + cascadeUserGrantIDs: []string{"usergrant1"}, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "projectgrant remove with cascading usergrants, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + []string{"key1"}, + )), + ), + expectFilter( + eventFromEventPusher(usergrant.NewUserGrantAddedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + "user1", + "project1", + "projectgrant1", + []string{"key1"}))), + expectPush( + project.NewGrantRemovedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + ), + usergrant.NewUserGrantCascadeRemovedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + "user1", + "project1", + "projectgrant1", + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + grantID: "projectgrant1", + resourceOwner: "org1", + cascadeUserGrantIDs: []string{"usergrant1"}, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, + } + got, err := r.DeleteProjectGrant(tt.args.ctx, tt.args.projectID, tt.args.grantID, tt.args.grantedOrgID, tt.args.resourceOwner, tt.args.cascadeUserGrantIDs...) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assertObjectDetails(t, tt.res.want, got) + } + }) + } +} diff --git a/internal/command/project_member.go b/internal/command/project_member.go index a2e4fae553..1bbd358490 100644 --- a/internal/command/project_member.go +++ b/internal/command/project_member.go @@ -2,8 +2,9 @@ package command import ( "context" - "reflect" + "slices" + "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/repository/project" @@ -11,18 +12,64 @@ import ( "github.com/zitadel/zitadel/internal/zerrors" ) -func (c *Commands) AddProjectMember(ctx context.Context, member *domain.Member, resourceOwner string) (_ *domain.Member, err error) { +type AddProjectMember struct { + ResourceOwner string + ProjectID string + UserID string + Roles []string +} + +func (i *AddProjectMember) IsValid(zitadelRoles []authz.RoleMapping) error { + if i.ProjectID == "" || i.UserID == "" || len(i.Roles) == 0 { + return zerrors.ThrowInvalidArgument(nil, "PROJECT-W8m4l", "Errors.Project.Member.Invalid") + } + if len(domain.CheckForInvalidRoles(i.Roles, domain.ProjectRolePrefix, zitadelRoles)) > 0 { + return zerrors.ThrowInvalidArgument(nil, "PROJECT-3m9ds", "Errors.Project.Member.Invalid") + } + return nil +} + +func (c *Commands) AddProjectMember(ctx context.Context, member *AddProjectMember) (_ *domain.ObjectDetails, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - addedMember := NewProjectMemberWriteModel(member.AggregateID, member.UserID, resourceOwner) - projectAgg := ProjectAggregateFromWriteModel(&addedMember.WriteModel) - event, err := c.addProjectMember(ctx, projectAgg, addedMember, member) + if err := member.IsValid(c.zitadelRoles); err != nil { + return nil, err + } + _, err = c.checkUserExists(ctx, member.UserID, "") if err != nil { return nil, err } + projectResourceOwner, err := c.checkProjectExists(ctx, member.ProjectID, member.ResourceOwner) + if err != nil { + return nil, err + } + // resourceowner of the member if not provided is the resourceowner of the project + if member.ResourceOwner == "" { + member.ResourceOwner = projectResourceOwner + } + addedMember, err := c.projectMemberWriteModelByID(ctx, member.ProjectID, member.UserID, member.ResourceOwner) + if err != nil { + return nil, err + } + // error if provided resourceowner is not equal to the resourceowner of the project + if projectResourceOwner != addedMember.ResourceOwner { + return nil, zerrors.ThrowPreconditionFailed(nil, "PROJECT-0l10S9OmZV", "Errors.Project.Member.Invalid") + } + if err := c.checkPermissionUpdateProjectMember(ctx, addedMember.ResourceOwner, addedMember.AggregateID); err != nil { + return nil, err + } + if addedMember.State.Exists() { + return nil, zerrors.ThrowAlreadyExists(nil, "PROJECT-PtXi1", "Errors.Project.Member.AlreadyExists") + } - pushedEvents, err := c.eventstore.Push(ctx, event) + pushedEvents, err := c.eventstore.Push(ctx, + project.NewProjectMemberAddedEvent(ctx, + ProjectAggregateFromWriteModelWithCTX(ctx, &addedMember.WriteModel), + member.UserID, + member.Roles..., + ), + ) if err != nil { return nil, err } @@ -31,53 +78,46 @@ func (c *Commands) AddProjectMember(ctx context.Context, member *domain.Member, return nil, err } - return memberWriteModelToMember(&addedMember.MemberWriteModel), nil + return writeModelToObjectDetails(&addedMember.WriteModel), nil } -func (c *Commands) addProjectMember(ctx context.Context, projectAgg *eventstore.Aggregate, addedMember *ProjectMemberWriteModel, member *domain.Member) (_ eventstore.Command, err error) { - ctx, span := tracing.NewSpan(ctx) - defer func() { span.EndWithError(err) }() +type ChangeProjectMember struct { + ResourceOwner string + ProjectID string + UserID string + Roles []string +} - if !member.IsValid() { - return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-W8m4l", "Errors.Project.Member.Invalid") +func (i *ChangeProjectMember) IsValid(zitadelRoles []authz.RoleMapping) error { + if i.ProjectID == "" || i.UserID == "" || len(i.Roles) == 0 { + return zerrors.ThrowInvalidArgument(nil, "PROJECT-LiaZi", "Errors.Project.Member.Invalid") } - if len(domain.CheckForInvalidRoles(member.Roles, domain.ProjectRolePrefix, c.zitadelRoles)) > 0 { - return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-3m9ds", "Errors.Project.Member.Invalid") + if len(domain.CheckForInvalidRoles(i.Roles, domain.ProjectRolePrefix, zitadelRoles)) > 0 { + return zerrors.ThrowInvalidArgument(nil, "PROJECT-3m9d", "Errors.Project.Member.Invalid") } - - err = c.checkUserExists(ctx, addedMember.UserID, "") - if err != nil { - return nil, err - } - err = c.eventstore.FilterToQueryReducer(ctx, addedMember) - if err != nil { - return nil, err - } - if addedMember.State == domain.MemberStateActive { - return nil, zerrors.ThrowAlreadyExists(nil, "PROJECT-PtXi1", "Errors.Project.Member.AlreadyExists") - } - - return project.NewProjectMemberAddedEvent(ctx, projectAgg, member.UserID, member.Roles...), nil + return nil } // ChangeProjectMember updates an existing member -func (c *Commands) ChangeProjectMember(ctx context.Context, member *domain.Member, resourceOwner string) (*domain.Member, error) { - if !member.IsValid() { - return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-LiaZi", "Errors.Project.Member.Invalid") - } - if len(domain.CheckForInvalidRoles(member.Roles, domain.ProjectRolePrefix, c.zitadelRoles)) > 0 { - return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-3m9d", "Errors.Project.Member.Invalid") - } - - existingMember, err := c.projectMemberWriteModelByID(ctx, member.AggregateID, member.UserID, resourceOwner) - if err != nil { +func (c *Commands) ChangeProjectMember(ctx context.Context, member *ChangeProjectMember) (*domain.ObjectDetails, error) { + if err := member.IsValid(c.zitadelRoles); err != nil { return nil, err } - if reflect.DeepEqual(existingMember.Roles, member.Roles) { - return nil, zerrors.ThrowPreconditionFailed(nil, "PROJECT-LiaZi", "Errors.Project.Member.RolesNotChanged") + existingMember, err := c.projectMemberWriteModelByID(ctx, member.ProjectID, member.UserID, member.ResourceOwner) + if err != nil { + return nil, err } - projectAgg := ProjectAggregateFromWriteModel(&existingMember.MemberWriteModel.WriteModel) + if !existingMember.State.Exists() { + return nil, zerrors.ThrowNotFound(nil, "PROJECT-D8JxR", "Errors.NotFound") + } + if err := c.checkPermissionUpdateProjectMember(ctx, existingMember.ResourceOwner, existingMember.AggregateID); err != nil { + return nil, err + } + if slices.Compare(existingMember.Roles, member.Roles) == 0 { + return writeModelToObjectDetails(&existingMember.WriteModel), nil + } + projectAgg := ProjectAggregateFromWriteModelWithCTX(ctx, &existingMember.WriteModel) pushedEvents, err := c.eventstore.Push(ctx, project.NewProjectMemberChangedEvent(ctx, projectAgg, member.UserID, member.Roles...)) if err != nil { return nil, err @@ -88,33 +128,35 @@ func (c *Commands) ChangeProjectMember(ctx context.Context, member *domain.Membe return nil, err } - return memberWriteModelToMember(&existingMember.MemberWriteModel), nil + return writeModelToObjectDetails(&existingMember.WriteModel), nil } func (c *Commands) RemoveProjectMember(ctx context.Context, projectID, userID, resourceOwner string) (*domain.ObjectDetails, error) { if projectID == "" || userID == "" { return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-66mHd", "Errors.Project.Member.Invalid") } - m, err := c.projectMemberWriteModelByID(ctx, projectID, userID, resourceOwner) - if err != nil && !zerrors.IsNotFound(err) { + existingMember, err := c.projectMemberWriteModelByID(ctx, projectID, userID, resourceOwner) + if err != nil { return nil, err } - if zerrors.IsNotFound(err) { - // empty response because we have no data that match the request - return &domain.ObjectDetails{}, nil + if !existingMember.State.Exists() { + return writeModelToObjectDetails(&existingMember.WriteModel), nil + } + if err := c.checkPermissionDeleteProjectMember(ctx, existingMember.ResourceOwner, existingMember.AggregateID); err != nil { + return nil, err } - projectAgg := ProjectAggregateFromWriteModel(&m.MemberWriteModel.WriteModel) + projectAgg := ProjectAggregateFromWriteModelWithCTX(ctx, &existingMember.WriteModel) removeEvent := c.removeProjectMember(ctx, projectAgg, userID, false) pushedEvents, err := c.eventstore.Push(ctx, removeEvent) if err != nil { return nil, err } - err = AppendAndReduce(m, pushedEvents...) + err = AppendAndReduce(existingMember, pushedEvents...) if err != nil { return nil, err } - return writeModelToObjectDetails(&m.WriteModel), nil + return writeModelToObjectDetails(&existingMember.WriteModel), nil } func (c *Commands) removeProjectMember(ctx context.Context, projectAgg *eventstore.Aggregate, userID string, cascade bool) eventstore.Command { @@ -138,9 +180,5 @@ func (c *Commands) projectMemberWriteModelByID(ctx context.Context, projectID, u return nil, err } - if writeModel.State == domain.MemberStateUnspecified || writeModel.State == domain.MemberStateRemoved { - return nil, zerrors.ThrowNotFound(nil, "PROJECT-D8JxR", "Errors.NotFound") - } - return writeModel, nil } diff --git a/internal/command/project_member_test.go b/internal/command/project_member_test.go index 88b52f63f8..1bd06e0b99 100644 --- a/internal/command/project_member_test.go +++ b/internal/command/project_member_test.go @@ -10,7 +10,6 @@ import ( "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" - "github.com/zitadel/zitadel/internal/eventstore/v1/models" "github.com/zitadel/zitadel/internal/repository/project" "github.com/zitadel/zitadel/internal/repository/user" "github.com/zitadel/zitadel/internal/zerrors" @@ -18,16 +17,15 @@ import ( func TestCommandSide_AddProjectMember(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore - zitadelRoles []authz.RoleMapping + eventstore func(t *testing.T) *eventstore.Eventstore + zitadelRoles []authz.RoleMapping + checkPermission domain.PermissionCheck } type args struct { - ctx context.Context - member *domain.Member - resourceOwner string + member *AddProjectMember } type res struct { - want *domain.Member + want *domain.ObjectDetails err func(error) bool } tests := []struct { @@ -39,18 +37,14 @@ func TestCommandSide_AddProjectMember(t *testing.T) { { name: "invalid member, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - ctx: context.Background(), - member: &domain.Member{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", - }, + member: &AddProjectMember{ + ResourceOwner: "org1", + ProjectID: "project1", }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsErrorInvalidArgument, @@ -59,20 +53,16 @@ func TestCommandSide_AddProjectMember(t *testing.T) { { name: "invalid roles, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - ctx: context.Background(), - member: &domain.Member{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", - }, - UserID: "user1", - Roles: []string{"PROJECT_OWNER"}, + member: &AddProjectMember{ + ResourceOwner: "org1", + ProjectID: "project1", + UserID: "user1", + Roles: []string{"PROJECT_OWNER"}, }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsErrorInvalidArgument, @@ -81,10 +71,10 @@ func TestCommandSide_AddProjectMember(t *testing.T) { { name: "user not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), zitadelRoles: []authz.RoleMapping{ { Role: domain.RoleProjectOwner, @@ -92,15 +82,12 @@ func TestCommandSide_AddProjectMember(t *testing.T) { }, }, args: args{ - ctx: context.Background(), - member: &domain.Member{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", - }, - UserID: "user1", - Roles: []string{"PROJECT_OWNER"}, + member: &AddProjectMember{ + ResourceOwner: "org1", + ProjectID: "project1", + UserID: "user1", + Roles: []string{"PROJECT_OWNER"}, }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsPreconditionFailed, @@ -109,8 +96,7 @@ func TestCommandSide_AddProjectMember(t *testing.T) { { name: "member already exists, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -127,6 +113,19 @@ func TestCommandSide_AddProjectMember(t *testing.T) { ), ), ), + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent( + context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project", + false, + false, + false, + domain.PrivateLabelingSettingUnspecified, + ), + ), + ), expectFilter( eventFromEventPusher( project.NewProjectMemberAddedEvent(context.Background(), @@ -136,6 +135,7 @@ func TestCommandSide_AddProjectMember(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), zitadelRoles: []authz.RoleMapping{ { Role: domain.RoleProjectOwner, @@ -143,15 +143,12 @@ func TestCommandSide_AddProjectMember(t *testing.T) { }, }, args: args{ - ctx: context.Background(), - member: &domain.Member{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", - }, - UserID: "user1", - Roles: []string{"PROJECT_OWNER"}, + member: &AddProjectMember{ + ResourceOwner: "org1", + ProjectID: "project1", + UserID: "user1", + Roles: []string{"PROJECT_OWNER"}, }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsErrorAlreadyExists, @@ -160,8 +157,7 @@ func TestCommandSide_AddProjectMember(t *testing.T) { { name: "member add uniqueconstraint err, already exists", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -178,6 +174,19 @@ func TestCommandSide_AddProjectMember(t *testing.T) { ), ), ), + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent( + context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project", + false, + false, + false, + domain.PrivateLabelingSettingUnspecified, + ), + ), + ), expectFilter(), expectPushFailed(zerrors.ThrowAlreadyExists(nil, "ERROR", "internal"), project.NewProjectMemberAddedEvent(context.Background(), @@ -187,6 +196,7 @@ func TestCommandSide_AddProjectMember(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), zitadelRoles: []authz.RoleMapping{ { Role: domain.RoleProjectOwner, @@ -194,15 +204,12 @@ func TestCommandSide_AddProjectMember(t *testing.T) { }, }, args: args{ - ctx: context.Background(), - member: &domain.Member{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", - }, - UserID: "user1", - Roles: []string{"PROJECT_OWNER"}, + member: &AddProjectMember{ + ResourceOwner: "org1", + ProjectID: "project1", + UserID: "user1", + Roles: []string{"PROJECT_OWNER"}, }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsErrorAlreadyExists, @@ -211,8 +218,7 @@ func TestCommandSide_AddProjectMember(t *testing.T) { { name: "member add, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -229,6 +235,19 @@ func TestCommandSide_AddProjectMember(t *testing.T) { ), ), ), + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent( + context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project", + false, + false, + false, + domain.PrivateLabelingSettingUnspecified, + ), + ), + ), expectFilter(), expectPush( project.NewProjectMemberAddedEvent(context.Background(), @@ -238,6 +257,7 @@ func TestCommandSide_AddProjectMember(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), zitadelRoles: []authz.RoleMapping{ { Role: domain.RoleProjectOwner, @@ -245,35 +265,81 @@ func TestCommandSide_AddProjectMember(t *testing.T) { }, }, args: args{ - ctx: context.Background(), - member: &domain.Member{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", - }, - UserID: "user1", - Roles: []string{"PROJECT_OWNER"}, + member: &AddProjectMember{ + ResourceOwner: "org1", + ProjectID: "project1", + UserID: "user1", + Roles: []string{"PROJECT_OWNER"}, }, - resourceOwner: "org1", }, res: res{ - want: &domain.Member{ - ObjectRoot: models.ObjectRoot{ - ResourceOwner: "org1", - AggregateID: "project1", - }, - UserID: "user1", - Roles: []string{domain.RoleProjectOwner}, + want: &domain.ObjectDetails{ + ResourceOwner: "org1", }, }, + }, { + name: "member add, no permission", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username1", + "firstname1", + "lastname1", + "nickname1", + "displayname1", + language.German, + domain.GenderMale, + "email1", + true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent( + context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project", + false, + false, + false, + domain.PrivateLabelingSettingUnspecified, + ), + ), + ), + expectFilter(), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + zitadelRoles: []authz.RoleMapping{ + { + Role: domain.RoleProjectOwner, + }, + }, + }, + args: args{ + member: &AddProjectMember{ + ResourceOwner: "org1", + ProjectID: "project1", + UserID: "user1", + Roles: []string{"PROJECT_OWNER"}, + }, + }, + res: res{ + err: zerrors.IsPermissionDenied, + }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, - zitadelRoles: tt.fields.zitadelRoles, + eventstore: tt.fields.eventstore(t), + zitadelRoles: tt.fields.zitadelRoles, + checkPermission: tt.fields.checkPermission, } - got, err := r.AddProjectMember(tt.args.ctx, tt.args.member, tt.args.resourceOwner) + got, err := r.AddProjectMember(context.Background(), tt.args.member) if tt.res.err == nil { assert.NoError(t, err) } @@ -281,7 +347,7 @@ func TestCommandSide_AddProjectMember(t *testing.T) { t.Errorf("got wrong err: %v ", err) } if tt.res.err == nil { - assert.Equal(t, tt.res.want, got) + assertObjectDetails(t, tt.res.want, got) } }) } @@ -289,16 +355,15 @@ func TestCommandSide_AddProjectMember(t *testing.T) { func TestCommandSide_ChangeProjectMember(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore - zitadelRoles []authz.RoleMapping + eventstore func(t *testing.T) *eventstore.Eventstore + zitadelRoles []authz.RoleMapping + checkPermission domain.PermissionCheck } type args struct { - ctx context.Context - member *domain.Member - resourceOwner string + member *ChangeProjectMember } type res struct { - want *domain.Member + want *domain.ObjectDetails err func(error) bool } tests := []struct { @@ -310,18 +375,14 @@ func TestCommandSide_ChangeProjectMember(t *testing.T) { { name: "invalid member, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - ctx: context.Background(), - member: &domain.Member{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", - }, + member: &ChangeProjectMember{ + ResourceOwner: "org1", + ProjectID: "project1", }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsErrorInvalidArgument, @@ -330,20 +391,16 @@ func TestCommandSide_ChangeProjectMember(t *testing.T) { { name: "invalid roles, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - ctx: context.Background(), - member: &domain.Member{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", - }, - UserID: "user1", - Roles: []string{"PROJECT_OWNER"}, + member: &ChangeProjectMember{ + ResourceOwner: "org1", + ProjectID: "project1", + UserID: "user1", + Roles: []string{"PROJECT_OWNER"}, }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsErrorInvalidArgument, @@ -352,10 +409,10 @@ func TestCommandSide_ChangeProjectMember(t *testing.T) { { name: "member not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), zitadelRoles: []authz.RoleMapping{ { Role: domain.RoleProjectOwner, @@ -363,15 +420,12 @@ func TestCommandSide_ChangeProjectMember(t *testing.T) { }, }, args: args{ - ctx: context.Background(), - member: &domain.Member{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", - }, - UserID: "user1", - Roles: []string{"PROJECT_OWNER"}, + member: &ChangeProjectMember{ + ResourceOwner: "org1", + ProjectID: "project1", + UserID: "user1", + Roles: []string{"PROJECT_OWNER"}, }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsNotFound, @@ -380,8 +434,7 @@ func TestCommandSide_ChangeProjectMember(t *testing.T) { { name: "member not changed, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectMemberAddedEvent(context.Background(), @@ -392,6 +445,7 @@ func TestCommandSide_ChangeProjectMember(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), zitadelRoles: []authz.RoleMapping{ { Role: domain.RoleProjectOwner, @@ -399,25 +453,23 @@ func TestCommandSide_ChangeProjectMember(t *testing.T) { }, }, args: args{ - ctx: context.Background(), - member: &domain.Member{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", - }, - UserID: "user1", - Roles: []string{"PROJECT_OWNER"}, + member: &ChangeProjectMember{ + ResourceOwner: "org1", + ProjectID: "project1", + UserID: "user1", + Roles: []string{"PROJECT_OWNER"}, }, - resourceOwner: "org1", }, res: res{ - err: zerrors.IsPreconditionFailed, + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, }, }, { name: "member change, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectMemberAddedEvent(context.Background(), @@ -435,6 +487,7 @@ func TestCommandSide_ChangeProjectMember(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), zitadelRoles: []authz.RoleMapping{ { Role: domain.RoleProjectOwner, @@ -445,35 +498,64 @@ func TestCommandSide_ChangeProjectMember(t *testing.T) { }, }, args: args{ - ctx: context.Background(), - member: &domain.Member{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", - }, - UserID: "user1", - Roles: []string{"PROJECT_OWNER", "PROJECT_VIEWER"}, + member: &ChangeProjectMember{ + ResourceOwner: "org1", + ProjectID: "project1", + UserID: "user1", + Roles: []string{"PROJECT_OWNER", "PROJECT_VIEWER"}, }, - resourceOwner: "org1", }, res: res{ - want: &domain.Member{ - ObjectRoot: models.ObjectRoot{ - ResourceOwner: "org1", - AggregateID: "project1", - }, - UserID: "user1", - Roles: []string{domain.RoleProjectOwner, "PROJECT_VIEWER"}, + want: &domain.ObjectDetails{ + ResourceOwner: "org1", }, }, }, + { + name: "member change, no permission", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewProjectMemberAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "user1", + []string{"PROJECT_OWNER"}..., + ), + ), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + zitadelRoles: []authz.RoleMapping{ + { + Role: domain.RoleProjectOwner, + }, + { + Role: "PROJECT_VIEWER", + }, + }, + }, + args: args{ + member: &ChangeProjectMember{ + ResourceOwner: "org1", + ProjectID: "project1", + UserID: "user1", + Roles: []string{"PROJECT_OWNER", "PROJECT_VIEWER"}, + }, + }, + res: res{ + err: zerrors.IsPermissionDenied, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, - zitadelRoles: tt.fields.zitadelRoles, + eventstore: tt.fields.eventstore(t), + zitadelRoles: tt.fields.zitadelRoles, + checkPermission: tt.fields.checkPermission, } - got, err := r.ChangeProjectMember(tt.args.ctx, tt.args.member, tt.args.resourceOwner) + got, err := r.ChangeProjectMember(context.Background(), tt.args.member) if tt.res.err == nil { assert.NoError(t, err) } @@ -481,7 +563,7 @@ func TestCommandSide_ChangeProjectMember(t *testing.T) { t.Errorf("got wrong err: %v ", err) } if tt.res.err == nil { - assert.Equal(t, tt.res.want, got) + assertObjectDetails(t, tt.res.want, got) } }) } @@ -489,10 +571,10 @@ func TestCommandSide_ChangeProjectMember(t *testing.T) { func TestCommandSide_RemoveProjectMember(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck } type args struct { - ctx context.Context projectID string userID string resourceOwner string @@ -510,12 +592,10 @@ func TestCommandSide_RemoveProjectMember(t *testing.T) { { name: "invalid member projectid missing, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - ctx: context.Background(), projectID: "", userID: "user1", resourceOwner: "org1", @@ -527,12 +607,10 @@ func TestCommandSide_RemoveProjectMember(t *testing.T) { { name: "invalid member userid missing, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - ctx: context.Background(), projectID: "project1", userID: "", resourceOwner: "org1", @@ -544,26 +622,26 @@ func TestCommandSide_RemoveProjectMember(t *testing.T) { { name: "member not existing, empty object details result", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - ctx: context.Background(), projectID: "project1", userID: "user1", resourceOwner: "org1", }, res: res{ - want: &domain.ObjectDetails{}, + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, }, }, { name: "member remove, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectMemberAddedEvent(context.Background(), @@ -580,9 +658,9 @@ func TestCommandSide_RemoveProjectMember(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - ctx: context.Background(), projectID: "project1", userID: "user1", resourceOwner: "org1", @@ -593,13 +671,39 @@ func TestCommandSide_RemoveProjectMember(t *testing.T) { }, }, }, + { + name: "member remove, no permission", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewProjectMemberAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "user1", + []string{"PROJECT_OWNER"}..., + ), + ), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args: args{ + projectID: "project1", + userID: "user1", + resourceOwner: "org1", + }, + res: res{ + err: zerrors.IsPermissionDenied, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, } - got, err := r.RemoveProjectMember(tt.args.ctx, tt.args.projectID, tt.args.userID, tt.args.resourceOwner) + got, err := r.RemoveProjectMember(context.Background(), tt.args.projectID, tt.args.userID, tt.args.resourceOwner) if tt.res.err == nil { assert.NoError(t, err) } diff --git a/internal/command/project_model.go b/internal/command/project_model.go index a46b07a8fe..4c9496b3ad 100644 --- a/internal/command/project_model.go +++ b/internal/command/project_model.go @@ -2,6 +2,7 @@ package command import ( "context" + "slices" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" @@ -88,55 +89,45 @@ func (wm *ProjectWriteModel) Query() *eventstore.SearchQueryBuilder { func (wm *ProjectWriteModel) NewChangedEvent( ctx context.Context, aggregate *eventstore.Aggregate, - name string, + name *string, projectRoleAssertion, projectRoleCheck, - hasProjectCheck bool, - privateLabelingSetting domain.PrivateLabelingSetting, -) (*project.ProjectChangeEvent, bool, error) { + hasProjectCheck *bool, + privateLabelingSetting *domain.PrivateLabelingSetting, +) *project.ProjectChangeEvent { changes := make([]project.ProjectChanges, 0) - var err error oldName := "" - if wm.Name != name { + if name != nil && wm.Name != *name { oldName = wm.Name - changes = append(changes, project.ChangeName(name)) + changes = append(changes, project.ChangeName(*name)) } - if wm.ProjectRoleAssertion != projectRoleAssertion { - changes = append(changes, project.ChangeProjectRoleAssertion(projectRoleAssertion)) + if projectRoleAssertion != nil && wm.ProjectRoleAssertion != *projectRoleAssertion { + changes = append(changes, project.ChangeProjectRoleAssertion(*projectRoleAssertion)) } - if wm.ProjectRoleCheck != projectRoleCheck { - changes = append(changes, project.ChangeProjectRoleCheck(projectRoleCheck)) + if projectRoleCheck != nil && wm.ProjectRoleCheck != *projectRoleCheck { + changes = append(changes, project.ChangeProjectRoleCheck(*projectRoleCheck)) } - if wm.HasProjectCheck != hasProjectCheck { - changes = append(changes, project.ChangeHasProjectCheck(hasProjectCheck)) + if hasProjectCheck != nil && wm.HasProjectCheck != *hasProjectCheck { + changes = append(changes, project.ChangeHasProjectCheck(*hasProjectCheck)) } - if wm.PrivateLabelingSetting != privateLabelingSetting { - changes = append(changes, project.ChangePrivateLabelingSetting(privateLabelingSetting)) + if privateLabelingSetting != nil && wm.PrivateLabelingSetting != *privateLabelingSetting { + changes = append(changes, project.ChangePrivateLabelingSetting(*privateLabelingSetting)) } if len(changes) == 0 { - return nil, false, nil + return nil } - changeEvent, err := project.NewProjectChangeEvent(ctx, aggregate, oldName, changes) - if err != nil { - return nil, false, err - } - return changeEvent, true, nil + return project.NewProjectChangeEvent(ctx, aggregate, oldName, changes) } func isProjectStateExists(state domain.ProjectState) bool { - return !hasProjectState(state, domain.ProjectStateRemoved, domain.ProjectStateUnspecified) + return !slices.Contains([]domain.ProjectState{domain.ProjectStateRemoved, domain.ProjectStateUnspecified}, state) } func ProjectAggregateFromWriteModel(wm *eventstore.WriteModel) *eventstore.Aggregate { return eventstore.AggregateFromWriteModel(wm, project.AggregateType, project.AggregateVersion) } -func hasProjectState(check domain.ProjectState, states ...domain.ProjectState) bool { - for _, state := range states { - if check == state { - return true - } - } - return false +func ProjectAggregateFromWriteModelWithCTX(ctx context.Context, wm *eventstore.WriteModel) *eventstore.Aggregate { + return project.AggregateFromWriteModel(ctx, wm) } diff --git a/internal/command/project_old.go b/internal/command/project_old.go index 35ea9b3ebb..99d7dd2e34 100644 --- a/internal/command/project_old.go +++ b/internal/command/project_old.go @@ -9,18 +9,18 @@ import ( "github.com/zitadel/zitadel/internal/zerrors" ) -func (c *Commands) checkProjectExistsOld(ctx context.Context, projectID, resourceOwner string) (err error) { +func (c *Commands) checkProjectExistsOld(ctx context.Context, projectID, resourceOwner string) (_ string, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() projectWriteModel, err := c.getProjectWriteModelByID(ctx, projectID, resourceOwner) if err != nil { - return err + return "", err } if !isProjectStateExists(projectWriteModel.State) { - return zerrors.ThrowPreconditionFailed(nil, "COMMAND-EbFMN", "Errors.Project.NotFound") + return "", zerrors.ThrowPreconditionFailed(nil, "COMMAND-EbFMN", "Errors.Project.NotFound") } - return nil + return projectWriteModel.ResourceOwner, nil } func (c *Commands) deactivateProjectOld(ctx context.Context, projectID string, resourceOwner string) (*domain.ObjectDetails, error) { @@ -34,6 +34,9 @@ func (c *Commands) deactivateProjectOld(ctx context.Context, projectID string, r if existingProject.State != domain.ProjectStateActive { return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-mki55", "Errors.Project.NotActive") } + if err := c.checkPermissionUpdateProject(ctx, existingProject.ResourceOwner, existingProject.AggregateID); err != nil { + return nil, err + } //nolint: contextcheck projectAgg := ProjectAggregateFromWriteModel(&existingProject.WriteModel) @@ -59,6 +62,9 @@ func (c *Commands) reactivateProjectOld(ctx context.Context, projectID string, r if existingProject.State != domain.ProjectStateInactive { return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-5M9bs", "Errors.Project.NotInactive") } + if err := c.checkPermissionUpdateProject(ctx, existingProject.ResourceOwner, existingProject.AggregateID); err != nil { + return nil, err + } //nolint: contextcheck projectAgg := ProjectAggregateFromWriteModel(&existingProject.WriteModel) @@ -73,20 +79,20 @@ func (c *Commands) reactivateProjectOld(ctx context.Context, projectID string, r return writeModelToObjectDetails(&existingProject.WriteModel), nil } -func (c *Commands) checkProjectGrantPreConditionOld(ctx context.Context, projectGrant *domain.ProjectGrant, resourceOwner string) error { - preConditions := NewProjectGrantPreConditionReadModel(projectGrant.AggregateID, projectGrant.GrantedOrgID, resourceOwner) +func (c *Commands) checkProjectGrantPreConditionOld(ctx context.Context, projectID, grantedOrgID, resourceOwner string, roles []string) (string, error) { + preConditions := NewProjectGrantPreConditionReadModel(projectID, grantedOrgID, resourceOwner) err := c.eventstore.FilterToQueryReducer(ctx, preConditions) if err != nil { - return err + return "", err } if !preConditions.ProjectExists { - return zerrors.ThrowPreconditionFailed(err, "COMMAND-m9gsd", "Errors.Project.NotFound") + return "", zerrors.ThrowPreconditionFailed(err, "COMMAND-m9gsd", "Errors.Project.NotFound") } if !preConditions.GrantedOrgExists { - return zerrors.ThrowPreconditionFailed(err, "COMMAND-3m9gg", "Errors.Org.NotFound") + return "", zerrors.ThrowPreconditionFailed(err, "COMMAND-3m9gg", "Errors.Org.NotFound") } - if projectGrant.HasInvalidRoles(preConditions.ExistingRoleKeys) { - return zerrors.ThrowPreconditionFailed(err, "COMMAND-6m9gd", "Errors.Project.Role.NotFound") + if domain.HasInvalidRoles(preConditions.ExistingRoleKeys, roles) { + return "", zerrors.ThrowPreconditionFailed(err, "COMMAND-6m9gd", "Errors.Project.Role.NotFound") } - return nil + return preConditions.ProjectResourceOwner, nil } diff --git a/internal/command/project_role.go b/internal/command/project_role.go index 3c8f9c725c..60d002b967 100644 --- a/internal/command/project_role.go +++ b/internal/command/project_role.go @@ -7,22 +7,45 @@ import ( "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/eventstore/v1/models" "github.com/zitadel/zitadel/internal/repository/project" "github.com/zitadel/zitadel/internal/telemetry/tracing" "github.com/zitadel/zitadel/internal/zerrors" ) -func (c *Commands) AddProjectRole(ctx context.Context, projectRole *domain.ProjectRole, resourceOwner string) (_ *domain.ProjectRole, err error) { +type AddProjectRole struct { + models.ObjectRoot + + Key string + DisplayName string + Group string +} + +func (p *AddProjectRole) IsValid() bool { + return p.AggregateID != "" && p.Key != "" +} + +func (c *Commands) AddProjectRole(ctx context.Context, projectRole *AddProjectRole) (_ *domain.ObjectDetails, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - err = c.checkProjectExists(ctx, projectRole.AggregateID, resourceOwner) + projectResourceOwner, err := c.checkProjectExists(ctx, projectRole.AggregateID, projectRole.ResourceOwner) if err != nil { return nil, err } + if projectRole.ResourceOwner == "" { + projectRole.ResourceOwner = projectResourceOwner + } + if err := c.checkPermissionWriteProjectRole(ctx, projectRole.ResourceOwner, projectRole.Key); err != nil { + return nil, err + } - roleWriteModel := NewProjectRoleWriteModelWithKey(projectRole.Key, projectRole.AggregateID, resourceOwner) - projectAgg := ProjectAggregateFromWriteModel(&roleWriteModel.WriteModel) + roleWriteModel := NewProjectRoleWriteModelWithKey(projectRole.Key, projectRole.AggregateID, projectRole.ResourceOwner) + if roleWriteModel.ResourceOwner != projectResourceOwner { + return nil, zerrors.ThrowPreconditionFailed(nil, "PROJECT-RLB4UpqQSd", "Errors.Project.Role.Invalid") + } + + projectAgg := ProjectAggregateFromWriteModelWithCTX(ctx, &roleWriteModel.WriteModel) events, err := c.addProjectRoles(ctx, projectAgg, projectRole) if err != nil { return nil, err @@ -35,37 +58,45 @@ func (c *Commands) AddProjectRole(ctx context.Context, projectRole *domain.Proje if err != nil { return nil, err } - return roleWriteModelToRole(roleWriteModel), nil + return writeModelToObjectDetails(&roleWriteModel.WriteModel), nil } -func (c *Commands) BulkAddProjectRole(ctx context.Context, projectID, resourceOwner string, projectRoles []*domain.ProjectRole) (details *domain.ObjectDetails, err error) { - err = c.checkProjectExists(ctx, projectID, resourceOwner) +func (c *Commands) checkPermissionWriteProjectRole(ctx context.Context, resourceOwner, roleKey string) error { + return c.checkPermission(ctx, domain.PermissionProjectRoleWrite, resourceOwner, roleKey) +} + +func (c *Commands) BulkAddProjectRole(ctx context.Context, projectID, resourceOwner string, projectRoles []*AddProjectRole) (details *domain.ObjectDetails, err error) { + projectResourceOwner, err := c.checkProjectExists(ctx, projectID, resourceOwner) if err != nil { return nil, err } + for _, projectRole := range projectRoles { + if projectRole.ResourceOwner == "" { + projectRole.ResourceOwner = projectResourceOwner + } + if err := c.checkPermissionWriteProjectRole(ctx, projectRole.ResourceOwner, projectRole.Key); err != nil { + return nil, err + } + if projectRole.ResourceOwner != projectResourceOwner { + return nil, zerrors.ThrowPreconditionFailed(nil, "PROJECT-9ZXtdaJKJJ", "Errors.Project.Role.Invalid") + } + } roleWriteModel := NewProjectRoleWriteModel(projectID, resourceOwner) - projectAgg := ProjectAggregateFromWriteModel(&roleWriteModel.WriteModel) + projectAgg := ProjectAggregateFromWriteModelWithCTX(ctx, &roleWriteModel.WriteModel) events, err := c.addProjectRoles(ctx, projectAgg, projectRoles...) if err != nil { return details, err } - - pushedEvents, err := c.eventstore.Push(ctx, events...) - if err != nil { - return nil, err - } - err = AppendAndReduce(roleWriteModel, pushedEvents...) - if err != nil { - return nil, err - } - return writeModelToObjectDetails(&roleWriteModel.WriteModel), nil + return c.pushAppendAndReduceDetails(ctx, roleWriteModel, events...) } -func (c *Commands) addProjectRoles(ctx context.Context, projectAgg *eventstore.Aggregate, projectRoles ...*domain.ProjectRole) ([]eventstore.Command, error) { +func (c *Commands) addProjectRoles(ctx context.Context, projectAgg *eventstore.Aggregate, projectRoles ...*AddProjectRole) ([]eventstore.Command, error) { var events []eventstore.Command for _, projectRole := range projectRoles { - projectRole.AggregateID = projectAgg.ID + if projectRole.ResourceOwner != projectAgg.ResourceOwner { + return nil, zerrors.ThrowPreconditionFailed(nil, "PROJECT-4Q2WjlbHvc", "Errors.Project.Role.Invalid") + } if !projectRole.IsValid() { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-4m9vS", "Errors.Project.Role.Invalid") } @@ -81,42 +112,55 @@ func (c *Commands) addProjectRoles(ctx context.Context, projectAgg *eventstore.A return events, nil } -func (c *Commands) ChangeProjectRole(ctx context.Context, projectRole *domain.ProjectRole, resourceOwner string) (_ *domain.ProjectRole, err error) { +type ChangeProjectRole struct { + models.ObjectRoot + + Key string + DisplayName string + Group string +} + +func (p *ChangeProjectRole) IsValid() bool { + return p.AggregateID != "" && p.Key != "" +} + +func (c *Commands) ChangeProjectRole(ctx context.Context, projectRole *ChangeProjectRole) (_ *domain.ObjectDetails, err error) { if !projectRole.IsValid() { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-2ilfW", "Errors.Project.Invalid") } - err = c.checkProjectExists(ctx, projectRole.AggregateID, resourceOwner) + projectResourceOwner, err := c.checkProjectExists(ctx, projectRole.AggregateID, projectRole.ResourceOwner) if err != nil { return nil, err } + if projectRole.ResourceOwner == "" { + projectRole.ResourceOwner = projectResourceOwner + } + if err := c.checkPermissionWriteProjectRole(ctx, projectRole.ResourceOwner, projectRole.Key); err != nil { + return nil, err + } - existingRole, err := c.getProjectRoleWriteModelByID(ctx, projectRole.Key, projectRole.AggregateID, resourceOwner) + existingRole, err := c.getProjectRoleWriteModelByID(ctx, projectRole.Key, projectRole.AggregateID, projectRole.ResourceOwner) if err != nil { return nil, err } - if existingRole.State == domain.ProjectRoleStateUnspecified || existingRole.State == domain.ProjectRoleStateRemoved { + if !existingRole.State.Exists() { return nil, zerrors.ThrowNotFound(nil, "COMMAND-vv8M9", "Errors.Project.Role.NotExisting") } + if existingRole.ResourceOwner != projectResourceOwner { + return nil, zerrors.ThrowPreconditionFailed(nil, "PROJECT-3MizLWveMf", "Errors.Project.Role.Invalid") + } - projectAgg := ProjectAggregateFromWriteModel(&existingRole.WriteModel) + projectAgg := ProjectAggregateFromWriteModelWithCTX(ctx, &existingRole.WriteModel) changeEvent, changed, err := existingRole.NewProjectRoleChangedEvent(ctx, projectAgg, projectRole.Key, projectRole.DisplayName, projectRole.Group) if err != nil { return nil, err } if !changed { - return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-5M0cs", "Errors.NoChangesFound") + return writeModelToObjectDetails(&existingRole.WriteModel), nil } - pushedEvents, err := c.eventstore.Push(ctx, changeEvent) - if err != nil { - return nil, err - } - err = AppendAndReduce(existingRole, pushedEvents...) - if err != nil { - return nil, err - } - return roleWriteModelToRole(existingRole), nil + return c.pushAppendAndReduceDetails(ctx, existingRole, changeEvent) } func (c *Commands) RemoveProjectRole(ctx context.Context, projectID, key, resourceOwner string, cascadingProjectGrantIds []string, cascadeUserGrantIDs ...string) (details *domain.ObjectDetails, err error) { @@ -127,10 +171,14 @@ func (c *Commands) RemoveProjectRole(ctx context.Context, projectID, key, resour if err != nil { return details, err } - if existingRole.State == domain.ProjectRoleStateUnspecified || existingRole.State == domain.ProjectRoleStateRemoved { - return details, zerrors.ThrowNotFound(nil, "COMMAND-m9vMf", "Errors.Project.Role.NotExisting") + // return if project role is not existing + if !existingRole.State.Exists() { + return writeModelToObjectDetails(&existingRole.WriteModel), nil } - projectAgg := ProjectAggregateFromWriteModel(&existingRole.WriteModel) + if err := c.checkPermissionDeleteProjectRole(ctx, existingRole.ResourceOwner, existingRole.Key); err != nil { + return nil, err + } + projectAgg := ProjectAggregateFromWriteModelWithCTX(ctx, &existingRole.WriteModel) events := []eventstore.Command{ project.NewRoleRemovedEvent(ctx, projectAgg, key), } @@ -153,15 +201,11 @@ func (c *Commands) RemoveProjectRole(ctx context.Context, projectID, key, resour events = append(events, event) } - pushedEvents, err := c.eventstore.Push(ctx, events...) - if err != nil { - return nil, err - } - err = AppendAndReduce(existingRole, pushedEvents...) - if err != nil { - return nil, err - } - return writeModelToObjectDetails(&existingRole.WriteModel), nil + return c.pushAppendAndReduceDetails(ctx, existingRole, events...) +} + +func (c *Commands) checkPermissionDeleteProjectRole(ctx context.Context, resourceOwner, roleKey string) error { + return c.checkPermission(ctx, domain.PermissionProjectRoleDelete, resourceOwner, roleKey) } func (c *Commands) getProjectRoleWriteModelByID(ctx context.Context, key, projectID, resourceOwner string) (*ProjectRoleWriteModel, error) { diff --git a/internal/command/project_role_model.go b/internal/command/project_role_model.go index 641879f238..3fc9a6c814 100644 --- a/internal/command/project_role_model.go +++ b/internal/command/project_role_model.go @@ -17,6 +17,10 @@ type ProjectRoleWriteModel struct { State domain.ProjectRoleState } +func (wm *ProjectRoleWriteModel) GetWriteModel() *eventstore.WriteModel { + return &wm.WriteModel +} + func NewProjectRoleWriteModelWithKey(key, projectID, resourceOwner string) *ProjectRoleWriteModel { return &ProjectRoleWriteModel{ WriteModel: eventstore.WriteModel{ diff --git a/internal/command/project_role_test.go b/internal/command/project_role_test.go index 2dc7ee35a6..14db198270 100644 --- a/internal/command/project_role_test.go +++ b/internal/command/project_role_test.go @@ -16,15 +16,15 @@ import ( func TestCommandSide_AddProjectRole(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck } type args struct { - ctx context.Context - role *domain.ProjectRole - resourceOwner string + ctx context.Context + role *AddProjectRole } type res struct { - want *domain.ProjectRole + want *domain.ObjectDetails err func(error) bool } tests := []struct { @@ -36,8 +36,7 @@ func TestCommandSide_AddProjectRole(t *testing.T) { { name: "project not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -55,16 +54,16 @@ func TestCommandSide_AddProjectRole(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - role: &domain.ProjectRole{ + role: &AddProjectRole{ ObjectRoot: models.ObjectRoot{ AggregateID: "project1", }, Key: "key1", }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsPreconditionFailed, @@ -73,8 +72,7 @@ func TestCommandSide_AddProjectRole(t *testing.T) { { name: "invalid role, error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -85,15 +83,15 @@ func TestCommandSide_AddProjectRole(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - role: &domain.ProjectRole{ + role: &AddProjectRole{ ObjectRoot: models.ObjectRoot{ AggregateID: "project1", }, }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsErrorInvalidArgument, @@ -102,8 +100,7 @@ func TestCommandSide_AddProjectRole(t *testing.T) { { name: "role key already exists, already exists error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -123,10 +120,11 @@ func TestCommandSide_AddProjectRole(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - role: &domain.ProjectRole{ + role: &AddProjectRole{ ObjectRoot: models.ObjectRoot{ AggregateID: "project1", }, @@ -134,7 +132,6 @@ func TestCommandSide_AddProjectRole(t *testing.T) { DisplayName: "key", Group: "group", }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsErrorAlreadyExists, @@ -143,8 +140,7 @@ func TestCommandSide_AddProjectRole(t *testing.T) { { name: "add role,ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -164,10 +160,11 @@ func TestCommandSide_AddProjectRole(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - role: &domain.ProjectRole{ + role: &AddProjectRole{ ObjectRoot: models.ObjectRoot{ AggregateID: "project1", }, @@ -175,10 +172,41 @@ func TestCommandSide_AddProjectRole(t *testing.T) { DisplayName: "key", Group: "group", }, - resourceOwner: "org1", }, res: res{ - want: &domain.ProjectRole{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "add role, resourceowner, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", true, true, true, + domain.PrivateLabelingSettingUnspecified, + ), + ), + ), + expectPush( + project.NewRoleAddedEvent( + context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "key1", + "key", + "group", + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + role: &AddProjectRole{ ObjectRoot: models.ObjectRoot{ AggregateID: "project1", ResourceOwner: "org1", @@ -188,14 +216,20 @@ func TestCommandSide_AddProjectRole(t *testing.T) { Group: "group", }, }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, } - got, err := r.AddProjectRole(tt.args.ctx, tt.args.role, tt.args.resourceOwner) + got, err := r.AddProjectRole(tt.args.ctx, tt.args.role) if tt.res.err == nil { assert.NoError(t, err) } @@ -203,7 +237,7 @@ func TestCommandSide_AddProjectRole(t *testing.T) { t.Errorf("got wrong err: %v ", err) } if tt.res.err == nil { - assert.Equal(t, tt.res.want, got) + assertObjectDetails(t, tt.res.want, got) } }) } @@ -211,11 +245,12 @@ func TestCommandSide_AddProjectRole(t *testing.T) { func TestCommandSide_BulkAddProjectRole(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck } type args struct { ctx context.Context - roles []*domain.ProjectRole + roles []*AddProjectRole projectID string resourceOwner string } @@ -232,8 +267,7 @@ func TestCommandSide_BulkAddProjectRole(t *testing.T) { { name: "project not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -251,10 +285,11 @@ func TestCommandSide_BulkAddProjectRole(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - roles: []*domain.ProjectRole{ + roles: []*AddProjectRole{ { ObjectRoot: models.ObjectRoot{ AggregateID: "project1", @@ -271,8 +306,7 @@ func TestCommandSide_BulkAddProjectRole(t *testing.T) { { name: "invalid role, error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -283,10 +317,11 @@ func TestCommandSide_BulkAddProjectRole(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - roles: []*domain.ProjectRole{ + roles: []*AddProjectRole{ { ObjectRoot: models.ObjectRoot{}, }, @@ -304,8 +339,7 @@ func TestCommandSide_BulkAddProjectRole(t *testing.T) { { name: "role key already exists, already exists error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -332,16 +366,23 @@ func TestCommandSide_BulkAddProjectRole(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - roles: []*domain.ProjectRole{ + roles: []*AddProjectRole{ { + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, Key: "key1", DisplayName: "key", Group: "group", }, { + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, Key: "key2", DisplayName: "key2", Group: "group", @@ -357,8 +398,7 @@ func TestCommandSide_BulkAddProjectRole(t *testing.T) { { name: "add roles,ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -385,16 +425,23 @@ func TestCommandSide_BulkAddProjectRole(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - roles: []*domain.ProjectRole{ + roles: []*AddProjectRole{ { + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, Key: "key1", DisplayName: "key", Group: "group", }, { + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, Key: "key2", DisplayName: "key2", Group: "group", @@ -413,7 +460,8 @@ func TestCommandSide_BulkAddProjectRole(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, } got, err := r.BulkAddProjectRole(tt.args.ctx, tt.args.projectID, tt.args.resourceOwner, tt.args.roles) if tt.res.err == nil { @@ -431,15 +479,15 @@ func TestCommandSide_BulkAddProjectRole(t *testing.T) { func TestCommandSide_ChangeProjectRole(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck } type args struct { - ctx context.Context - role *domain.ProjectRole - resourceOwner string + ctx context.Context + role *ChangeProjectRole } type res struct { - want *domain.ProjectRole + want *domain.ObjectDetails err func(error) bool } tests := []struct { @@ -451,18 +499,16 @@ func TestCommandSide_ChangeProjectRole(t *testing.T) { { name: "invalid role, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - role: &domain.ProjectRole{ + role: &ChangeProjectRole{ ObjectRoot: models.ObjectRoot{ AggregateID: "project1", }, }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsErrorInvalidArgument, @@ -471,8 +517,7 @@ func TestCommandSide_ChangeProjectRole(t *testing.T) { { name: "project not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -490,16 +535,16 @@ func TestCommandSide_ChangeProjectRole(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - role: &domain.ProjectRole{ + role: &ChangeProjectRole{ ObjectRoot: models.ObjectRoot{ AggregateID: "project1", }, Key: "key1", }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsPreconditionFailed, @@ -508,8 +553,7 @@ func TestCommandSide_ChangeProjectRole(t *testing.T) { { name: "role removed, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -536,10 +580,11 @@ func TestCommandSide_ChangeProjectRole(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - role: &domain.ProjectRole{ + role: &ChangeProjectRole{ ObjectRoot: models.ObjectRoot{ AggregateID: "project1", }, @@ -547,7 +592,6 @@ func TestCommandSide_ChangeProjectRole(t *testing.T) { DisplayName: "key", Group: "group", }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsNotFound, @@ -556,8 +600,7 @@ func TestCommandSide_ChangeProjectRole(t *testing.T) { { name: "role not changed, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -578,10 +621,11 @@ func TestCommandSide_ChangeProjectRole(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - role: &domain.ProjectRole{ + role: &ChangeProjectRole{ ObjectRoot: models.ObjectRoot{ AggregateID: "project1", }, @@ -589,17 +633,17 @@ func TestCommandSide_ChangeProjectRole(t *testing.T) { DisplayName: "key", Group: "group", }, - resourceOwner: "org1", }, res: res{ - err: zerrors.IsPreconditionFailed, + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, }, }, { name: "role changed, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -623,10 +667,11 @@ func TestCommandSide_ChangeProjectRole(t *testing.T) { newRoleChangedEvent(context.Background(), "project1", "org1", "key1", "keychanged", "groupchanged"), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - role: &domain.ProjectRole{ + role: &ChangeProjectRole{ ObjectRoot: models.ObjectRoot{ AggregateID: "project1", }, @@ -634,17 +679,10 @@ func TestCommandSide_ChangeProjectRole(t *testing.T) { DisplayName: "keychanged", Group: "groupchanged", }, - resourceOwner: "org1", }, res: res{ - want: &domain.ProjectRole{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", - ResourceOwner: "org1", - }, - Key: "key1", - DisplayName: "keychanged", - Group: "groupchanged", + want: &domain.ObjectDetails{ + ResourceOwner: "org1", }, }, }, @@ -652,9 +690,10 @@ func TestCommandSide_ChangeProjectRole(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, } - got, err := r.ChangeProjectRole(tt.args.ctx, tt.args.role, tt.args.resourceOwner) + got, err := r.ChangeProjectRole(tt.args.ctx, tt.args.role) if tt.res.err == nil { assert.NoError(t, err) } @@ -662,7 +701,7 @@ func TestCommandSide_ChangeProjectRole(t *testing.T) { t.Errorf("got wrong err: %v ", err) } if tt.res.err == nil { - assert.Equal(t, tt.res.want, got) + assertObjectDetails(t, tt.res.want, got) } }) } @@ -670,7 +709,8 @@ func TestCommandSide_ChangeProjectRole(t *testing.T) { func TestCommandSide_RemoveProjectRole(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck } type args struct { ctx context.Context @@ -693,9 +733,8 @@ func TestCommandSide_RemoveProjectRole(t *testing.T) { { name: "invalid projectid, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -709,9 +748,8 @@ func TestCommandSide_RemoveProjectRole(t *testing.T) { { name: "invalid key, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -726,10 +764,10 @@ func TestCommandSide_RemoveProjectRole(t *testing.T) { { name: "role not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -738,14 +776,15 @@ func TestCommandSide_RemoveProjectRole(t *testing.T) { resourceOwner: "org1", }, res: res{ - err: zerrors.IsNotFound, + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, }, }, { name: "role removed, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewRoleAddedEvent(context.Background(), @@ -763,6 +802,7 @@ func TestCommandSide_RemoveProjectRole(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -771,14 +811,15 @@ func TestCommandSide_RemoveProjectRole(t *testing.T) { resourceOwner: "org1", }, res: res{ - err: zerrors.IsNotFound, + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, }, }, { name: "role removed, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewRoleAddedEvent(context.Background(), @@ -796,6 +837,7 @@ func TestCommandSide_RemoveProjectRole(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -812,8 +854,7 @@ func TestCommandSide_RemoveProjectRole(t *testing.T) { { name: "role removed with cascadingProjectGrantids, grant not found, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewRoleAddedEvent(context.Background(), @@ -832,6 +873,7 @@ func TestCommandSide_RemoveProjectRole(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -849,8 +891,7 @@ func TestCommandSide_RemoveProjectRole(t *testing.T) { { name: "role removed with cascadingProjectGrantids, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewRoleAddedEvent(context.Background(), @@ -883,6 +924,7 @@ func TestCommandSide_RemoveProjectRole(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -900,8 +942,7 @@ func TestCommandSide_RemoveProjectRole(t *testing.T) { { name: "role removed with cascadingUserGrantIDs, grant not found, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewRoleAddedEvent(context.Background(), @@ -920,6 +961,7 @@ func TestCommandSide_RemoveProjectRole(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -937,8 +979,7 @@ func TestCommandSide_RemoveProjectRole(t *testing.T) { { name: "role removed with cascadingUserGrantIDs, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewRoleAddedEvent(context.Background(), @@ -969,6 +1010,7 @@ func TestCommandSide_RemoveProjectRole(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -987,7 +1029,8 @@ func TestCommandSide_RemoveProjectRole(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, } got, err := r.RemoveProjectRole(tt.args.ctx, tt.args.projectID, tt.args.key, tt.args.resourceOwner, tt.args.cascadingProjectGrantIDs, tt.args.cascadingUserGrantIDs...) if tt.res.err == nil { diff --git a/internal/command/project_test.go b/internal/command/project_test.go index 842e1aa640..6c03420f6b 100644 --- a/internal/command/project_test.go +++ b/internal/command/project_test.go @@ -4,6 +4,7 @@ import ( "context" "testing" + "github.com/muhlemmer/gu" "github.com/stretchr/testify/assert" "github.com/zitadel/zitadel/internal/api/authz" @@ -18,16 +19,16 @@ import ( func TestCommandSide_AddProject(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore - idGenerator id.Generator + eventstore func(t *testing.T) *eventstore.Eventstore + idGenerator id.Generator + checkPermission domain.PermissionCheck } type args struct { - ctx context.Context - project *domain.Project - resourceOwner string + ctx context.Context + project *AddProject } type res struct { - want *domain.Project + want *domain.ObjectDetails err func(error) bool } tests := []struct { @@ -39,14 +40,14 @@ func TestCommandSide_AddProject(t *testing.T) { { name: "invalid project, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - ctx: authz.WithInstanceID(context.Background(), "instanceID"), - project: &domain.Project{}, - resourceOwner: "org1", + ctx: authz.WithInstanceID(context.Background(), "instanceID"), + project: &AddProject{ + ObjectRoot: models.ObjectRoot{ResourceOwner: "org1"}, + }, }, res: res{ err: zerrors.IsErrorInvalidArgument, @@ -55,62 +56,51 @@ func TestCommandSide_AddProject(t *testing.T) { { name: "project, resourceowner empty", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: authz.WithInstanceID(context.Background(), "instanceID"), - project: &domain.Project{ + project: &AddProject{ + ObjectRoot: models.ObjectRoot{ResourceOwner: ""}, Name: "project", ProjectRoleAssertion: true, ProjectRoleCheck: true, HasProjectCheck: true, PrivateLabelingSetting: domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy, }, - resourceOwner: "", }, res: res{ err: zerrors.IsErrorInvalidArgument, }, }, { - name: "project, error already exists", + name: "project, no permission", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), - expectPushFailed(zerrors.ThrowAlreadyExists(nil, "ERROR", "internl"), - project.NewProjectAddedEvent( - context.Background(), - &project.NewAggregate("project1", "org1").Aggregate, - "project", true, true, true, - domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy, - ), - ), ), - idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "project1"), + checkPermission: newMockPermissionCheckNotAllowed(), }, args: args{ ctx: authz.WithInstanceID(context.Background(), "instanceID"), - project: &domain.Project{ + project: &AddProject{ + ObjectRoot: models.ObjectRoot{AggregateID: "project1", ResourceOwner: "org1"}, Name: "project", ProjectRoleAssertion: true, ProjectRoleCheck: true, HasProjectCheck: true, PrivateLabelingSetting: domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy, }, - resourceOwner: "org1", }, res: res{ - err: zerrors.IsErrorAlreadyExists, + err: zerrors.IsPermissionDenied, }, }, { name: "project, already exists", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -120,18 +110,19 @@ func TestCommandSide_AddProject(t *testing.T) { ), ), ), - idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "project1"), + checkPermission: newMockPermissionCheckAllowed(), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "project1"), }, args: args{ ctx: authz.WithInstanceID(context.Background(), "instanceID"), - project: &domain.Project{ + project: &AddProject{ + ObjectRoot: models.ObjectRoot{ResourceOwner: "org1"}, Name: "project", ProjectRoleAssertion: true, ProjectRoleCheck: true, HasProjectCheck: true, PrivateLabelingSetting: domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy, }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsErrorAlreadyExists, @@ -140,8 +131,7 @@ func TestCommandSide_AddProject(t *testing.T) { { name: "project, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), expectPush( project.NewProjectAddedEvent( @@ -152,25 +142,46 @@ func TestCommandSide_AddProject(t *testing.T) { ), ), ), - idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "project1"), + checkPermission: newMockPermissionCheckAllowed(), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "project1"), }, args: args{ ctx: authz.WithInstanceID(context.Background(), "instanceID"), - project: &domain.Project{ + project: &AddProject{ + ObjectRoot: models.ObjectRoot{ResourceOwner: "org1"}, Name: "project", ProjectRoleAssertion: true, ProjectRoleCheck: true, HasProjectCheck: true, PrivateLabelingSetting: domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy, }, - resourceOwner: "org1", }, res: res{ - want: &domain.Project{ - ObjectRoot: models.ObjectRoot{ - ResourceOwner: "org1", - AggregateID: "project1", - }, + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "project, with id, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter(), + expectPush( + project.NewProjectAddedEvent( + context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project", true, true, true, + domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy, + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instanceID"), + project: &AddProject{ + ObjectRoot: models.ObjectRoot{AggregateID: "project1", ResourceOwner: "org1"}, Name: "project", ProjectRoleAssertion: true, ProjectRoleCheck: true, @@ -178,16 +189,22 @@ func TestCommandSide_AddProject(t *testing.T) { PrivateLabelingSetting: domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy, }, }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &Commands{ - eventstore: tt.fields.eventstore, - idGenerator: tt.fields.idGenerator, + eventstore: tt.fields.eventstore(t), + idGenerator: tt.fields.idGenerator, + checkPermission: tt.fields.checkPermission, } c.setMilestonesCompletedForTest("instanceID") - got, err := c.AddProject(tt.args.ctx, tt.args.project, tt.args.resourceOwner) + got, err := c.AddProject(tt.args.ctx, tt.args.project) if tt.res.err == nil { assert.NoError(t, err) } @@ -195,7 +212,8 @@ func TestCommandSide_AddProject(t *testing.T) { t.Errorf("got wrong err: %v ", err) } if tt.res.err == nil { - assert.Equal(t, tt.res.want, got) + assert.NotEmpty(t, got.ID) + assertObjectDetails(t, tt.res.want, got) } }) } @@ -203,15 +221,16 @@ func TestCommandSide_AddProject(t *testing.T) { func TestCommandSide_ChangeProject(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck } type args struct { ctx context.Context - project *domain.Project + project *ChangeProject resourceOwner string } type res struct { - want *domain.Project + want *domain.ObjectDetails err func(error) bool } tests := []struct { @@ -223,16 +242,16 @@ func TestCommandSide_ChangeProject(t *testing.T) { { name: "invalid project, invalid error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - project: &domain.Project{ + project: &ChangeProject{ ObjectRoot: models.ObjectRoot{ AggregateID: "project1", }, + Name: gu.Ptr(""), }, resourceOwner: "org1", }, @@ -243,14 +262,13 @@ func TestCommandSide_ChangeProject(t *testing.T) { { name: "invalid project empty aggregateid, invalid error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - project: &domain.Project{ - Name: "project", + project: &ChangeProject{ + Name: gu.Ptr("project"), }, resourceOwner: "org1", }, @@ -261,18 +279,18 @@ func TestCommandSide_ChangeProject(t *testing.T) { { name: "project not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - project: &domain.Project{ + project: &ChangeProject{ ObjectRoot: models.ObjectRoot{ AggregateID: "project1", }, - Name: "project change", + Name: gu.Ptr("project change"), }, resourceOwner: "org1", }, @@ -283,8 +301,7 @@ func TestCommandSide_ChangeProject(t *testing.T) { { name: "project removed, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -300,14 +317,15 @@ func TestCommandSide_ChangeProject(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - project: &domain.Project{ + project: &ChangeProject{ ObjectRoot: models.ObjectRoot{ AggregateID: "project1", }, - Name: "project change", + Name: gu.Ptr("project change"), }, resourceOwner: "org1", }, @@ -316,10 +334,9 @@ func TestCommandSide_ChangeProject(t *testing.T) { }, }, { - name: "no changes, precondition error", + name: "no changes, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -329,30 +346,65 @@ func TestCommandSide_ChangeProject(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - project: &domain.Project{ + project: &ChangeProject{ ObjectRoot: models.ObjectRoot{ AggregateID: "project1", }, - Name: "project", - ProjectRoleAssertion: true, - ProjectRoleCheck: true, - HasProjectCheck: true, - PrivateLabelingSetting: domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy, + Name: gu.Ptr("project"), + ProjectRoleAssertion: gu.Ptr(true), + ProjectRoleCheck: gu.Ptr(true), + HasProjectCheck: gu.Ptr(true), + PrivateLabelingSetting: gu.Ptr(domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy), }, resourceOwner: "org1", }, res: res{ - err: zerrors.IsPreconditionFailed, + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "no changes, no permission", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project", true, true, true, + domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy), + ), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args: args{ + ctx: context.Background(), + project: &ChangeProject{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + Name: gu.Ptr("project"), + ProjectRoleAssertion: gu.Ptr(true), + ProjectRoleCheck: gu.Ptr(true), + HasProjectCheck: gu.Ptr(true), + PrivateLabelingSetting: gu.Ptr(domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy), + }, + resourceOwner: "org1", + }, + res: res{ + err: zerrors.IsPermissionDenied, }, }, { name: "project change with name and unique constraints, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -374,40 +426,32 @@ func TestCommandSide_ChangeProject(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - project: &domain.Project{ + project: &ChangeProject{ ObjectRoot: models.ObjectRoot{ AggregateID: "project1", }, - Name: "project-new", - ProjectRoleAssertion: false, - ProjectRoleCheck: false, - HasProjectCheck: false, - PrivateLabelingSetting: domain.PrivateLabelingSettingEnforceProjectResourceOwnerPolicy, + Name: gu.Ptr("project-new"), + ProjectRoleAssertion: gu.Ptr(false), + ProjectRoleCheck: gu.Ptr(false), + HasProjectCheck: gu.Ptr(false), + PrivateLabelingSetting: gu.Ptr(domain.PrivateLabelingSettingEnforceProjectResourceOwnerPolicy), }, resourceOwner: "org1", }, res: res{ - want: &domain.Project{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", - ResourceOwner: "org1", - }, - Name: "project-new", - ProjectRoleAssertion: false, - ProjectRoleCheck: false, - HasProjectCheck: false, - PrivateLabelingSetting: domain.PrivateLabelingSettingEnforceProjectResourceOwnerPolicy, + want: &domain.ObjectDetails{ + ResourceOwner: "org1", }, }, }, { name: "project change without name and unique constraints, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -429,32 +473,25 @@ func TestCommandSide_ChangeProject(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), - project: &domain.Project{ + project: &ChangeProject{ ObjectRoot: models.ObjectRoot{ AggregateID: "project1", }, - Name: "project", - ProjectRoleAssertion: false, - ProjectRoleCheck: false, - HasProjectCheck: false, - PrivateLabelingSetting: domain.PrivateLabelingSettingEnforceProjectResourceOwnerPolicy, + Name: gu.Ptr("project"), + ProjectRoleAssertion: gu.Ptr(false), + ProjectRoleCheck: gu.Ptr(false), + HasProjectCheck: gu.Ptr(false), + PrivateLabelingSetting: gu.Ptr(domain.PrivateLabelingSettingEnforceProjectResourceOwnerPolicy), }, resourceOwner: "org1", }, res: res{ - want: &domain.Project{ - ObjectRoot: models.ObjectRoot{ - AggregateID: "project1", - ResourceOwner: "org1", - }, - Name: "project", - ProjectRoleAssertion: false, - ProjectRoleCheck: false, - HasProjectCheck: false, - PrivateLabelingSetting: domain.PrivateLabelingSettingEnforceProjectResourceOwnerPolicy, + want: &domain.ObjectDetails{ + ResourceOwner: "org1", }, }, }, @@ -462,9 +499,10 @@ func TestCommandSide_ChangeProject(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, } - got, err := r.ChangeProject(tt.args.ctx, tt.args.project, tt.args.resourceOwner) + got, err := r.ChangeProject(tt.args.ctx, tt.args.project) if tt.res.err == nil { assert.NoError(t, err) } @@ -472,7 +510,7 @@ func TestCommandSide_ChangeProject(t *testing.T) { t.Errorf("got wrong err: %v ", err) } if tt.res.err == nil { - assert.Equal(t, tt.res.want, got) + assertObjectDetails(t, tt.res.want, got) } }) } @@ -480,7 +518,8 @@ func TestCommandSide_ChangeProject(t *testing.T) { func TestCommandSide_DeactivateProject(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck } type args struct { ctx context.Context @@ -500,9 +539,8 @@ func TestCommandSide_DeactivateProject(t *testing.T) { { name: "invalid project id, invalid error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -513,29 +551,13 @@ func TestCommandSide_DeactivateProject(t *testing.T) { err: zerrors.IsErrorInvalidArgument, }, }, - { - name: "invalid resourceowner, invalid error", - fields: fields{ - eventstore: eventstoreExpect( - t, - ), - }, - args: args{ - ctx: context.Background(), - projectID: "project1", - resourceOwner: "", - }, - res: res{ - err: zerrors.IsErrorInvalidArgument, - }, - }, { name: "project not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -549,8 +571,7 @@ func TestCommandSide_DeactivateProject(t *testing.T) { { name: "project removed, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -566,6 +587,7 @@ func TestCommandSide_DeactivateProject(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -579,8 +601,7 @@ func TestCommandSide_DeactivateProject(t *testing.T) { { name: "project already inactive, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -594,6 +615,7 @@ func TestCommandSide_DeactivateProject(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -605,10 +627,9 @@ func TestCommandSide_DeactivateProject(t *testing.T) { }, }, { - name: "project deactivate, ok", + name: "project deactivate,no resourceOwner, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -622,6 +643,61 @@ func TestCommandSide_DeactivateProject(t *testing.T) { &project.NewAggregate("project1", "org1").Aggregate), ), ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + resourceOwner: "", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "project deactivate, no permission", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project", true, true, true, + domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy), + ), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + resourceOwner: "org1", + }, + res: res{ + err: zerrors.IsPermissionDenied, + }, + }, + { + name: "project deactivate, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project", true, true, true, + domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy), + ), + ), + expectPush( + project.NewProjectDeactivatedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -638,7 +714,8 @@ func TestCommandSide_DeactivateProject(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, } got, err := r.DeactivateProject(tt.args.ctx, tt.args.projectID, tt.args.resourceOwner) if tt.res.err == nil { @@ -656,7 +733,8 @@ func TestCommandSide_DeactivateProject(t *testing.T) { func TestCommandSide_ReactivateProject(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck } type args struct { ctx context.Context @@ -676,9 +754,8 @@ func TestCommandSide_ReactivateProject(t *testing.T) { { name: "invalid project id, invalid error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -689,29 +766,13 @@ func TestCommandSide_ReactivateProject(t *testing.T) { err: zerrors.IsErrorInvalidArgument, }, }, - { - name: "invalid resourceowner, invalid error", - fields: fields{ - eventstore: eventstoreExpect( - t, - ), - }, - args: args{ - ctx: context.Background(), - projectID: "project1", - resourceOwner: "", - }, - res: res{ - err: zerrors.IsErrorInvalidArgument, - }, - }, { name: "project not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -725,8 +786,7 @@ func TestCommandSide_ReactivateProject(t *testing.T) { { name: "project removed, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -742,6 +802,7 @@ func TestCommandSide_ReactivateProject(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -755,8 +816,7 @@ func TestCommandSide_ReactivateProject(t *testing.T) { { name: "project not inactive, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -766,6 +826,7 @@ func TestCommandSide_ReactivateProject(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -777,10 +838,9 @@ func TestCommandSide_ReactivateProject(t *testing.T) { }, }, { - name: "project reactivate, ok", + name: "project reactivate, no resourceOwner, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -798,6 +858,69 @@ func TestCommandSide_ReactivateProject(t *testing.T) { &project.NewAggregate("project1", "org1").Aggregate), ), ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + resourceOwner: "", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "project reactivate, no permission", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project", true, true, true, + domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy), + ), + eventFromEventPusher( + project.NewProjectDeactivatedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate), + ), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + resourceOwner: "org1", + }, + res: res{ + err: zerrors.IsPermissionDenied, + }, + }, + { + name: "project reactivate, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project", true, true, true, + domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy), + ), + eventFromEventPusher( + project.NewProjectDeactivatedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate), + ), + ), + expectPush( + project.NewProjectReactivatedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -814,7 +937,8 @@ func TestCommandSide_ReactivateProject(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, } got, err := r.ReactivateProject(tt.args.ctx, tt.args.projectID, tt.args.resourceOwner) if tt.res.err == nil { @@ -832,7 +956,8 @@ func TestCommandSide_ReactivateProject(t *testing.T) { func TestCommandSide_RemoveProject(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck } type args struct { ctx context.Context @@ -852,9 +977,8 @@ func TestCommandSide_RemoveProject(t *testing.T) { { name: "invalid project id, invalid error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -868,9 +992,8 @@ func TestCommandSide_RemoveProject(t *testing.T) { { name: "invalid resourceowner, invalid error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -884,10 +1007,10 @@ func TestCommandSide_RemoveProject(t *testing.T) { { name: "project not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -901,8 +1024,7 @@ func TestCommandSide_RemoveProject(t *testing.T) { { name: "project removed, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -918,6 +1040,7 @@ func TestCommandSide_RemoveProject(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -931,8 +1054,7 @@ func TestCommandSide_RemoveProject(t *testing.T) { { name: "project remove, without entityConstraints, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -950,6 +1072,7 @@ func TestCommandSide_RemoveProject(t *testing.T) { nil), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -965,8 +1088,7 @@ func TestCommandSide_RemoveProject(t *testing.T) { { name: "project remove, with entityConstraints, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -1003,6 +1125,7 @@ func TestCommandSide_RemoveProject(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -1018,8 +1141,7 @@ func TestCommandSide_RemoveProject(t *testing.T) { { name: "project remove, with multiple entityConstraints, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( project.NewProjectAddedEvent(context.Background(), @@ -1090,6 +1212,7 @@ func TestCommandSide_RemoveProject(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -1106,7 +1229,8 @@ func TestCommandSide_RemoveProject(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, } got, err := r.RemoveProject(tt.args.ctx, tt.args.projectID, tt.args.resourceOwner) if tt.res.err == nil { @@ -1122,6 +1246,328 @@ func TestCommandSide_RemoveProject(t *testing.T) { } } +func TestCommandSide_DeleteProject(t *testing.T) { + type fields struct { + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck + } + type args struct { + ctx context.Context + projectID string + resourceOwner string + } + type res struct { + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "invalid project id, invalid error", + fields: fields{ + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "", + resourceOwner: "org1", + }, + res: res{ + err: zerrors.IsErrorInvalidArgument, + }, + }, + { + name: "project not existing, not found error", + fields: fields{ + eventstore: expectEventstore( + expectFilter(), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + resourceOwner: "org1", + }, + res: res{ + err: nil, + }, + }, + { + name: "project removed, not found error", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project", true, true, true, + domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy), + ), + eventFromEventPusher( + project.NewProjectRemovedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project", + nil), + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + resourceOwner: "org1", + }, + res: res{ + err: nil, + }, + }, { + name: "project remove, no resourceOwner, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project", true, true, true, + domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy), + ), + ), + // no saml application events + expectFilter(), + expectPush( + project.NewProjectRemovedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project", + nil), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + resourceOwner: "", + }, + res: res{ + err: nil, + }, + }, + { + name: "project remove, no permission", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project", true, true, true, + domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy), + ), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + resourceOwner: "", + }, + res: res{ + err: zerrors.IsPermissionDenied, + }, + }, + { + name: "project remove, without entityConstraints, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project", true, true, true, + domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy), + ), + ), + // no saml application events + expectFilter(), + expectPush( + project.NewProjectRemovedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project", + nil), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + resourceOwner: "org1", + }, + res: res{ + err: nil, + }, + }, + { + name: "project remove, with entityConstraints, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project", true, true, true, + domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy), + ), + ), + expectFilter( + eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "app1", + "app", + )), + eventFromEventPusher( + project.NewSAMLConfigAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "app1", + "https://test.com/saml/metadata", + []byte("\n\n \n urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified\n \n \n \n"), + "http://localhost:8080/saml/metadata", + domain.LoginVersionUnspecified, + "", + ), + ), + ), + expectPush( + project.NewProjectRemovedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project", + []*eventstore.UniqueConstraint{ + project.NewRemoveSAMLConfigEntityIDUniqueConstraint("https://test.com/saml/metadata"), + }, + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + resourceOwner: "org1", + }, + res: res{ + err: nil, + }, + }, + { + name: "project remove, with multiple entityConstraints, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project", true, true, true, + domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy), + ), + ), + expectFilter( + eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "app1", + "app", + )), + eventFromEventPusher( + project.NewSAMLConfigAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "app1", + "https://test1.com/saml/metadata", + []byte("\n\n \n urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified\n \n \n \n"), + "", + domain.LoginVersionUnspecified, + "", + ), + ), + eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "app2", + "app", + )), + eventFromEventPusher( + project.NewSAMLConfigAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "app2", + "https://test2.com/saml/metadata", + []byte("\n\n \n urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified\n \n \n \n"), + "", + domain.LoginVersionUnspecified, + "", + ), + ), + eventFromEventPusher(project.NewApplicationAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "app3", + "app", + )), + eventFromEventPusher( + project.NewSAMLConfigAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "app3", + "https://test3.com/saml/metadata", + []byte("\n\n \n urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified\n \n \n \n"), + "", + domain.LoginVersionUnspecified, + "", + ), + ), + ), + expectPush( + project.NewProjectRemovedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "project", + []*eventstore.UniqueConstraint{ + project.NewRemoveSAMLConfigEntityIDUniqueConstraint("https://test1.com/saml/metadata"), + project.NewRemoveSAMLConfigEntityIDUniqueConstraint("https://test2.com/saml/metadata"), + project.NewRemoveSAMLConfigEntityIDUniqueConstraint("https://test3.com/saml/metadata"), + }, + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + resourceOwner: "org1", + }, + res: res{ + err: nil, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, + } + _, err := r.DeleteProject(tt.args.ctx, tt.args.projectID, tt.args.resourceOwner) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + }) + } +} + func newProjectChangedEvent(ctx context.Context, projectID, resourceOwner, oldName, newName string, roleAssertion, roleCheck, hasProjectCheck bool, privateLabelingSetting domain.PrivateLabelingSetting) *project.ProjectChangeEvent { changes := []project.ProjectChanges{ project.ChangeProjectRoleAssertion(roleAssertion), @@ -1132,12 +1578,11 @@ func newProjectChangedEvent(ctx context.Context, projectID, resourceOwner, oldNa if newName != "" { changes = append(changes, project.ChangeName(newName)) } - event, _ := project.NewProjectChangeEvent(ctx, + return project.NewProjectChangeEvent(ctx, &project.NewAggregate(projectID, resourceOwner).Aggregate, oldName, changes, ) - return event } func TestAddProject(t *testing.T) { diff --git a/internal/command/resource_ower_model.go b/internal/command/resource_owner_model.go similarity index 100% rename from internal/command/resource_ower_model.go rename to internal/command/resource_owner_model.go diff --git a/internal/command/saml_session.go b/internal/command/saml_session.go index 6e0c37af9e..6329f35c5f 100644 --- a/internal/command/saml_session.go +++ b/internal/command/saml_session.go @@ -80,7 +80,15 @@ func (c *Commands) CreateSAMLSessionFromSAMLRequest(ctx context.Context, samlReq return err } cmd.SetSAMLRequestSuccessful(ctx, samlReqModel.aggregate) + postCommit, err := cmd.SetMilestones(ctx) + if err != nil { + return err + } _, err = cmd.PushEvents(ctx) + if err != nil { + return err + } + postCommit(ctx) return err } diff --git a/internal/command/saml_session_test.go b/internal/command/saml_session_test.go index 4781381cc4..15445e9e5c 100644 --- a/internal/command/saml_session_test.go +++ b/internal/command/saml_session_test.go @@ -334,6 +334,7 @@ func TestCommands_CreateSAMLSessionFromSAMLRequest(t *testing.T) { idGenerator: tt.fields.idGenerator, keyAlgorithm: tt.fields.keyAlgorithm, } + c.setMilestonesCompletedForTest("instanceID") err := c.CreateSAMLSessionFromSAMLRequest(tt.args.ctx, tt.args.samlRequestID, tt.args.complianceCheck, tt.args.samlResponseID, tt.args.samlResponseLifetime) require.ErrorIs(t, err, tt.res.err) }) diff --git a/internal/command/system_features.go b/internal/command/system_features.go index b317ea93bb..c2d4f9f9e7 100644 --- a/internal/command/system_features.go +++ b/internal/command/system_features.go @@ -10,23 +10,19 @@ import ( ) type SystemFeatures struct { - LoginDefaultOrg *bool - TriggerIntrospectionProjections *bool - LegacyIntrospection *bool - TokenExchange *bool - UserSchema *bool - ImprovedPerformance []feature.ImprovedPerformanceType - OIDCSingleV1SessionTermination *bool - DisableUserTokenEvent *bool - EnableBackChannelLogout *bool - LoginV2 *feature.LoginV2 - PermissionCheckV2 *bool + LoginDefaultOrg *bool + TokenExchange *bool + UserSchema *bool + ImprovedPerformance []feature.ImprovedPerformanceType + OIDCSingleV1SessionTermination *bool + DisableUserTokenEvent *bool + EnableBackChannelLogout *bool + LoginV2 *feature.LoginV2 + PermissionCheckV2 *bool } func (m *SystemFeatures) isEmpty() bool { return m.LoginDefaultOrg == nil && - m.TriggerIntrospectionProjections == nil && - m.LegacyIntrospection == nil && m.UserSchema == nil && m.TokenExchange == nil && // nil check to allow unset improvements diff --git a/internal/command/system_features_model.go b/internal/command/system_features_model.go index 28e56f8bd4..212d00e6ce 100644 --- a/internal/command/system_features_model.go +++ b/internal/command/system_features_model.go @@ -60,8 +60,6 @@ func (m *SystemFeaturesWriteModel) Query() *eventstore.SearchQueryBuilder { EventTypes( feature_v2.SystemResetEventType, feature_v2.SystemLoginDefaultOrgEventType, - feature_v2.SystemTriggerIntrospectionProjectionsEventType, - feature_v2.SystemLegacyIntrospectionEventType, feature_v2.SystemUserSchemaEventType, feature_v2.SystemTokenExchangeEventType, feature_v2.SystemImprovedPerformanceEventType, @@ -85,12 +83,6 @@ func reduceSystemFeature(features *SystemFeatures, key feature.Key, value any) { case feature.KeyLoginDefaultOrg: v := value.(bool) features.LoginDefaultOrg = &v - case feature.KeyTriggerIntrospectionProjections: - v := value.(bool) - features.TriggerIntrospectionProjections = &v - case feature.KeyLegacyIntrospection: - v := value.(bool) - features.LegacyIntrospection = &v case feature.KeyUserSchema: v := value.(bool) features.UserSchema = &v @@ -120,8 +112,6 @@ func (wm *SystemFeaturesWriteModel) setCommands(ctx context.Context, f *SystemFe aggregate := feature_v2.NewAggregate(wm.AggregateID, wm.ResourceOwner) cmds := make([]eventstore.Command, 0, len(feature.KeyValues())-1) cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.LoginDefaultOrg, f.LoginDefaultOrg, feature_v2.SystemLoginDefaultOrgEventType) - cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.TriggerIntrospectionProjections, f.TriggerIntrospectionProjections, feature_v2.SystemTriggerIntrospectionProjectionsEventType) - cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.LegacyIntrospection, f.LegacyIntrospection, feature_v2.SystemLegacyIntrospectionEventType) cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.UserSchema, f.UserSchema, feature_v2.SystemUserSchemaEventType) cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.TokenExchange, f.TokenExchange, feature_v2.SystemTokenExchangeEventType) cmds = appendFeatureSliceUpdate(ctx, cmds, aggregate, wm.ImprovedPerformance, f.ImprovedPerformance, feature_v2.SystemImprovedPerformanceEventType) diff --git a/internal/command/system_features_test.go b/internal/command/system_features_test.go index b1b5207b8c..2defd23d5e 100644 --- a/internal/command/system_features_test.go +++ b/internal/command/system_features_test.go @@ -63,42 +63,6 @@ func TestCommands_SetSystemFeatures(t *testing.T) { ResourceOwner: "SYSTEM", }, }, - { - name: "set TriggerIntrospectionProjections", - eventstore: expectEventstore( - expectFilter(), - expectPush( - feature_v2.NewSetEvent[bool]( - context.Background(), aggregate, - feature_v2.SystemTriggerIntrospectionProjectionsEventType, true, - ), - ), - ), - args: args{context.Background(), &SystemFeatures{ - TriggerIntrospectionProjections: gu.Ptr(true), - }}, - want: &domain.ObjectDetails{ - ResourceOwner: "SYSTEM", - }, - }, - { - name: "set LegacyIntrospection", - eventstore: expectEventstore( - expectFilter(), - expectPush( - feature_v2.NewSetEvent[bool]( - context.Background(), aggregate, - feature_v2.SystemLegacyIntrospectionEventType, true, - ), - ), - ), - args: args{context.Background(), &SystemFeatures{ - LegacyIntrospection: gu.Ptr(true), - }}, - want: &domain.ObjectDetails{ - ResourceOwner: "SYSTEM", - }, - }, { name: "set UserSchema", eventstore: expectEventstore( @@ -124,12 +88,12 @@ func TestCommands_SetSystemFeatures(t *testing.T) { expectPushFailed(io.ErrClosedPipe, feature_v2.NewSetEvent[bool]( context.Background(), aggregate, - feature_v2.SystemLegacyIntrospectionEventType, true, + feature_v2.SystemEnableBackChannelLogout, true, ), ), ), args: args{context.Background(), &SystemFeatures{ - LegacyIntrospection: gu.Ptr(true), + EnableBackChannelLogout: gu.Ptr(true), }}, wantErr: io.ErrClosedPipe, }, @@ -142,14 +106,6 @@ func TestCommands_SetSystemFeatures(t *testing.T) { context.Background(), aggregate, feature_v2.SystemLoginDefaultOrgEventType, true, ), - feature_v2.NewSetEvent[bool]( - context.Background(), aggregate, - feature_v2.SystemTriggerIntrospectionProjectionsEventType, false, - ), - feature_v2.NewSetEvent[bool]( - context.Background(), aggregate, - feature_v2.SystemLegacyIntrospectionEventType, true, - ), feature_v2.NewSetEvent[bool]( context.Background(), aggregate, feature_v2.SystemUserSchemaEventType, true, @@ -161,11 +117,9 @@ func TestCommands_SetSystemFeatures(t *testing.T) { ), ), args: args{context.Background(), &SystemFeatures{ - LoginDefaultOrg: gu.Ptr(true), - TriggerIntrospectionProjections: gu.Ptr(false), - LegacyIntrospection: gu.Ptr(true), - UserSchema: gu.Ptr(true), - OIDCSingleV1SessionTermination: gu.Ptr(true), + LoginDefaultOrg: gu.Ptr(true), + UserSchema: gu.Ptr(true), + OIDCSingleV1SessionTermination: gu.Ptr(true), }}, want: &domain.ObjectDetails{ ResourceOwner: "SYSTEM", @@ -180,10 +134,6 @@ func TestCommands_SetSystemFeatures(t *testing.T) { context.Background(), aggregate, feature_v2.SystemLoginDefaultOrgEventType, true, )), - eventFromEventPusher(feature_v2.NewSetEvent[bool]( - context.Background(), aggregate, - feature_v2.SystemTriggerIntrospectionProjectionsEventType, false, - )), eventFromEventPusher(feature_v2.NewResetEvent( context.Background(), aggregate, feature_v2.SystemResetEventType, @@ -192,20 +142,12 @@ func TestCommands_SetSystemFeatures(t *testing.T) { context.Background(), aggregate, feature_v2.SystemLoginDefaultOrgEventType, false, )), - eventFromEventPusher(feature_v2.NewSetEvent[bool]( - context.Background(), aggregate, - feature_v2.SystemLegacyIntrospectionEventType, true, - )), ), expectPush( feature_v2.NewSetEvent[bool]( context.Background(), aggregate, feature_v2.SystemLoginDefaultOrgEventType, true, ), - feature_v2.NewSetEvent[bool]( - context.Background(), aggregate, - feature_v2.SystemTriggerIntrospectionProjectionsEventType, false, - ), feature_v2.NewSetEvent[bool]( context.Background(), aggregate, feature_v2.SystemUserSchemaEventType, true, @@ -217,11 +159,9 @@ func TestCommands_SetSystemFeatures(t *testing.T) { ), ), args: args{context.Background(), &SystemFeatures{ - LoginDefaultOrg: gu.Ptr(true), - TriggerIntrospectionProjections: gu.Ptr(false), - LegacyIntrospection: gu.Ptr(true), - UserSchema: gu.Ptr(true), - OIDCSingleV1SessionTermination: gu.Ptr(false), + LoginDefaultOrg: gu.Ptr(true), + UserSchema: gu.Ptr(true), + OIDCSingleV1SessionTermination: gu.Ptr(false), }}, want: &domain.ObjectDetails{ ResourceOwner: "SYSTEM", diff --git a/internal/command/user.go b/internal/command/user.go index 6b65aa83ec..d834169f8a 100644 --- a/internal/command/user.go +++ b/internal/command/user.go @@ -194,7 +194,7 @@ func (c *Commands) RemoveUser(ctx context.Context, userID, resourceOwner string, events = append(events, user.NewUserRemovedEvent(ctx, userAgg, existingUser.UserName, existingUser.IDPLinks, domainPolicy.UserLoginMustBeDomain)) for _, grantID := range cascadingGrantIDs { - removeEvent, _, err := c.removeUserGrant(ctx, grantID, "", true) + removeEvent, _, err := c.removeUserGrant(ctx, grantID, "", true, false, nil) if err != nil { logging.WithFields("usergrantid", grantID).WithError(err).Warn("could not cascade remove role on user grant") continue @@ -327,18 +327,18 @@ func (c *Commands) UserDomainClaimedSent(ctx context.Context, orgID, userID stri return err } -func (c *Commands) checkUserExists(ctx context.Context, userID, resourceOwner string) (err error) { +func (c *Commands) checkUserExists(ctx context.Context, userID, resourceOwner string) (_ string, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() existingUser, err := c.userWriteModelByID(ctx, userID, resourceOwner) if err != nil { - return err + return "", err } if !isUserStateExists(existingUser.UserState) { - return zerrors.ThrowPreconditionFailed(nil, "COMMAND-uXHNj", "Errors.User.NotFound") + return "", zerrors.ThrowPreconditionFailed(nil, "COMMAND-uXHNj", "Errors.User.NotFound") } - return nil + return existingUser.ResourceOwner, nil } func (c *Commands) userWriteModelByID(ctx context.Context, userID, resourceOwner string) (writeModel *UserWriteModel, err error) { @@ -353,21 +353,27 @@ func (c *Commands) userWriteModelByID(ctx context.Context, userID, resourceOwner return writeModel, nil } -func ExistsUser(ctx context.Context, filter preparation.FilterToQueryReducer, id, resourceOwner string) (exists bool, err error) { +func ExistsUser(ctx context.Context, filter preparation.FilterToQueryReducer, id, resourceOwner string, machineOnly bool) (exists bool, err error) { + eventTypes := []eventstore.EventType{ + user.MachineAddedEventType, + user.UserRemovedType, + } + if !machineOnly { + eventTypes = append(eventTypes, + user.HumanRegisteredType, + user.UserV1RegisteredType, + user.HumanAddedType, + user.UserV1AddedType, + ) + } events, err := filter(ctx, eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). ResourceOwner(resourceOwner). OrderAsc(). AddQuery(). AggregateTypes(user.AggregateType). AggregateIDs(id). - EventTypes( - user.HumanRegisteredType, - user.UserV1RegisteredType, - user.HumanAddedType, - user.UserV1AddedType, - user.MachineAddedEventType, - user.UserRemovedType, - ).Builder()) + EventTypes(eventTypes...). + Builder()) if err != nil { return false, err } diff --git a/internal/command/user_grant.go b/internal/command/user_grant.go index 6bb4a20b0a..2b0ff4cf18 100644 --- a/internal/command/user_grant.go +++ b/internal/command/user_grant.go @@ -2,7 +2,7 @@ package command import ( "context" - "reflect" + "slices" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/domain" @@ -15,11 +15,14 @@ import ( "github.com/zitadel/zitadel/internal/zerrors" ) -func (c *Commands) AddUserGrant(ctx context.Context, usergrant *domain.UserGrant, resourceOwner string) (_ *domain.UserGrant, err error) { +// AddUserGrant authorizes a user for a project with the given role keys. +// The project must be owned by or granted to the resourceOwner. +// If the resourceOwner is nil, the project must be owned by the project that belongs to usergrant.ProjectID. +func (c *Commands) AddUserGrant(ctx context.Context, usergrant *domain.UserGrant, check UserGrantPermissionCheck) (_ *domain.UserGrant, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - event, addedUserGrant, err := c.addUserGrant(ctx, usergrant, resourceOwner) + event, addedUserGrant, err := c.addUserGrant(ctx, usergrant, check) if err != nil { return nil, err } @@ -35,11 +38,11 @@ func (c *Commands) AddUserGrant(ctx context.Context, usergrant *domain.UserGrant return userGrantWriteModelToUserGrant(addedUserGrant), nil } -func (c *Commands) addUserGrant(ctx context.Context, userGrant *domain.UserGrant, resourceOwner string) (command eventstore.Command, _ *UserGrantWriteModel, err error) { +func (c *Commands) addUserGrant(ctx context.Context, userGrant *domain.UserGrant, check UserGrantPermissionCheck) (command eventstore.Command, _ *UserGrantWriteModel, err error) { if !userGrant.IsValid() { return nil, nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-kVfMa", "Errors.UserGrant.Invalid") } - err = c.checkUserGrantPreCondition(ctx, userGrant, resourceOwner) + err = c.checkUserGrantPreCondition(ctx, userGrant, check) if err != nil { return nil, nil, err } @@ -48,7 +51,7 @@ func (c *Commands) addUserGrant(ctx context.Context, userGrant *domain.UserGrant return nil, nil, err } - addedUserGrant := NewUserGrantWriteModel(userGrant.AggregateID, resourceOwner) + addedUserGrant := NewUserGrantWriteModel(userGrant.AggregateID, userGrant.ResourceOwner) userGrantAgg := UserGrantAggregateFromWriteModel(&addedUserGrant.WriteModel) command = usergrant.NewUserGrantAddedEvent( ctx, @@ -61,54 +64,51 @@ func (c *Commands) addUserGrant(ctx context.Context, userGrant *domain.UserGrant return command, addedUserGrant, nil } -func (c *Commands) ChangeUserGrant(ctx context.Context, userGrant *domain.UserGrant, resourceOwner string) (_ *domain.UserGrant, err error) { - event, changedUserGrant, err := c.changeUserGrant(ctx, userGrant, resourceOwner, false) +func (c *Commands) ChangeUserGrant(ctx context.Context, userGrant *domain.UserGrant, cascade, ignoreUnchanged bool, check UserGrantPermissionCheck) (_ *domain.UserGrant, err error) { + if userGrant.AggregateID == "" { + return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-3M0sd", "Errors.UserGrant.Invalid") + } + existingUserGrant, err := c.userGrantWriteModelByID(ctx, userGrant.AggregateID, "") if err != nil { return nil, err } + if existingUserGrant.State == domain.UserGrantStateUnspecified || existingUserGrant.State == domain.UserGrantStateRemoved { + return nil, zerrors.ThrowNotFound(nil, "COMMAND-3M9sd", "Errors.UserGrant.NotFound") + } + + grantUnchanged := slices.Equal(existingUserGrant.RoleKeys, userGrant.RoleKeys) + if grantUnchanged { + if ignoreUnchanged { + return userGrantWriteModelToUserGrant(existingUserGrant), nil + } + return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-Rs8fy", "Errors.UserGrant.NotChanged") + } + userGrant.UserID = existingUserGrant.UserID + userGrant.ProjectID = existingUserGrant.ProjectID + userGrant.ProjectGrantID = existingUserGrant.ProjectGrantID + userGrant.ResourceOwner = existingUserGrant.ResourceOwner + + err = c.checkUserGrantPreCondition(ctx, userGrant, check) + if err != nil { + return nil, err + } + + changedUserGrant := NewUserGrantWriteModel(userGrant.AggregateID, userGrant.ResourceOwner) + userGrantAgg := UserGrantAggregateFromWriteModel(&changedUserGrant.WriteModel) + + var event eventstore.Command = usergrant.NewUserGrantChangedEvent(ctx, userGrantAgg, existingUserGrant.UserID, userGrant.RoleKeys) + if cascade { + event = usergrant.NewUserGrantCascadeChangedEvent(ctx, userGrantAgg, userGrant.RoleKeys) + } pushedEvents, err := c.eventstore.Push(ctx, event) if err != nil { return nil, err } - err = AppendAndReduce(changedUserGrant, pushedEvents...) + err = AppendAndReduce(existingUserGrant, pushedEvents...) if err != nil { return nil, err } - return userGrantWriteModelToUserGrant(changedUserGrant), nil -} - -func (c *Commands) changeUserGrant(ctx context.Context, userGrant *domain.UserGrant, resourceOwner string, cascade bool) (_ eventstore.Command, _ *UserGrantWriteModel, err error) { - if userGrant.AggregateID == "" { - return nil, nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-3M0sd", "Errors.UserGrant.Invalid") - } - existingUserGrant, err := c.userGrantWriteModelByID(ctx, userGrant.AggregateID, userGrant.ResourceOwner) - if err != nil { - return nil, nil, err - } - err = checkExplicitProjectPermission(ctx, existingUserGrant.ProjectGrantID, existingUserGrant.ProjectID) - if err != nil { - return nil, nil, err - } - if existingUserGrant.State == domain.UserGrantStateUnspecified || existingUserGrant.State == domain.UserGrantStateRemoved { - return nil, nil, zerrors.ThrowNotFound(nil, "COMMAND-3M9sd", "Errors.UserGrant.NotFound") - } - if reflect.DeepEqual(existingUserGrant.RoleKeys, userGrant.RoleKeys) { - return nil, nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-Rs8fy", "Errors.UserGrant.NotChanged") - } - userGrant.ProjectID = existingUserGrant.ProjectID - userGrant.ProjectGrantID = existingUserGrant.ProjectGrantID - err = c.checkUserGrantPreCondition(ctx, userGrant, resourceOwner) - if err != nil { - return nil, nil, err - } - - changedUserGrant := NewUserGrantWriteModel(userGrant.AggregateID, resourceOwner) - userGrantAgg := UserGrantAggregateFromWriteModel(&changedUserGrant.WriteModel) - - if cascade { - return usergrant.NewUserGrantCascadeChangedEvent(ctx, userGrantAgg, userGrant.RoleKeys), existingUserGrant, nil - } - return usergrant.NewUserGrantChangedEvent(ctx, userGrantAgg, existingUserGrant.UserID, userGrant.RoleKeys), existingUserGrant, nil + return userGrantWriteModelToUserGrant(existingUserGrant), nil } func (c *Commands) removeRoleFromUserGrant(ctx context.Context, userGrantID string, roleKeys []string, cascade bool) (_ eventstore.Command, err error) { @@ -144,8 +144,8 @@ func (c *Commands) removeRoleFromUserGrant(ctx context.Context, userGrantID stri return usergrant.NewUserGrantChangedEvent(ctx, userGrantAgg, existingUserGrant.UserID, existingUserGrant.RoleKeys), nil } -func (c *Commands) DeactivateUserGrant(ctx context.Context, grantID, resourceOwner string) (objectDetails *domain.ObjectDetails, err error) { - if grantID == "" || resourceOwner == "" { +func (c *Commands) DeactivateUserGrant(ctx context.Context, grantID string, resourceOwner string, check UserGrantPermissionCheck) (objectDetails *domain.ObjectDetails, err error) { + if grantID == "" { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-N2OhG", "Errors.UserGrant.IDMissing") } @@ -157,14 +157,17 @@ func (c *Commands) DeactivateUserGrant(ctx context.Context, grantID, resourceOwn return nil, zerrors.ThrowNotFound(nil, "COMMAND-3M9sd", "Errors.UserGrant.NotFound") } if existingUserGrant.State != domain.UserGrantStateActive { - return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-1S9gx", "Errors.UserGrant.NotActive") + return writeModelToObjectDetails(&existingUserGrant.WriteModel), nil + } + if check != nil { + err = check(existingUserGrant.ProjectID, existingUserGrant.ProjectGrantID)(existingUserGrant.ResourceOwner, "") + } else { + err = checkExplicitProjectPermission(ctx, existingUserGrant.ProjectGrantID, existingUserGrant.ProjectID) } - err = checkExplicitProjectPermission(ctx, existingUserGrant.ProjectGrantID, existingUserGrant.ProjectID) if err != nil { return nil, err } - - deactivateUserGrant := NewUserGrantWriteModel(grantID, resourceOwner) + deactivateUserGrant := NewUserGrantWriteModel(grantID, existingUserGrant.ResourceOwner) userGrantAgg := UserGrantAggregateFromWriteModel(&deactivateUserGrant.WriteModel) pushedEvents, err := c.eventstore.Push(ctx, usergrant.NewUserGrantDeactivatedEvent(ctx, userGrantAgg)) if err != nil { @@ -177,8 +180,8 @@ func (c *Commands) DeactivateUserGrant(ctx context.Context, grantID, resourceOwn return writeModelToObjectDetails(&existingUserGrant.WriteModel), nil } -func (c *Commands) ReactivateUserGrant(ctx context.Context, grantID, resourceOwner string) (objectDetails *domain.ObjectDetails, err error) { - if grantID == "" || resourceOwner == "" { +func (c *Commands) ReactivateUserGrant(ctx context.Context, grantID string, resourceOwner string, check UserGrantPermissionCheck) (objectDetails *domain.ObjectDetails, err error) { + if grantID == "" { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-Qxy8v", "Errors.UserGrant.IDMissing") } @@ -190,13 +193,17 @@ func (c *Commands) ReactivateUserGrant(ctx context.Context, grantID, resourceOwn return nil, zerrors.ThrowNotFound(nil, "COMMAND-Lp0gs", "Errors.UserGrant.NotFound") } if existingUserGrant.State != domain.UserGrantStateInactive { - return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-1ML0v", "Errors.UserGrant.NotInactive") + return writeModelToObjectDetails(&existingUserGrant.WriteModel), nil + } + if check != nil { + err = check(existingUserGrant.ProjectID, existingUserGrant.ProjectGrantID)(existingUserGrant.ResourceOwner, "") + } else { + err = checkExplicitProjectPermission(ctx, existingUserGrant.ProjectGrantID, existingUserGrant.ProjectID) } - err = checkExplicitProjectPermission(ctx, existingUserGrant.ProjectGrantID, existingUserGrant.ProjectID) if err != nil { return nil, err } - deactivateUserGrant := NewUserGrantWriteModel(grantID, resourceOwner) + deactivateUserGrant := NewUserGrantWriteModel(grantID, existingUserGrant.ResourceOwner) userGrantAgg := UserGrantAggregateFromWriteModel(&deactivateUserGrant.WriteModel) pushedEvents, err := c.eventstore.Push(ctx, usergrant.NewUserGrantReactivatedEvent(ctx, userGrantAgg)) if err != nil { @@ -209,12 +216,14 @@ func (c *Commands) ReactivateUserGrant(ctx context.Context, grantID, resourceOwn return writeModelToObjectDetails(&existingUserGrant.WriteModel), nil } -func (c *Commands) RemoveUserGrant(ctx context.Context, grantID, resourceOwner string) (objectDetails *domain.ObjectDetails, err error) { - event, existingUserGrant, err := c.removeUserGrant(ctx, grantID, resourceOwner, false) +func (c *Commands) RemoveUserGrant(ctx context.Context, grantID string, resourceOwner string, ignoreNotFound bool, check UserGrantPermissionCheck) (objectDetails *domain.ObjectDetails, err error) { + event, existingUserGrant, err := c.removeUserGrant(ctx, grantID, resourceOwner, false, ignoreNotFound, check) if err != nil { return nil, err } - + if event == nil { + return writeModelToObjectDetails(&existingUserGrant.WriteModel), nil + } pushedEvents, err := c.eventstore.Push(ctx, event) if err != nil { return nil, err @@ -232,7 +241,7 @@ func (c *Commands) BulkRemoveUserGrant(ctx context.Context, grantIDs []string, r } events := make([]eventstore.Command, len(grantIDs)) for i, grantID := range grantIDs { - event, _, err := c.removeUserGrant(ctx, grantID, resourceOwner, false) + event, _, err := c.removeUserGrant(ctx, grantID, resourceOwner, false, false, nil) if err != nil { return err } @@ -242,7 +251,7 @@ func (c *Commands) BulkRemoveUserGrant(ctx context.Context, grantIDs []string, r return err } -func (c *Commands) removeUserGrant(ctx context.Context, grantID, resourceOwner string, cascade bool) (_ eventstore.Command, writeModel *UserGrantWriteModel, err error) { +func (c *Commands) removeUserGrant(ctx context.Context, grantID string, resourceOwner string, cascade, ignoreNotFound bool, check UserGrantPermissionCheck) (_ eventstore.Command, writeModel *UserGrantWriteModel, err error) { if grantID == "" { return nil, nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-J9sc5", "Errors.UserGrant.IDMissing") } @@ -252,15 +261,22 @@ func (c *Commands) removeUserGrant(ctx context.Context, grantID, resourceOwner s return nil, nil, err } if existingUserGrant.State == domain.UserGrantStateUnspecified || existingUserGrant.State == domain.UserGrantStateRemoved { + if ignoreNotFound { + return nil, existingUserGrant, nil + } return nil, nil, zerrors.ThrowNotFound(nil, "COMMAND-1My0t", "Errors.UserGrant.NotFound") } - if !cascade { + if !cascade && check == nil { err = checkExplicitProjectPermission(ctx, existingUserGrant.ProjectGrantID, existingUserGrant.ProjectID) if err != nil { return nil, nil, err } } - + if check != nil { + if err = check(existingUserGrant.ProjectID, existingUserGrant.ProjectGrantID)(existingUserGrant.ResourceOwner, ""); err != nil { + return nil, nil, err + } + } removeUserGrant := NewUserGrantWriteModel(grantID, existingUserGrant.ResourceOwner) userGrantAgg := UserGrantAggregateFromWriteModel(&removeUserGrant.WriteModel) if cascade { @@ -279,7 +295,7 @@ func (c *Commands) removeUserGrant(ctx context.Context, grantID, resourceOwner s existingUserGrant.ProjectGrantID), existingUserGrant, nil } -func (c *Commands) userGrantWriteModelByID(ctx context.Context, userGrantID, resourceOwner string) (writeModel *UserGrantWriteModel, err error) { +func (c *Commands) userGrantWriteModelByID(ctx context.Context, userGrantID string, resourceOwner string) (writeModel *UserGrantWriteModel, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -291,31 +307,46 @@ func (c *Commands) userGrantWriteModelByID(ctx context.Context, userGrantID, res return writeModel, nil } -func (c *Commands) checkUserGrantPreCondition(ctx context.Context, usergrant *domain.UserGrant, resourceOwner string) (err error) { +func (c *Commands) checkUserGrantPreCondition(ctx context.Context, usergrant *domain.UserGrant, check UserGrantPermissionCheck) (err error) { if !authz.GetFeatures(ctx).ShouldUseImprovedPerformance(feature.ImprovedPerformanceTypeUserGrant) { - return c.checkUserGrantPreConditionOld(ctx, usergrant, resourceOwner) + return c.checkUserGrantPreConditionOld(ctx, usergrant, check) } ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - if err := c.checkUserExists(ctx, usergrant.UserID, ""); err != nil { + if _, err := c.checkUserExists(ctx, usergrant.UserID, ""); err != nil { return err } - existingRoleKeys, err := c.searchUserGrantPreConditionState(ctx, usergrant, resourceOwner) + if usergrant.ProjectGrantID != "" || usergrant.ResourceOwner == "" { + projectOwner, grantID, err := c.searchProjectOwnerAndGrantID(ctx, usergrant.ProjectID, "") + if err != nil { + return err + } + if usergrant.ResourceOwner == "" { + usergrant.ResourceOwner = projectOwner + } + if usergrant.ProjectGrantID == "" { + usergrant.ProjectGrantID = grantID + } + } + existingRoleKeys, err := c.searchUserGrantPreConditionState(ctx, usergrant) if err != nil { return err } if usergrant.HasInvalidRoles(existingRoleKeys) { return zerrors.ThrowPreconditionFailed(err, "COMMAND-mm9F4", "Errors.Project.Role.NotFound") } - return nil + if check != nil { + return check(usergrant.ProjectID, usergrant.ProjectGrantID)(usergrant.ResourceOwner, "") + } + return checkExplicitProjectPermission(ctx, usergrant.ProjectGrantID, usergrant.ProjectID) } // this code needs to be rewritten anyways as soon as we improved the fields handling // //nolint:gocognit -func (c *Commands) searchUserGrantPreConditionState(ctx context.Context, userGrant *domain.UserGrant, resourceOwner string) (existingRoleKeys []string, err error) { +func (c *Commands) searchUserGrantPreConditionState(ctx context.Context, userGrant *domain.UserGrant) (existingRoleKeys []string, err error) { criteria := []map[eventstore.FieldType]any{ // project state query { @@ -327,7 +358,7 @@ func (c *Commands) searchUserGrantPreConditionState(ctx context.Context, userGra // granted org query { eventstore.FieldTypeAggregateType: org.AggregateType, - eventstore.FieldTypeAggregateID: resourceOwner, + eventstore.FieldTypeAggregateID: userGrant.ResourceOwner, eventstore.FieldTypeFieldName: org.OrgStateSearchField, eventstore.FieldTypeObjectType: org.OrgSearchType, }, @@ -386,7 +417,7 @@ func (c *Commands) searchUserGrantPreConditionState(ctx context.Context, userGra case project.ProjectGrantGrantedOrgIDSearchField: var orgID string err := result.Value.Unmarshal(&orgID) - if err != nil || orgID != resourceOwner { + if err != nil || orgID != userGrant.ResourceOwner { return nil, zerrors.ThrowPreconditionFailed(err, "COMMAND-3m9gg", "Errors.Org.NotFound") } case project.ProjectGrantStateSearchField: @@ -425,26 +456,63 @@ func (c *Commands) searchUserGrantPreConditionState(ctx context.Context, userGra return existingRoleKeys, nil } -func (c *Commands) checkUserGrantPreConditionOld(ctx context.Context, usergrant *domain.UserGrant, resourceOwner string) (err error) { +func (c *Commands) checkUserGrantPreConditionOld(ctx context.Context, usergrant *domain.UserGrant, check UserGrantPermissionCheck) (err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - preConditions := NewUserGrantPreConditionReadModel(usergrant.UserID, usergrant.ProjectID, usergrant.ProjectGrantID, resourceOwner) + preConditions := NewUserGrantPreConditionReadModel(usergrant.UserID, usergrant.ProjectID, usergrant.ProjectGrantID, usergrant.ResourceOwner) err = c.eventstore.FilterToQueryReducer(ctx, preConditions) if err != nil { return err } + if usergrant.ResourceOwner == "" { + usergrant.ResourceOwner = preConditions.ProjectResourceOwner + } + if usergrant.ProjectGrantID == "" { + usergrant.ProjectGrantID = preConditions.ProjectGrantID + } if !preConditions.UserExists { return zerrors.ThrowPreconditionFailed(err, "COMMAND-4f8sg", "Errors.User.NotFound") } - if usergrant.ProjectGrantID == "" && !preConditions.ProjectExists { + projectIsOwned := usergrant.ResourceOwner == "" || usergrant.ResourceOwner == preConditions.ProjectResourceOwner + if projectIsOwned && !preConditions.ProjectExists { return zerrors.ThrowPreconditionFailed(err, "COMMAND-3n77S", "Errors.Project.NotFound") } - if usergrant.ProjectGrantID != "" && !preConditions.ProjectGrantExists { + if !projectIsOwned && !preConditions.ProjectGrantExists { return zerrors.ThrowPreconditionFailed(err, "COMMAND-4m9ff", "Errors.Project.Grant.NotFound") } if usergrant.HasInvalidRoles(preConditions.ExistingRoleKeys) { return zerrors.ThrowPreconditionFailed(err, "COMMAND-mm9F4", "Errors.Project.Role.NotFound") } - return nil + if check != nil { + return check(usergrant.ProjectID, usergrant.ProjectGrantID)(usergrant.ResourceOwner, "") + } + return checkExplicitProjectPermission(ctx, usergrant.ProjectGrantID, usergrant.ProjectID) +} + +func (c *Commands) searchProjectOwnerAndGrantID(ctx context.Context, projectID string, grantedOrgID string) (projectOwner string, grantID string, err error) { + grantIDQuery := map[eventstore.FieldType]any{ + eventstore.FieldTypeAggregateType: project.AggregateType, + eventstore.FieldTypeAggregateID: projectID, + eventstore.FieldTypeFieldName: project.ProjectGrantGrantedOrgIDSearchField, + } + if grantedOrgID != "" { + grantIDQuery[eventstore.FieldTypeValue] = grantedOrgID + grantIDQuery[eventstore.FieldTypeObjectType] = project.ProjectGrantSearchType + + } + results, err := c.eventstore.Search(ctx, grantIDQuery) + if err != nil { + return "", "", err + } + for _, result := range results { + projectOwner = result.Aggregate.ResourceOwner + if grantedOrgID != "" && grantedOrgID == projectOwner { + return projectOwner, "", nil + } + if result.Object.Type == project.ProjectGrantSearchType { + return projectOwner, result.Object.ID, nil + } + } + return projectOwner, grantID, err } diff --git a/internal/command/user_grant_model.go b/internal/command/user_grant_model.go index b2490177d9..b35c96a2d5 100644 --- a/internal/command/user_grant_model.go +++ b/internal/command/user_grant_model.go @@ -36,6 +36,7 @@ func (wm *UserGrantWriteModel) Reduce() error { wm.ProjectGrantID = e.ProjectGrantID wm.RoleKeys = e.RoleKeys wm.State = domain.UserGrantStateActive + wm.ResourceOwner = e.Aggregate().ResourceOwner case *usergrant.UserGrantChangedEvent: wm.RoleKeys = e.RoleKeys case *usergrant.UserGrantCascadeChangedEvent: @@ -86,17 +87,18 @@ func UserGrantAggregateFromWriteModel(wm *eventstore.WriteModel) *eventstore.Agg type UserGrantPreConditionReadModel struct { eventstore.WriteModel - UserID string - ProjectID string - ProjectGrantID string - ResourceOwner string - UserExists bool - ProjectExists bool - ProjectGrantExists bool - ExistingRoleKeys []string + UserID string + ProjectID string + ProjectResourceOwner string + ProjectGrantID string + ResourceOwner string + UserExists bool + ProjectExists bool + ProjectGrantExists bool + ExistingRoleKeys []string } -func NewUserGrantPreConditionReadModel(userID, projectID, projectGrantID, resourceOwner string) *UserGrantPreConditionReadModel { +func NewUserGrantPreConditionReadModel(userID, projectID, projectGrantID string, resourceOwner string) *UserGrantPreConditionReadModel { return &UserGrantPreConditionReadModel{ UserID: userID, ProjectID: projectID, @@ -117,15 +119,19 @@ func (wm *UserGrantPreConditionReadModel) Reduce() error { case *user.UserRemovedEvent: wm.UserExists = false case *project.ProjectAddedEvent: - if wm.ProjectGrantID == "" && wm.ResourceOwner == e.Aggregate().ResourceOwner { + if wm.ResourceOwner == "" || wm.ResourceOwner == e.Aggregate().ResourceOwner { wm.ProjectExists = true } + wm.ProjectResourceOwner = e.Aggregate().ResourceOwner case *project.ProjectRemovedEvent: wm.ProjectExists = false case *project.GrantAddedEvent: - if wm.ProjectGrantID == e.GrantID && wm.ResourceOwner == e.GrantedOrgID { + if (wm.ProjectGrantID == e.GrantID || wm.ProjectGrantID == "") && wm.ResourceOwner != "" && wm.ResourceOwner == e.GrantedOrgID { wm.ProjectGrantExists = true wm.ExistingRoleKeys = e.RoleKeys + if wm.ProjectGrantID == "" { + wm.ProjectGrantID = e.GrantID + } } case *project.GrantChangedEvent: if wm.ProjectGrantID == e.GrantID { diff --git a/internal/command/user_grant_test.go b/internal/command/user_grant_test.go index dec5903fe8..b12e190d82 100644 --- a/internal/command/user_grant_test.go +++ b/internal/command/user_grant_test.go @@ -2,6 +2,7 @@ package command import ( "context" + "errors" "testing" "github.com/stretchr/testify/assert" @@ -19,15 +20,27 @@ import ( "github.com/zitadel/zitadel/internal/zerrors" ) +var ( + errMockedPermissionCheck = errors.New("mocked permission check error") + isMockedPermissionCheckErr = func(err error) bool { + return errors.Is(err, errMockedPermissionCheck) + } + succeedingUserGrantPermissionCheck = func(_, _ string) PermissionCheck { + return func(_, _ string) error { return nil } + } + failingUserGrantPermissionCheck = func(_, _ string) PermissionCheck { + return func(_, _ string) error { return errMockedPermissionCheck } + } +) + func TestCommandSide_AddUserGrant(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore - idGenerator id.Generator + eventstore func(t *testing.T) *eventstore.Eventstore + idGenerator func(t *testing.T) id.Generator } type args struct { - ctx context.Context - userGrant *domain.UserGrant - resourceOwner string + ctx context.Context + userGrant *domain.UserGrant } type res struct { want *domain.UserGrant @@ -42,16 +55,16 @@ func TestCommandSide_AddUserGrant(t *testing.T) { { name: "invalid usergrant, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ ctx: authz.NewMockContextWithPermissions("", "org", "user", []string{domain.RoleProjectOwner}), userGrant: &domain.UserGrant{ UserID: "user1", + ObjectRoot: models.ObjectRoot{ + ResourceOwner: "org1", + }, }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsErrorInvalidArgument, @@ -60,8 +73,7 @@ func TestCommandSide_AddUserGrant(t *testing.T) { { name: "user removed, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -94,8 +106,10 @@ func TestCommandSide_AddUserGrant(t *testing.T) { userGrant: &domain.UserGrant{ UserID: "user1", ProjectID: "project1", + ObjectRoot: models.ObjectRoot{ + ResourceOwner: "org1", + }, }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsPreconditionFailed, @@ -104,8 +118,7 @@ func TestCommandSide_AddUserGrant(t *testing.T) { { name: "project removed, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -143,8 +156,10 @@ func TestCommandSide_AddUserGrant(t *testing.T) { userGrant: &domain.UserGrant{ UserID: "user1", ProjectID: "project1", + ObjectRoot: models.ObjectRoot{ + ResourceOwner: "org1", + }, }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsPreconditionFailed, @@ -153,8 +168,7 @@ func TestCommandSide_AddUserGrant(t *testing.T) { { name: "project on other org, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -185,8 +199,10 @@ func TestCommandSide_AddUserGrant(t *testing.T) { userGrant: &domain.UserGrant{ UserID: "user1", ProjectID: "project1", + ObjectRoot: models.ObjectRoot{ + ResourceOwner: "org2", + }, }, - resourceOwner: "org2", }, res: res{ err: zerrors.IsPreconditionFailed, @@ -195,8 +211,7 @@ func TestCommandSide_AddUserGrant(t *testing.T) { { name: "project roles not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -228,8 +243,10 @@ func TestCommandSide_AddUserGrant(t *testing.T) { UserID: "user1", ProjectID: "project1", RoleKeys: []string{"roleKey"}, + ObjectRoot: models.ObjectRoot{ + ResourceOwner: "org1", + }, }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsPreconditionFailed, @@ -238,8 +255,7 @@ func TestCommandSide_AddUserGrant(t *testing.T) { { name: "project grant not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -272,8 +288,10 @@ func TestCommandSide_AddUserGrant(t *testing.T) { ProjectID: "project1", ProjectGrantID: "projectgrant1", RoleKeys: []string{"roleKey"}, + ObjectRoot: models.ObjectRoot{ + ResourceOwner: "org1", + }, }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsPreconditionFailed, @@ -282,8 +300,7 @@ func TestCommandSide_AddUserGrant(t *testing.T) { { name: "project grant roles not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -332,8 +349,10 @@ func TestCommandSide_AddUserGrant(t *testing.T) { ProjectID: "project1", ProjectGrantID: "projectgrant1", RoleKeys: []string{"roleKey"}, + ObjectRoot: models.ObjectRoot{ + ResourceOwner: "org1", + }, }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsPreconditionFailed, @@ -342,8 +361,7 @@ func TestCommandSide_AddUserGrant(t *testing.T) { { name: "project grant on other org, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -392,8 +410,10 @@ func TestCommandSide_AddUserGrant(t *testing.T) { ProjectID: "project1", ProjectGrantID: "projectgrant1", RoleKeys: []string{"rolekey1"}, + ObjectRoot: models.ObjectRoot{ + ResourceOwner: "org2", + }, }, - resourceOwner: "org2", }, res: res{ err: zerrors.IsPreconditionFailed, @@ -402,8 +422,7 @@ func TestCommandSide_AddUserGrant(t *testing.T) { { name: "usergrant for project, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -445,7 +464,82 @@ func TestCommandSide_AddUserGrant(t *testing.T) { ), ), ), - idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "usergrant1"), + idGenerator: func(t *testing.T) id.Generator { + return id_mock.NewIDGeneratorExpectIDs(t, "usergrant1") + }, + }, + args: args{ + ctx: authz.NewMockContextWithPermissions("", "", "", []string{domain.RoleProjectOwner}), + userGrant: &domain.UserGrant{ + UserID: "user1", + ProjectID: "project1", + RoleKeys: []string{"rolekey1"}, + ObjectRoot: models.ObjectRoot{ + ResourceOwner: "org1", + }, + }, + }, + res: res{ + want: &domain.UserGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "usergrant1", + ResourceOwner: "org1", + }, + UserID: "user1", + ProjectID: "project1", + RoleKeys: []string{"rolekey1"}, + State: domain.UserGrantStateActive, + }, + }, + }, + { + name: "usergrant without resource owner on project, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username1", + "firstname1", + "lastname1", + "nickname1", + "displayname1", + language.German, + domain.GenderMale, + "email1", + true, + ), + ), + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", true, true, true, + domain.PrivateLabelingSettingUnspecified, + ), + ), + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "rolekey1", + "rolekey", + "", + ), + ), + ), + expectPush( + usergrant.NewUserGrantAddedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + "user1", + "project1", + "", + []string{"rolekey1"}, + ), + ), + ), + idGenerator: func(t *testing.T) id.Generator { + return id_mock.NewIDGeneratorExpectIDs(t, "usergrant1") + }, }, args: args{ ctx: authz.NewMockContextWithPermissions("", "", "", []string{domain.RoleProjectOwner}), @@ -454,7 +548,6 @@ func TestCommandSide_AddUserGrant(t *testing.T) { ProjectID: "project1", RoleKeys: []string{"rolekey1"}, }, - resourceOwner: "org1", }, res: res{ want: &domain.UserGrant{ @@ -472,8 +565,90 @@ func TestCommandSide_AddUserGrant(t *testing.T) { { name: "usergrant for projectgrant, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username1", + "firstname1", + "lastname1", + "nickname1", + "displayname1", + language.German, + domain.GenderMale, + "email1", + true, + ), + ), + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org2").Aggregate, + "projectname1", true, true, true, + domain.PrivateLabelingSettingUnspecified, + ), + ), + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org2").Aggregate, + "rolekey1", + "rolekey", + "", + ), + ), + eventFromEventPusher( + project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org2").Aggregate, + "projectgrant1", + "org1", + []string{"rolekey1"}, + ), + ), + ), + expectPush( + usergrant.NewUserGrantAddedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + "user1", + "project1", + "projectgrant1", + []string{"rolekey1"}, + ), + ), + ), + idGenerator: func(t *testing.T) id.Generator { + return id_mock.NewIDGeneratorExpectIDs(t, "usergrant1") + }, + }, + args: args{ + ctx: authz.NewMockContextWithPermissions("", "", "", []string{domain.RoleProjectOwner}), + userGrant: &domain.UserGrant{ + UserID: "user1", + ProjectID: "project1", + ProjectGrantID: "projectgrant1", + RoleKeys: []string{"rolekey1"}, + ObjectRoot: models.ObjectRoot{ + ResourceOwner: "org1", + }, + }, + }, + res: res{ + want: &domain.UserGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "usergrant1", + ResourceOwner: "org1", + }, + UserID: "user1", + ProjectID: "project1", + ProjectGrantID: "projectgrant1", + RoleKeys: []string{"rolekey1"}, + State: domain.UserGrantStateActive, + }, + }, + }, + { + name: "usergrant for granted resource owner, ok", + fields: fields{ + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -523,17 +698,20 @@ func TestCommandSide_AddUserGrant(t *testing.T) { ), ), ), - idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "usergrant1"), + idGenerator: func(t *testing.T) id.Generator { + return id_mock.NewIDGeneratorExpectIDs(t, "usergrant1") + }, }, args: args{ ctx: authz.NewMockContextWithPermissions("", "", "", []string{domain.RoleProjectOwner}), userGrant: &domain.UserGrant{ - UserID: "user1", - ProjectID: "project1", - ProjectGrantID: "projectgrant1", - RoleKeys: []string{"rolekey1"}, + UserID: "user1", + ProjectID: "project1", + RoleKeys: []string{"rolekey1"}, + ObjectRoot: models.ObjectRoot{ + ResourceOwner: "org1", + }, }, - resourceOwner: "org1", }, res: res{ want: &domain.UserGrant{ @@ -550,34 +728,111 @@ func TestCommandSide_AddUserGrant(t *testing.T) { }, }, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - r := &Commands{ - eventstore: tt.fields.eventstore, - idGenerator: tt.fields.idGenerator, - } - got, err := r.AddUserGrant(tt.args.ctx, tt.args.userGrant, tt.args.resourceOwner) - if tt.res.err == nil { - assert.NoError(t, err) - } - if tt.res.err != nil && !tt.res.err(err) { - t.Errorf("got wrong err: %v ", err) - } - if tt.res.err == nil { - assert.Equal(t, tt.res.want, got) - } - }) - } + t.Run("without permission check", func(t *testing.T) { + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore(t), + } + if tt.fields.idGenerator != nil { + r.idGenerator = tt.fields.idGenerator(t) + } + got, err := r.AddUserGrant(tt.args.ctx, tt.args.userGrant, nil) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } + }) + t.Run("with succeeding permission check", func(t *testing.T) { + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore(t), + } + if tt.fields.idGenerator != nil { + r.idGenerator = tt.fields.idGenerator(t) + } + // we use an empty context and only rely on the permission check implementation + got, err := r.AddUserGrant(context.Background(), tt.args.userGrant, succeedingUserGrantPermissionCheck) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } + }) + t.Run("with failing permission check", func(t *testing.T) { + r := &Commands{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username1", + "firstname1", + "lastname1", + "nickname1", + "displayname1", + language.German, + domain.GenderMale, + "email1", + true, + ), + ), + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", true, true, true, + domain.PrivateLabelingSettingUnspecified, + ), + ), + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "rolekey1", + "rolekey", + "", + ), + ), + ), + ), + } + // we use an empty context and only rely on the permission check implementation + _, err := r.AddUserGrant(context.Background(), &domain.UserGrant{ + UserID: "user1", + ProjectID: "project1", + RoleKeys: []string{"rolekey1"}, + ObjectRoot: models.ObjectRoot{ + ResourceOwner: "org1", + }, + }, failingUserGrantPermissionCheck) + assert.ErrorIs(t, err, errMockedPermissionCheck) + }) } func TestCommandSide_ChangeUserGrant(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore } type args struct { - ctx context.Context - userGrant *domain.UserGrant - resourceOwner string + ctx context.Context + userGrant *domain.UserGrant + permissionCheck UserGrantPermissionCheck + cascade bool + ignoreUnchanged bool } type res struct { want *domain.UserGrant @@ -592,16 +847,16 @@ func TestCommandSide_ChangeUserGrant(t *testing.T) { { name: "invalid usergrant, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ ctx: authz.NewMockContextWithPermissions("", "org", "user", []string{domain.RoleProjectOwner}), userGrant: &domain.UserGrant{ UserID: "user1", + ObjectRoot: models.ObjectRoot{ + ResourceOwner: "org1", + }, }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsErrorInvalidArgument, @@ -610,28 +865,66 @@ func TestCommandSide_ChangeUserGrant(t *testing.T) { { name: "invalid permissions, error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( usergrant.NewUserGrantAddedEvent(context.Background(), - &usergrant.NewAggregate("usergrant1", "org").Aggregate, + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, "user1", "project1", "", []string{"rolekey1"}), ), ), + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username1", + "firstname1", + "lastname1", + "nickname1", + "displayname1", + language.German, + domain.GenderMale, + "email1", + true, + ), + ), + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", true, true, true, + domain.PrivateLabelingSettingUnspecified, + ), + ), + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "rolekey1", + "rolekey", + "", + ), + ), + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "rolekey2", + "rolekey 2", + "", + ), + ), + ), ), }, args: args{ ctx: context.Background(), userGrant: &domain.UserGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: "usergrant1", + AggregateID: "usergrant1", + ResourceOwner: "org1", }, UserID: "user1", }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsPermissionDenied, @@ -640,8 +933,7 @@ func TestCommandSide_ChangeUserGrant(t *testing.T) { { name: "usergrant not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -649,13 +941,13 @@ func TestCommandSide_ChangeUserGrant(t *testing.T) { ctx: authz.NewMockContextWithPermissions("", "", "", []string{domain.RoleProjectOwner}), userGrant: &domain.UserGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: "usergrant1", + AggregateID: "usergrant1", + ResourceOwner: "org1", }, UserID: "user1", ProjectID: "project1", RoleKeys: []string{"rolekey1"}, }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsNotFound, @@ -664,8 +956,7 @@ func TestCommandSide_ChangeUserGrant(t *testing.T) { { name: "usergrant not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -673,13 +964,13 @@ func TestCommandSide_ChangeUserGrant(t *testing.T) { ctx: authz.NewMockContextWithPermissions("", "", "", []string{domain.RoleProjectOwner}), userGrant: &domain.UserGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: "usergrant1", + AggregateID: "usergrant1", + ResourceOwner: "org1", }, UserID: "user1", ProjectID: "project1", RoleKeys: []string{"rolekey1"}, }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsNotFound, @@ -688,8 +979,7 @@ func TestCommandSide_ChangeUserGrant(t *testing.T) { { name: "usergrant roles not changed, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( usergrant.NewUserGrantAddedEvent(context.Background(), @@ -705,13 +995,13 @@ func TestCommandSide_ChangeUserGrant(t *testing.T) { ctx: authz.NewMockContextWithPermissions("", "", "", []string{domain.RoleProjectOwner}), userGrant: &domain.UserGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: "usergrant1", + AggregateID: "usergrant1", + ResourceOwner: "org1", }, UserID: "user1", ProjectID: "project1", RoleKeys: []string{"rolekey1"}, }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsPreconditionFailed, @@ -720,8 +1010,7 @@ func TestCommandSide_ChangeUserGrant(t *testing.T) { { name: "user removed, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( usergrant.NewUserGrantAddedEvent(context.Background(), @@ -762,12 +1051,12 @@ func TestCommandSide_ChangeUserGrant(t *testing.T) { ctx: authz.NewMockContextWithPermissions("", "org", "user", []string{domain.RoleProjectOwner}), userGrant: &domain.UserGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: "usergrant1", + AggregateID: "usergrant1", + ResourceOwner: "org1", }, UserID: "user1", ProjectID: "project1", }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsPreconditionFailed, @@ -776,8 +1065,7 @@ func TestCommandSide_ChangeUserGrant(t *testing.T) { { name: "project removed, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( usergrant.NewUserGrantAddedEvent(context.Background(), @@ -823,12 +1111,12 @@ func TestCommandSide_ChangeUserGrant(t *testing.T) { ctx: authz.NewMockContextWithPermissions("", "org", "user", []string{domain.RoleProjectOwner}), userGrant: &domain.UserGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: "usergrant1", + AggregateID: "usergrant1", + ResourceOwner: "org1", }, UserID: "user1", ProjectID: "project1", }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsPreconditionFailed, @@ -837,8 +1125,7 @@ func TestCommandSide_ChangeUserGrant(t *testing.T) { { name: "project roles not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( usergrant.NewUserGrantAddedEvent(context.Background(), @@ -877,13 +1164,13 @@ func TestCommandSide_ChangeUserGrant(t *testing.T) { ctx: authz.NewMockContextWithPermissions("", "org", "user", []string{domain.RoleProjectOwner}), userGrant: &domain.UserGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: "usergrant1", + AggregateID: "usergrant1", + ResourceOwner: "org1", }, UserID: "user1", ProjectID: "project1", RoleKeys: []string{"roleKey"}, }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsPreconditionFailed, @@ -892,8 +1179,7 @@ func TestCommandSide_ChangeUserGrant(t *testing.T) { { name: "project grant not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( usergrant.NewUserGrantAddedEvent(context.Background(), @@ -932,14 +1218,14 @@ func TestCommandSide_ChangeUserGrant(t *testing.T) { ctx: authz.NewMockContextWithPermissions("", "org", "user", []string{domain.RoleProjectOwner}), userGrant: &domain.UserGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: "usergrant1", + AggregateID: "usergrant1", + ResourceOwner: "org1", }, UserID: "user1", ProjectID: "project1", ProjectGrantID: "projectgrant1", RoleKeys: []string{"roleKey"}, }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsPreconditionFailed, @@ -948,8 +1234,7 @@ func TestCommandSide_ChangeUserGrant(t *testing.T) { { name: "project grant roles not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( usergrant.NewUserGrantAddedEvent(context.Background(), @@ -1004,14 +1289,14 @@ func TestCommandSide_ChangeUserGrant(t *testing.T) { ctx: authz.NewMockContextWithPermissions("", "org", "user", []string{domain.RoleProjectOwner}), userGrant: &domain.UserGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: "usergrant1", + AggregateID: "usergrant1", + ResourceOwner: "org1", }, UserID: "user1", ProjectID: "project1", ProjectGrantID: "projectgrant1", RoleKeys: []string{"roleKey"}, }, - resourceOwner: "org1", }, res: res{ err: zerrors.IsPreconditionFailed, @@ -1020,8 +1305,7 @@ func TestCommandSide_ChangeUserGrant(t *testing.T) { { name: "usergrant for project, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( usergrant.NewUserGrantAddedEvent(context.Background(), @@ -1083,13 +1367,13 @@ func TestCommandSide_ChangeUserGrant(t *testing.T) { ctx: authz.NewMockContextWithPermissions("", "", "", []string{domain.RoleProjectOwner}), userGrant: &domain.UserGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: "usergrant1", + AggregateID: "usergrant1", + ResourceOwner: "org1", }, UserID: "user1", ProjectID: "project1", RoleKeys: []string{"rolekey1", "rolekey2"}, }, - resourceOwner: "org1", }, res: res{ want: &domain.UserGrant{ @@ -1104,11 +1388,94 @@ func TestCommandSide_ChangeUserGrant(t *testing.T) { }, }, }, + { + name: "usergrant for project cascade, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + usergrant.NewUserGrantAddedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + "user1", + "project1", + "", []string{"rolekey1"}), + ), + ), + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username1", + "firstname1", + "lastname1", + "nickname1", + "displayname1", + language.German, + domain.GenderMale, + "email1", + true, + ), + ), + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", true, true, true, + domain.PrivateLabelingSettingUnspecified, + ), + ), + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "rolekey1", + "rolekey", + "", + ), + ), + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "rolekey2", + "rolekey 2", + "", + ), + ), + ), + expectPush( + usergrant.NewUserGrantCascadeChangedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + []string{"rolekey1", "rolekey2"}, + ), + ), + ), + }, + args: args{ + ctx: authz.NewMockContextWithPermissions("", "", "", []string{domain.RoleProjectOwner}), + userGrant: &domain.UserGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "usergrant1", + ResourceOwner: "org1", + }, + RoleKeys: []string{"rolekey1", "rolekey2"}, + }, + cascade: true, + }, + res: res{ + want: &domain.UserGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "usergrant1", + ResourceOwner: "org1", + }, + UserID: "user1", + ProjectID: "project1", + State: domain.UserGrantStateActive, + RoleKeys: []string{"rolekey1", "rolekey2"}, + }, + }, + }, { name: "usergrant for projectgrant, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( usergrant.NewUserGrantAddedEvent(context.Background(), @@ -1178,14 +1545,14 @@ func TestCommandSide_ChangeUserGrant(t *testing.T) { ctx: authz.NewMockContextWithPermissions("", "", "", []string{domain.RoleProjectOwner}), userGrant: &domain.UserGrant{ ObjectRoot: models.ObjectRoot{ - AggregateID: "usergrant1", + AggregateID: "usergrant1", + ResourceOwner: "org1", }, UserID: "user1", ProjectID: "project1", ProjectGrantID: "projectgrant1", RoleKeys: []string{"rolekey1", "rolekey2"}, }, - resourceOwner: "org1", }, res: res{ want: &domain.UserGrant{ @@ -1201,13 +1568,307 @@ func TestCommandSide_ChangeUserGrant(t *testing.T) { }, }, }, + { + name: "usergrant for project without resource owner, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + usergrant.NewUserGrantAddedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + "user1", + "project1", + "", []string{"rolekey1"}), + ), + ), + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username1", + "firstname1", + "lastname1", + "nickname1", + "displayname1", + language.German, + domain.GenderMale, + "email1", + true, + ), + ), + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", true, true, true, + domain.PrivateLabelingSettingUnspecified, + ), + ), + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "rolekey1", + "rolekey", + "", + ), + ), + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "rolekey2", + "rolekey 2", + "", + ), + ), + ), + expectPush( + usergrant.NewUserGrantChangedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + "user1", + []string{"rolekey1", "rolekey2"}, + ), + ), + ), + }, + args: args{ + ctx: authz.NewMockContextWithPermissions("", "", "", []string{domain.RoleProjectOwner}), + userGrant: &domain.UserGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "usergrant1", + }, + UserID: "user1", + ProjectID: "project1", + RoleKeys: []string{"rolekey1", "rolekey2"}, + }, + }, + res: res{ + want: &domain.UserGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "usergrant1", + ResourceOwner: "org1", + }, + UserID: "user1", + ProjectID: "project1", + RoleKeys: []string{"rolekey1", "rolekey2"}, + State: domain.UserGrantStateActive, + }, + }, + }, + { + name: "usergrant for project with passed succeeding permission check, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + usergrant.NewUserGrantAddedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + "user1", + "project1", + "", []string{"rolekey1"}), + ), + ), + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username1", + "firstname1", + "lastname1", + "nickname1", + "displayname1", + language.German, + domain.GenderMale, + "email1", + true, + ), + ), + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", true, true, true, + domain.PrivateLabelingSettingUnspecified, + ), + ), + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "rolekey1", + "rolekey", + "", + ), + ), + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "rolekey2", + "rolekey 2", + "", + ), + ), + ), + expectPush( + usergrant.NewUserGrantChangedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + "user1", + []string{"rolekey1", "rolekey2"}, + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + userGrant: &domain.UserGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "usergrant1", + ResourceOwner: "org1", + }, + UserID: "user1", + ProjectID: "project1", + RoleKeys: []string{"rolekey1", "rolekey2"}, + }, + permissionCheck: succeedingUserGrantPermissionCheck, + }, + res: res{ + want: &domain.UserGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "usergrant1", + ResourceOwner: "org1", + }, + UserID: "user1", + ProjectID: "project1", + RoleKeys: []string{"rolekey1", "rolekey2"}, + State: domain.UserGrantStateActive, + }, + }, + }, + { + name: "usergrant for project with passed failing permission check, error", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + usergrant.NewUserGrantAddedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + "user1", + "project1", + "", []string{"rolekey1"}), + ), + ), + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username1", + "firstname1", + "lastname1", + "nickname1", + "displayname1", + language.German, + domain.GenderMale, + "email1", + true, + ), + ), + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", true, true, true, + domain.PrivateLabelingSettingUnspecified, + ), + ), + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "rolekey1", + "rolekey", + "", + ), + ), + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "rolekey2", + "rolekey 2", + "", + ), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + userGrant: &domain.UserGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "usergrant1", + ResourceOwner: "org1", + }, + UserID: "user1", + ProjectID: "project1", + RoleKeys: []string{"rolekey1", "rolekey2"}, + }, + permissionCheck: failingUserGrantPermissionCheck, + }, + res: res{ + want: &domain.UserGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "usergrant1", + ResourceOwner: "org1", + }, + UserID: "user1", + ProjectID: "project1", + RoleKeys: []string{"rolekey1", "rolekey2"}, + State: domain.UserGrantStateActive, + }, + err: isMockedPermissionCheckErr, + }, + }, + { + name: "usergrant roles not changed, ignore unchanged, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + usergrant.NewUserGrantAddedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + "user1", + "project1", + "", []string{"rolekey1", "rolekey2"}), + ), + ), + ), + }, + args: args{ + ctx: authz.NewMockContextWithPermissions("", "", "", []string{domain.RoleProjectOwner}), + userGrant: &domain.UserGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "usergrant1", + ResourceOwner: "org1", + }, + UserID: "user1", + ProjectID: "project1", + RoleKeys: []string{"rolekey1", "rolekey2"}, + }, + ignoreUnchanged: true, + }, + res: res{ + want: &domain.UserGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "usergrant1", + ResourceOwner: "org1", + }, + UserID: "user1", + ProjectID: "project1", + RoleKeys: []string{"rolekey1", "rolekey2"}, + State: domain.UserGrantStateActive, + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), } - got, err := r.ChangeUserGrant(tt.args.ctx, tt.args.userGrant, tt.args.resourceOwner) + got, err := r.ChangeUserGrant(tt.args.ctx, tt.args.userGrant, tt.args.cascade, tt.args.ignoreUnchanged, tt.args.permissionCheck) if tt.res.err == nil { assert.NoError(t, err) } @@ -1223,12 +1884,13 @@ func TestCommandSide_ChangeUserGrant(t *testing.T) { func TestCommandSide_DeactivateUserGrant(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore } type args struct { ctx context.Context userGrantID string resourceOwner string + check UserGrantPermissionCheck } type res struct { want *domain.ObjectDetails @@ -1243,12 +1905,10 @@ func TestCommandSide_DeactivateUserGrant(t *testing.T) { { name: "invalid usergrantID, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ - ctx: context.Background(), + ctx: authz.NewMockContextWithPermissions("", "", "", []string{domain.RoleProjectOwner}), resourceOwner: "org1", }, res: res{ @@ -1256,25 +1916,39 @@ func TestCommandSide_DeactivateUserGrant(t *testing.T) { }, }, { - name: "invalid resourceOwner, error", + name: "not provided resourceOwner, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + usergrant.NewUserGrantAddedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org").Aggregate, + "user1", + "project1", + "", []string{"rolekey1"}), + ), + ), + expectPush( + usergrant.NewUserGrantDeactivatedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org").Aggregate, + ), + ), ), }, args: args{ - ctx: context.Background(), + ctx: authz.NewMockContextWithPermissions("", "", "", []string{domain.RoleProjectOwner}), userGrantID: "usergrant1", }, res: res{ - err: zerrors.IsErrorInvalidArgument, + want: &domain.ObjectDetails{ + ResourceOwner: "org", + }, }, }, { name: "usergrant not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -1290,8 +1964,7 @@ func TestCommandSide_DeactivateUserGrant(t *testing.T) { { name: "usergrant removed, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( usergrant.NewUserGrantAddedEvent(context.Background(), @@ -1320,14 +1993,13 @@ func TestCommandSide_DeactivateUserGrant(t *testing.T) { }, }, { - name: "no permissions, permisison denied error", + name: "no permissions, permission denied error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( usergrant.NewUserGrantAddedEvent(context.Background(), - &usergrant.NewAggregate("usergrant1", "org").Aggregate, + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, "user1", "project1", "", []string{"rolekey1"}), @@ -1345,10 +2017,9 @@ func TestCommandSide_DeactivateUserGrant(t *testing.T) { }, }, { - name: "already deactivated, precondition error", + name: "already deactivated, ignore, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( usergrant.NewUserGrantAddedEvent(context.Background(), @@ -1370,14 +2041,15 @@ func TestCommandSide_DeactivateUserGrant(t *testing.T) { resourceOwner: "org1", }, res: res{ - err: zerrors.IsPreconditionFailed, + want: &domain.ObjectDetails{ + ResourceOwner: "org", + }, }, }, { name: "deactivated, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( usergrant.NewUserGrantAddedEvent(context.Background(), @@ -1405,13 +2077,70 @@ func TestCommandSide_DeactivateUserGrant(t *testing.T) { }, }, }, + { + name: "with passed succeeding permission check, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + usergrant.NewUserGrantAddedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + "user1", + "project1", + "", []string{"rolekey1"}), + ), + ), + expectPush( + usergrant.NewUserGrantDeactivatedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + userGrantID: "usergrant1", + resourceOwner: "org1", + check: succeedingUserGrantPermissionCheck, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "with passed failing permission check, error", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + usergrant.NewUserGrantAddedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + "user1", + "project1", + "", []string{"rolekey1"}), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + userGrantID: "usergrant1", + resourceOwner: "org1", + check: failingUserGrantPermissionCheck, + }, + res: res{ + err: isMockedPermissionCheckErr, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), } - got, err := r.DeactivateUserGrant(tt.args.ctx, tt.args.userGrantID, tt.args.resourceOwner) + got, err := r.DeactivateUserGrant(tt.args.ctx, tt.args.userGrantID, tt.args.resourceOwner, tt.args.check) if tt.res.err == nil { assert.NoError(t, err) } @@ -1427,12 +2156,13 @@ func TestCommandSide_DeactivateUserGrant(t *testing.T) { func TestCommandSide_ReactivateUserGrant(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore } type args struct { ctx context.Context userGrantID string resourceOwner string + check UserGrantPermissionCheck } type res struct { want *domain.ObjectDetails @@ -1447,12 +2177,10 @@ func TestCommandSide_ReactivateUserGrant(t *testing.T) { { name: "invalid usergrantID, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ - ctx: context.Background(), + ctx: authz.NewMockContextWithPermissions("", "", "", []string{domain.RoleProjectOwner}), resourceOwner: "org1", }, res: res{ @@ -1460,25 +2188,43 @@ func TestCommandSide_ReactivateUserGrant(t *testing.T) { }, }, { - name: "invalid resourceOwner, error", + name: "not provided resourceOwner, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + usergrant.NewUserGrantAddedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org").Aggregate, + "user1", + "project1", + "", []string{"rolekey1"}), + ), + eventFromEventPusher( + usergrant.NewUserGrantDeactivatedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org").Aggregate), + ), + ), + expectPush( + usergrant.NewUserGrantReactivatedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org").Aggregate, + ), + ), ), }, args: args{ - ctx: context.Background(), + ctx: authz.NewMockContextWithPermissions("", "", "", []string{domain.RoleProjectOwner}), userGrantID: "usergrant1", }, res: res{ - err: zerrors.IsErrorInvalidArgument, + want: &domain.ObjectDetails{ + ResourceOwner: "org", + }, }, }, { name: "usergrant not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -1494,8 +2240,7 @@ func TestCommandSide_ReactivateUserGrant(t *testing.T) { { name: "usergrant removed, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( usergrant.NewUserGrantAddedEvent(context.Background(), @@ -1524,10 +2269,9 @@ func TestCommandSide_ReactivateUserGrant(t *testing.T) { }, }, { - name: "no permissions, permisison denied error", + name: "no permissions, permission denied error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( usergrant.NewUserGrantAddedEvent(context.Background(), @@ -1553,10 +2297,9 @@ func TestCommandSide_ReactivateUserGrant(t *testing.T) { }, }, { - name: "already active, precondition error", + name: "already active, ignore, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( usergrant.NewUserGrantAddedEvent(context.Background(), @@ -1569,19 +2312,20 @@ func TestCommandSide_ReactivateUserGrant(t *testing.T) { ), }, args: args{ - ctx: context.Background(), + ctx: authz.NewMockContextWithPermissions("", "", "", []string{domain.RoleProjectOwner}), userGrantID: "usergrant1", resourceOwner: "org1", }, res: res{ - err: zerrors.IsPreconditionFailed, + want: &domain.ObjectDetails{ + ResourceOwner: "org", + }, }, }, { name: "reactivated, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( usergrant.NewUserGrantAddedEvent(context.Background(), @@ -1613,13 +2357,78 @@ func TestCommandSide_ReactivateUserGrant(t *testing.T) { }, }, }, + { + name: "with passed succeeding permission check, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + usergrant.NewUserGrantAddedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + "user1", + "project1", + "", []string{"rolekey1"}), + ), + eventFromEventPusher( + usergrant.NewUserGrantDeactivatedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org").Aggregate), + ), + ), + expectPush( + usergrant.NewUserGrantReactivatedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + userGrantID: "usergrant1", + resourceOwner: "org1", + check: succeedingUserGrantPermissionCheck, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "with passed failing permission check, error", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + usergrant.NewUserGrantAddedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + "user1", + "project1", + "", []string{"rolekey1"}), + ), + eventFromEventPusher( + usergrant.NewUserGrantDeactivatedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org").Aggregate), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + userGrantID: "usergrant1", + resourceOwner: "org1", + check: failingUserGrantPermissionCheck, + }, + res: res{ + err: isMockedPermissionCheckErr, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), } - got, err := r.ReactivateUserGrant(tt.args.ctx, tt.args.userGrantID, tt.args.resourceOwner) + got, err := r.ReactivateUserGrant(tt.args.ctx, tt.args.userGrantID, tt.args.resourceOwner, tt.args.check) if tt.res.err == nil { assert.NoError(t, err) } @@ -1635,12 +2444,14 @@ func TestCommandSide_ReactivateUserGrant(t *testing.T) { func TestCommandSide_RemoveUserGrant(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore } type args struct { - ctx context.Context - userGrantID string - resourceOwner string + ctx context.Context + userGrantID string + resourceOwner string + ignoreNotFound bool + check UserGrantPermissionCheck } type res struct { want *domain.ObjectDetails @@ -1655,12 +2466,10 @@ func TestCommandSide_RemoveUserGrant(t *testing.T) { { name: "invalid usergrantID, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ - ctx: context.Background(), + ctx: authz.NewMockContextWithPermissions("", "", "", []string{domain.RoleProjectOwner}), resourceOwner: "org1", }, res: res{ @@ -1670,8 +2479,7 @@ func TestCommandSide_RemoveUserGrant(t *testing.T) { { name: "usergrant not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -1684,11 +2492,29 @@ func TestCommandSide_RemoveUserGrant(t *testing.T) { err: zerrors.IsNotFound, }, }, + { + name: "usergrant not existing, ignore, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter(), + ), + }, + args: args{ + ctx: authz.NewMockContextWithPermissions("", "", "", []string{domain.RoleProjectOwner}), + userGrantID: "usergrant1", + resourceOwner: "org1", + ignoreNotFound: true, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, { name: "usergrant removed, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( usergrant.NewUserGrantAddedEvent(context.Background(), @@ -1717,10 +2543,9 @@ func TestCommandSide_RemoveUserGrant(t *testing.T) { }, }, { - name: "no permissions, permisison denied error", + name: "no permissions, permission denied error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( usergrant.NewUserGrantAddedEvent(context.Background(), @@ -1748,8 +2573,7 @@ func TestCommandSide_RemoveUserGrant(t *testing.T) { { name: "remove usergrant project, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( usergrant.NewUserGrantAddedEvent(context.Background(), @@ -1780,11 +2604,43 @@ func TestCommandSide_RemoveUserGrant(t *testing.T) { }, }, }, + { + name: "not provided resourceOwner, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + usergrant.NewUserGrantAddedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + "user1", + "project1", + "", []string{"rolekey1"}), + ), + ), + expectPush( + usergrant.NewUserGrantRemovedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + "user1", + "project1", + "", + ), + ), + ), + }, + args: args{ + ctx: authz.NewMockContextWithPermissions("", "", "", []string{domain.RoleProjectOwner}), + userGrantID: "usergrant1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, { name: "remove usergrant projectgrant, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( usergrant.NewUserGrantAddedEvent(context.Background(), @@ -1815,13 +2671,73 @@ func TestCommandSide_RemoveUserGrant(t *testing.T) { }, }, }, + { + name: "with passed succeeding permission check, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + usergrant.NewUserGrantAddedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + "user1", + "project1", + "projectgrant1", []string{"rolekey1"}), + ), + ), + expectPush( + usergrant.NewUserGrantRemovedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + "user1", + "project1", + "projectgrant1", + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + userGrantID: "usergrant1", + resourceOwner: "org1", + check: succeedingUserGrantPermissionCheck, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "with passed failing permission check, error", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + usergrant.NewUserGrantAddedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + "user1", + "project1", + "projectgrant1", []string{"rolekey1"}), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + userGrantID: "usergrant1", + resourceOwner: "org1", + check: failingUserGrantPermissionCheck, + }, + res: res{ + err: isMockedPermissionCheckErr, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), } - got, err := r.RemoveUserGrant(tt.args.ctx, tt.args.userGrantID, tt.args.resourceOwner) + got, err := r.RemoveUserGrant(tt.args.ctx, tt.args.userGrantID, tt.args.resourceOwner, tt.args.ignoreNotFound, tt.args.check) if tt.res.err == nil { assert.NoError(t, err) } @@ -1857,9 +2773,7 @@ func TestCommandSide_BulkRemoveUserGrant(t *testing.T) { { name: "empty usergrantid list, error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: eventstoreExpect(t), }, args: args{ ctx: context.Background(), diff --git a/internal/command/user_human.go b/internal/command/user_human.go index 9e6ba43629..07628b9e19 100644 --- a/internal/command/user_human.go +++ b/internal/command/user_human.go @@ -428,7 +428,7 @@ func (h *AddHuman) shouldAddInitCode() bool { } // Deprecated: use commands.AddUserHuman -func (c *Commands) ImportHuman(ctx context.Context, orgID string, human *domain.Human, passwordless bool, links []*domain.UserIDPLink, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessCodeGenerator crypto.Generator) (_ *domain.Human, passwordlessCode *domain.PasswordlessInitCode, err error) { +func (c *Commands) ImportHuman(ctx context.Context, orgID string, human *domain.Human, passwordless bool, state *domain.UserState, links []*domain.UserIDPLink, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessCodeGenerator crypto.Generator) (_ *domain.Human, passwordlessCode *domain.PasswordlessInitCode, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -455,10 +455,32 @@ func (c *Commands) ImportHuman(ctx context.Context, orgID string, human *domain. } } - events, addedHuman, addedCode, code, err := c.importHuman(ctx, orgID, human, passwordless, links, domainPolicy, pwPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessCodeGenerator) + events, userAgg, addedHuman, addedCode, code, err := c.importHuman(ctx, orgID, human, passwordless, links, domainPolicy, pwPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessCodeGenerator) if err != nil { return nil, nil, err } + if state != nil { + var event eventstore.Command + switch *state { + case domain.UserStateInactive: + event = user.NewUserDeactivatedEvent(ctx, userAgg) + case domain.UserStateLocked: + event = user.NewUserLockedEvent(ctx, userAgg) + case domain.UserStateDeleted: + // users are never imported if deleted + case domain.UserStateActive: + // added because of the linter + case domain.UserStateSuspend: + // added because of the linter + case domain.UserStateInitial: + // added because of the linter + case domain.UserStateUnspecified: + // added because of the linter + } + if event != nil { + events = append(events, event) + } + } pushedEvents, err := c.eventstore.Push(ctx, events...) if err != nil { return nil, nil, err @@ -479,48 +501,48 @@ func (c *Commands) ImportHuman(ctx context.Context, orgID string, human *domain. return writeModelToHuman(addedHuman), passwordlessCode, nil } -func (c *Commands) importHuman(ctx context.Context, orgID string, human *domain.Human, passwordless bool, links []*domain.UserIDPLink, domainPolicy *domain.DomainPolicy, pwPolicy *domain.PasswordComplexityPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessCodeGenerator crypto.Generator) (events []eventstore.Command, humanWriteModel *HumanWriteModel, passwordlessCodeWriteModel *HumanPasswordlessInitCodeWriteModel, code string, err error) { +func (c *Commands) importHuman(ctx context.Context, orgID string, human *domain.Human, passwordless bool, links []*domain.UserIDPLink, domainPolicy *domain.DomainPolicy, pwPolicy *domain.PasswordComplexityPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator, passwordlessCodeGenerator crypto.Generator) (events []eventstore.Command, userAgg *eventstore.Aggregate, humanWriteModel *HumanWriteModel, passwordlessCodeWriteModel *HumanPasswordlessInitCodeWriteModel, code string, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() if orgID == "" { - return nil, nil, nil, "", zerrors.ThrowInvalidArgument(nil, "COMMAND-00p2b", "Errors.Org.Empty") + return nil, nil, nil, nil, "", zerrors.ThrowInvalidArgument(nil, "COMMAND-00p2b", "Errors.Org.Empty") } if err = human.Normalize(); err != nil { - return nil, nil, nil, "", err + return nil, nil, nil, nil, "", err } - events, humanWriteModel, err = c.createHuman(ctx, orgID, human, links, passwordless, domainPolicy, pwPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator) + events, userAgg, humanWriteModel, err = c.createHuman(ctx, orgID, human, links, passwordless, domainPolicy, pwPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator) if err != nil { - return nil, nil, nil, "", err + return nil, nil, nil, nil, "", err } if passwordless { var codeEvent eventstore.Command codeEvent, passwordlessCodeWriteModel, code, err = c.humanAddPasswordlessInitCode(ctx, human.AggregateID, orgID, true, passwordlessCodeGenerator) if err != nil { - return nil, nil, nil, "", err + return nil, nil, nil, nil, "", err } events = append(events, codeEvent) } - return events, humanWriteModel, passwordlessCodeWriteModel, code, nil + return events, userAgg, humanWriteModel, passwordlessCodeWriteModel, code, nil } -func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain.Human, links []*domain.UserIDPLink, passwordless bool, domainPolicy *domain.DomainPolicy, pwPolicy *domain.PasswordComplexityPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator crypto.Generator) (events []eventstore.Command, addedHuman *HumanWriteModel, err error) { +func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain.Human, links []*domain.UserIDPLink, passwordless bool, domainPolicy *domain.DomainPolicy, pwPolicy *domain.PasswordComplexityPolicy, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator crypto.Generator) (events []eventstore.Command, userAgg *eventstore.Aggregate, addedHuman *HumanWriteModel, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() if err = human.CheckDomainPolicy(domainPolicy); err != nil { - return nil, nil, err + return nil, nil, nil, err } human.Username = strings.TrimSpace(human.Username) human.EmailAddress = human.EmailAddress.Normalize() if err = c.userValidateDomain(ctx, orgID, human.Username, domainPolicy.UserLoginMustBeDomain); err != nil { - return nil, nil, err + return nil, nil, nil, err } if human.AggregateID == "" { userID, err := c.idGenerator.Next() if err != nil { - return nil, nil, err + return nil, nil, nil, err } human.AggregateID = userID } @@ -528,20 +550,21 @@ func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain. human.EnsureDisplayName() if human.Password != nil { if err := human.HashPasswordIfExisting(ctx, pwPolicy, c.userPasswordHasher, human.Password.ChangeRequired); err != nil { - return nil, nil, err + return nil, nil, nil, err } } addedHuman = NewHumanWriteModel(human.AggregateID, orgID) - //TODO: adlerhurst maybe we could simplify the code below - userAgg := UserAggregateFromWriteModel(&addedHuman.WriteModel) + + // TODO: adlerhurst maybe we could simplify the code below + userAgg = UserAggregateFromWriteModelCtx(ctx, &addedHuman.WriteModel) events = append(events, createAddHumanEvent(ctx, userAgg, human, domainPolicy.UserLoginMustBeDomain)) for _, link := range links { event, err := c.addUserIDPLink(ctx, userAgg, link, false) if err != nil { - return nil, nil, err + return nil, nil, nil, err } events = append(events, event) } @@ -549,7 +572,7 @@ func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain. if human.IsInitialState(passwordless, len(links) > 0) { initCode, err := domain.NewInitUserCode(initCodeGenerator) if err != nil { - return nil, nil, err + return nil, nil, nil, err } events = append(events, user.NewHumanInitialCodeAddedEvent(ctx, userAgg, initCode.Code, initCode.Expiry, "")) } else { @@ -558,7 +581,7 @@ func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain. } else { emailCode, _, err := domain.NewEmailCode(emailCodeGenerator) if err != nil { - return nil, nil, err + return nil, nil, nil, err } events = append(events, user.NewHumanEmailCodeAddedEvent(ctx, userAgg, emailCode.Code, emailCode.Expiry, "")) } @@ -567,14 +590,14 @@ func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain. if human.Phone != nil && human.PhoneNumber != "" && !human.IsPhoneVerified { phoneCode, generatorID, err := c.newPhoneCode(ctx, c.eventstore.Filter, domain.SecretGeneratorTypeVerifyPhoneCode, c.userEncryption, c.defaultSecretGenerators.PhoneVerificationCode) //nolint:staticcheck if err != nil { - return nil, nil, err + return nil, nil, nil, err } events = append(events, user.NewHumanPhoneCodeAddedEvent(ctx, userAgg, phoneCode.CryptedCode(), phoneCode.CodeExpiry(), generatorID)) } else if human.Phone != nil && human.PhoneNumber != "" && human.IsPhoneVerified { events = append(events, user.NewHumanPhoneVerifiedEvent(ctx, userAgg)) } - return events, addedHuman, nil + return events, userAgg, addedHuman, nil } func (c *Commands) HumanSkipMFAInit(ctx context.Context, userID, resourceowner string) (err error) { diff --git a/internal/command/user_human_otp.go b/internal/command/user_human_otp.go index 97596aabd8..fca762cbab 100644 --- a/internal/command/user_human_otp.go +++ b/internal/command/user_human_otp.go @@ -26,7 +26,7 @@ func (c *Commands) ImportHumanTOTP(ctx context.Context, userID, userAgentID, res if err != nil { return err } - if err = c.checkUserExists(ctx, userID, resourceOwner); err != nil { + if _, err = c.checkUserExists(ctx, userID, resourceOwner); err != nil { return err } diff --git a/internal/command/user_human_test.go b/internal/command/user_human_test.go index 78d7248516..1ef3e2aab6 100644 --- a/internal/command/user_human_test.go +++ b/internal/command/user_human_test.go @@ -1200,7 +1200,8 @@ func TestCommandSide_AddHuman(t *testing.T) { }, wantID: "user1", }, - }, { + }, + { name: "add human (with return code), ok", fields: fields{ eventstore: expectEventstore( @@ -1432,6 +1433,7 @@ func TestCommandSide_ImportHuman(t *testing.T) { orgID string human *domain.Human passwordless bool + state *domain.UserState links []*domain.UserIDPLink secretGenerator crypto.Generator passwordlessInitCode crypto.Generator @@ -1584,7 +1586,8 @@ func TestCommandSide_ImportHuman(t *testing.T) { res: res{ err: zerrors.IsErrorInvalidArgument, }, - }, { + }, + { name: "add human (with password and initial code), ok", given: func(t *testing.T) (fields, args) { return fields{ @@ -2985,6 +2988,364 @@ func TestCommandSide_ImportHuman(t *testing.T) { }, }, }, + { + name: "add human (with idp, auto creation not allowed) + deactivated state, ok", + given: func(t *testing.T) (fields, args) { + return fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + true, + true, + true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewPasswordComplexityPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + 1, + false, + false, + false, + false, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewIDPConfigAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "idpID", + "name", + domain.IDPConfigTypeOIDC, + domain.IDPConfigStylingTypeUnspecified, + false, + ), + ), + eventFromEventPusher( + org.NewIDPOIDCConfigAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "clientID", + "idpID", + "issuer", + "authEndpoint", + "tokenEndpoint", + nil, + domain.OIDCMappingFieldUnspecified, + domain.OIDCMappingFieldUnspecified, + ), + ), + eventFromEventPusher( + func() eventstore.Command { + e, _ := org.NewOIDCIDPChangedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "config1", + []idp.OIDCIDPChanges{ + idp.ChangeOIDCOptions(idp.OptionChanges{IsCreationAllowed: gu.Ptr(false)}), + }, + ) + return e + }(), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewIDPConfigAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "idpID", + "name", + domain.IDPConfigTypeOIDC, + domain.IDPConfigStylingTypeUnspecified, + false, + ), + ), + eventFromEventPusher( + org.NewIDPOIDCConfigAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "clientID", + "idpID", + "issuer", + "authEndpoint", + "tokenEndpoint", + nil, + domain.OIDCMappingFieldUnspecified, + domain.OIDCMappingFieldUnspecified, + ), + ), + eventFromEventPusher( + func() eventstore.Command { + e, _ := org.NewOIDCIDPChangedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "config1", + []idp.OIDCIDPChanges{ + idp.ChangeOIDCOptions(idp.OptionChanges{ + IsCreationAllowed: gu.Ptr(true), + IsAutoCreation: gu.Ptr(false), + }), + }, + ) + return e + }(), + ), + eventFromEventPusher( + org.NewIdentityProviderAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "idpID", + domain.IdentityProviderTypeOrg, + ), + ), + ), + expectPush( + newAddHumanEvent("", false, true, "", AllowedLanguage), + user.NewUserIDPLinkAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "idpID", + "name", + "externalID", + ), + user.NewHumanEmailVerifiedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate), + user.NewUserDeactivatedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + ), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + userPasswordHasher: mockPasswordHasher("x"), + }, + args{ + ctx: context.Background(), + orgID: "org1", + state: func() *domain.UserState { + state := domain.UserStateInactive + return &state + }(), + human: &domain.Human{ + Username: "username", + Profile: &domain.Profile{ + FirstName: "firstname", + LastName: "lastname", + PreferredLanguage: AllowedLanguage, + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + IsEmailVerified: true, + }, + }, + links: []*domain.UserIDPLink{ + { + IDPConfigID: "idpID", + ExternalUserID: "externalID", + DisplayName: "name", + }, + }, + secretGenerator: GetMockSecretGenerator(t), + } + }, + res: res{ + wantHuman: &domain.Human{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + ResourceOwner: "org1", + }, + Username: "username", + Profile: &domain.Profile{ + FirstName: "firstname", + LastName: "lastname", + DisplayName: "firstname lastname", + PreferredLanguage: AllowedLanguage, + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + IsEmailVerified: true, + }, + State: domain.UserStateInactive, + }, + }, + }, + { + name: "add human (with idp, auto creation not allowed) + locked state, ok", + given: func(t *testing.T) (fields, args) { + return fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + true, + true, + true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewPasswordComplexityPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + 1, + false, + false, + false, + false, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewIDPConfigAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "idpID", + "name", + domain.IDPConfigTypeOIDC, + domain.IDPConfigStylingTypeUnspecified, + false, + ), + ), + eventFromEventPusher( + org.NewIDPOIDCConfigAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "clientID", + "idpID", + "issuer", + "authEndpoint", + "tokenEndpoint", + nil, + domain.OIDCMappingFieldUnspecified, + domain.OIDCMappingFieldUnspecified, + ), + ), + eventFromEventPusher( + func() eventstore.Command { + e, _ := org.NewOIDCIDPChangedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "config1", + []idp.OIDCIDPChanges{ + idp.ChangeOIDCOptions(idp.OptionChanges{IsCreationAllowed: gu.Ptr(false)}), + }, + ) + return e + }(), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewIDPConfigAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "idpID", + "name", + domain.IDPConfigTypeOIDC, + domain.IDPConfigStylingTypeUnspecified, + false, + ), + ), + eventFromEventPusher( + org.NewIDPOIDCConfigAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "clientID", + "idpID", + "issuer", + "authEndpoint", + "tokenEndpoint", + nil, + domain.OIDCMappingFieldUnspecified, + domain.OIDCMappingFieldUnspecified, + ), + ), + eventFromEventPusher( + func() eventstore.Command { + e, _ := org.NewOIDCIDPChangedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "config1", + []idp.OIDCIDPChanges{ + idp.ChangeOIDCOptions(idp.OptionChanges{ + IsCreationAllowed: gu.Ptr(true), + IsAutoCreation: gu.Ptr(false), + }), + }, + ) + return e + }(), + ), + eventFromEventPusher( + org.NewIdentityProviderAddedEvent(context.Background(), + &org.NewAggregate("org1").Aggregate, + "idpID", + domain.IdentityProviderTypeOrg, + ), + ), + ), + expectPush( + newAddHumanEvent("", false, true, "", AllowedLanguage), + user.NewUserIDPLinkAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "idpID", + "name", + "externalID", + ), + user.NewHumanEmailVerifiedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate), + user.NewUserLockedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + ), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + userPasswordHasher: mockPasswordHasher("x"), + }, + args{ + ctx: context.Background(), + orgID: "org1", + state: func() *domain.UserState { + state := domain.UserStateLocked + return &state + }(), + human: &domain.Human{ + Username: "username", + Profile: &domain.Profile{ + FirstName: "firstname", + LastName: "lastname", + PreferredLanguage: AllowedLanguage, + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + IsEmailVerified: true, + }, + }, + links: []*domain.UserIDPLink{ + { + IDPConfigID: "idpID", + ExternalUserID: "externalID", + DisplayName: "name", + }, + }, + secretGenerator: GetMockSecretGenerator(t), + } + }, + res: res{ + wantHuman: &domain.Human{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + ResourceOwner: "org1", + }, + Username: "username", + Profile: &domain.Profile{ + FirstName: "firstname", + LastName: "lastname", + DisplayName: "firstname lastname", + PreferredLanguage: AllowedLanguage, + }, + Email: &domain.Email{ + EmailAddress: "email@test.ch", + IsEmailVerified: true, + }, + State: domain.UserStateLocked, + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -2996,7 +3357,7 @@ func TestCommandSide_ImportHuman(t *testing.T) { newEncryptedCodeWithDefault: f.newEncryptedCodeWithDefault, defaultSecretGenerators: f.defaultSecretGenerators, } - gotHuman, gotCode, err := r.ImportHuman(a.ctx, a.orgID, a.human, a.passwordless, a.links, a.secretGenerator, a.secretGenerator, a.secretGenerator, a.secretGenerator) + gotHuman, gotCode, err := r.ImportHuman(a.ctx, a.orgID, a.human, a.passwordless, a.state, a.links, a.secretGenerator, a.secretGenerator, a.secretGenerator, a.secretGenerator) if tt.res.err == nil { assert.NoError(t, err) } diff --git a/internal/command/user_idp_link.go b/internal/command/user_idp_link.go index 432d2e0b90..e3484c9753 100644 --- a/internal/command/user_idp_link.go +++ b/internal/command/user_idp_link.go @@ -56,7 +56,7 @@ func (c *Commands) BulkAddedUserIDPLinks(ctx context.Context, userID, resourceOw return zerrors.ThrowInvalidArgument(nil, "COMMAND-Ek9s", "Errors.User.ExternalIDP.MinimumExternalIDPNeeded") } - if err := c.checkUserExists(ctx, userID, resourceOwner); err != nil { + if _, err := c.checkUserExists(ctx, userID, resourceOwner); err != nil { return err } diff --git a/internal/command/user_machine.go b/internal/command/user_machine.go index 1ec32450ac..75ed43ee69 100644 --- a/internal/command/user_machine.go +++ b/internal/command/user_machine.go @@ -25,6 +25,7 @@ type Machine struct { Name string Description string AccessTokenType domain.OIDCTokenType + PermissionCheck PermissionCheck } func (m *Machine) IsZero() bool { @@ -33,8 +34,8 @@ func (m *Machine) IsZero() bool { func AddMachineCommand(a *user.Aggregate, machine *Machine) preparation.Validation { return func() (_ preparation.CreateCommands, err error) { - if a.ResourceOwner == "" { - return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-xiown2", "Errors.ResourceOwnerMissing") + if a.ResourceOwner == "" && machine.PermissionCheck == nil { + return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-xiown3", "Errors.ResourceOwnerMissing") } if a.ID == "" { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-p0p2mi", "Errors.User.UserIDMissing") @@ -49,7 +50,7 @@ func AddMachineCommand(a *user.Aggregate, machine *Machine) preparation.Validati ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - writeModel, err := getMachineWriteModel(ctx, a.ID, a.ResourceOwner, filter) + writeModel, err := getMachineWriteModel(ctx, a.ID, a.ResourceOwner, filter, machine.PermissionCheck) if err != nil { return nil, err } @@ -67,7 +68,18 @@ func AddMachineCommand(a *user.Aggregate, machine *Machine) preparation.Validati } } -func (c *Commands) AddMachine(ctx context.Context, machine *Machine) (_ *domain.ObjectDetails, err error) { +type addMachineOption func(context.Context, *Machine) error + +func AddMachineWithUsernameToIDFallback() addMachineOption { + return func(ctx context.Context, m *Machine) error { + if m.Username == "" { + m.Username = m.AggregateID + } + return nil + } +} + +func (c *Commands) AddMachine(ctx context.Context, machine *Machine, state *domain.UserState, check PermissionCheck, options ...addMachineOption) (_ *domain.ObjectDetails, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -80,11 +92,44 @@ func (c *Commands) AddMachine(ctx context.Context, machine *Machine) (_ *domain. } agg := user.NewAggregate(machine.AggregateID, machine.ResourceOwner) + for _, option := range options { + if err = option(ctx, machine); err != nil { + return nil, err + } + } + if check != nil { + if err = check(machine.ResourceOwner, machine.AggregateID); err != nil { + return nil, err + } + } cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, AddMachineCommand(agg, machine)) if err != nil { return nil, err } + if state != nil { + var cmd eventstore.Command + switch *state { + case domain.UserStateInactive: + cmd = user.NewUserDeactivatedEvent(ctx, &agg.Aggregate) + case domain.UserStateLocked: + cmd = user.NewUserLockedEvent(ctx, &agg.Aggregate) + case domain.UserStateDeleted: + // users are never imported if deleted + case domain.UserStateActive: + // added because of the linter + case domain.UserStateSuspend: + // added because of the linter + case domain.UserStateInitial: + // added because of the linter + case domain.UserStateUnspecified: + // added because of the linter + } + if cmd != nil { + cmds = append(cmds, cmd) + } + } + events, err := c.eventstore.Push(ctx, cmds...) if err != nil { return nil, err @@ -97,6 +142,7 @@ func (c *Commands) AddMachine(ctx context.Context, machine *Machine) (_ *domain. }, nil } +// Deprecated: use ChangeUserMachine instead func (c *Commands) ChangeMachine(ctx context.Context, machine *Machine) (*domain.ObjectDetails, error) { agg := user.NewAggregate(machine.AggregateID, machine.ResourceOwner) cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, changeMachineCommand(agg, machine)) @@ -118,24 +164,21 @@ func (c *Commands) ChangeMachine(ctx context.Context, machine *Machine) (*domain func changeMachineCommand(a *user.Aggregate, machine *Machine) preparation.Validation { return func() (_ preparation.CreateCommands, err error) { - if a.ResourceOwner == "" { + if a.ResourceOwner == "" && machine.PermissionCheck == nil { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-xiown3", "Errors.ResourceOwnerMissing") } if a.ID == "" { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-p0p3mi", "Errors.User.UserIDMissing") } return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { - writeModel, err := getMachineWriteModel(ctx, a.ID, a.ResourceOwner, filter) + writeModel, err := getMachineWriteModel(ctx, a.ID, a.ResourceOwner, filter, machine.PermissionCheck) if err != nil { return nil, err } if !isUserStateExists(writeModel.UserState) { return nil, zerrors.ThrowNotFound(nil, "COMMAND-5M0od", "Errors.User.NotFound") } - changedEvent, hasChanged, err := writeModel.NewChangedEvent(ctx, &a.Aggregate, machine.Name, machine.Description, machine.AccessTokenType) - if err != nil { - return nil, err - } + changedEvent, hasChanged := writeModel.NewChangedEvent(ctx, &a.Aggregate, machine.Name, machine.Description, machine.AccessTokenType) if !hasChanged { return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-2n8vs", "Errors.User.NotChanged") } @@ -147,10 +190,9 @@ func changeMachineCommand(a *user.Aggregate, machine *Machine) preparation.Valid } } -func getMachineWriteModel(ctx context.Context, userID, resourceOwner string, filter preparation.FilterToQueryReducer) (_ *MachineWriteModel, err error) { +func getMachineWriteModel(ctx context.Context, userID, resourceOwner string, filter preparation.FilterToQueryReducer, permissionCheck PermissionCheck) (_ *MachineWriteModel, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - writeModel := NewMachineWriteModel(userID, resourceOwner) events, err := filter(ctx, writeModel.Query()) if err != nil { @@ -161,5 +203,10 @@ func getMachineWriteModel(ctx context.Context, userID, resourceOwner string, fil } writeModel.AppendEvents(events...) err = writeModel.Reduce() + if permissionCheck != nil { + if err := permissionCheck(writeModel.ResourceOwner, writeModel.AggregateID); err != nil { + return nil, err + } + } return writeModel, err } diff --git a/internal/command/user_machine_key.go b/internal/command/user_machine_key.go index 8a0f0f437b..d628bf4c2d 100644 --- a/internal/command/user_machine_key.go +++ b/internal/command/user_machine_key.go @@ -15,12 +15,14 @@ import ( ) type AddMachineKey struct { - Type domain.AuthNKeyType - ExpirationDate time.Time + Type domain.AuthNKeyType + ExpirationDate time.Time + PermissionCheck PermissionCheck } type MachineKey struct { models.ObjectRoot + PermissionCheck PermissionCheck KeyID string Type domain.AuthNKeyType @@ -64,7 +66,7 @@ func (key *MachineKey) Detail() ([]byte, error) { } func (key *MachineKey) content() error { - if key.ResourceOwner == "" { + if key.PermissionCheck == nil && key.ResourceOwner == "" { return zerrors.ThrowInvalidArgument(nil, "COMMAND-kqpoix", "Errors.ResourceOwnerMissing") } if key.AggregateID == "" { @@ -91,7 +93,7 @@ func (key *MachineKey) valid() (err error) { } func (key *MachineKey) checkAggregate(ctx context.Context, filter preparation.FilterToQueryReducer) error { - if exists, err := ExistsUser(ctx, filter, key.AggregateID, key.ResourceOwner); err != nil || !exists { + if exists, err := ExistsUser(ctx, filter, key.AggregateID, key.ResourceOwner, true); err != nil || !exists { return zerrors.ThrowPreconditionFailed(err, "COMMAND-bnipwm1", "Errors.User.NotFound") } return nil @@ -142,7 +144,7 @@ func prepareAddUserMachineKey(machineKey *MachineKey, keySize int) preparation.V return nil, err } } - writeModel, err := getMachineKeyWriteModelByID(ctx, filter, machineKey.AggregateID, machineKey.KeyID, machineKey.ResourceOwner) + writeModel, err := getMachineKeyWriteModelByID(ctx, filter, machineKey.AggregateID, machineKey.KeyID, machineKey.ResourceOwner, machineKey.PermissionCheck) if err != nil { return nil, err } @@ -186,7 +188,7 @@ func prepareRemoveUserMachineKey(machineKey *MachineKey) preparation.Validation return nil, err } return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { - writeModel, err := getMachineKeyWriteModelByID(ctx, filter, machineKey.AggregateID, machineKey.KeyID, machineKey.ResourceOwner) + writeModel, err := getMachineKeyWriteModelByID(ctx, filter, machineKey.AggregateID, machineKey.KeyID, machineKey.ResourceOwner, machineKey.PermissionCheck) if err != nil { return nil, err } @@ -204,16 +206,18 @@ func prepareRemoveUserMachineKey(machineKey *MachineKey) preparation.Validation } } -func getMachineKeyWriteModelByID(ctx context.Context, filter preparation.FilterToQueryReducer, userID, keyID, resourceOwner string) (_ *MachineKeyWriteModel, err error) { +func getMachineKeyWriteModelByID(ctx context.Context, filter preparation.FilterToQueryReducer, userID, keyID, resourceOwner string, permissionCheck PermissionCheck) (_ *MachineKeyWriteModel, err error) { writeModel := NewMachineKeyWriteModel(userID, keyID, resourceOwner) events, err := filter(ctx, writeModel.Query()) if err != nil { return nil, err } - if len(events) == 0 { - return writeModel, nil - } writeModel.AppendEvents(events...) err = writeModel.Reduce() + if permissionCheck != nil { + if err := permissionCheck(writeModel.ResourceOwner, writeModel.AggregateID); err != nil { + return nil, err + } + } return writeModel, err } diff --git a/internal/command/user_machine_model.go b/internal/command/user_machine_model.go index b7dfb02d32..1ed6c8ca58 100644 --- a/internal/command/user_machine_model.go +++ b/internal/command/user_machine_model.go @@ -2,7 +2,6 @@ package command import ( "context" - "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" @@ -106,9 +105,8 @@ func (wm *MachineWriteModel) NewChangedEvent( name, description string, accessTokenType domain.OIDCTokenType, -) (*user.MachineChangedEvent, bool, error) { +) (*user.MachineChangedEvent, bool) { changes := make([]user.MachineChanges, 0) - var err error if wm.Name != name { changes = append(changes, user.ChangeName(name)) @@ -120,11 +118,8 @@ func (wm *MachineWriteModel) NewChangedEvent( changes = append(changes, user.ChangeAccessTokenType(accessTokenType)) } if len(changes) == 0 { - return nil, false, nil + return nil, false } - changeEvent, err := user.NewMachineChangedEvent(ctx, aggregate, changes) - if err != nil { - return nil, false, err - } - return changeEvent, true, nil + changeEvent := user.NewMachineChangedEvent(ctx, aggregate, changes) + return changeEvent, true } diff --git a/internal/command/user_machine_secret.go b/internal/command/user_machine_secret.go index 3349fc90a5..34e9c0c5cc 100644 --- a/internal/command/user_machine_secret.go +++ b/internal/command/user_machine_secret.go @@ -11,7 +11,8 @@ import ( ) type GenerateMachineSecret struct { - ClientSecret string + PermissionCheck PermissionCheck + ClientSecret string } func (c *Commands) GenerateMachineSecret(ctx context.Context, userID string, resourceOwner string, set *GenerateMachineSecret) (*domain.ObjectDetails, error) { @@ -35,14 +36,14 @@ func (c *Commands) GenerateMachineSecret(ctx context.Context, userID string, res func (c *Commands) prepareGenerateMachineSecret(a *user.Aggregate, set *GenerateMachineSecret) preparation.Validation { return func() (_ preparation.CreateCommands, err error) { - if a.ResourceOwner == "" { - return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-x0992n", "Errors.ResourceOwnerMissing") + if a.ResourceOwner == "" && set.PermissionCheck == nil { + return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-0qp2hus", "Errors.ResourceOwnerMissing") } if a.ID == "" { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-bzoqjs", "Errors.User.UserIDMissing") } return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { - writeModel, err := getMachineWriteModel(ctx, a.ID, a.ResourceOwner, filter) + writeModel, err := getMachineWriteModel(ctx, a.ID, a.ResourceOwner, filter, set.PermissionCheck) if err != nil { return nil, err } @@ -62,9 +63,10 @@ func (c *Commands) prepareGenerateMachineSecret(a *user.Aggregate, set *Generate } } -func (c *Commands) RemoveMachineSecret(ctx context.Context, userID string, resourceOwner string) (*domain.ObjectDetails, error) { +func (c *Commands) RemoveMachineSecret(ctx context.Context, userID string, resourceOwner string, permissionCheck PermissionCheck) (*domain.ObjectDetails, error) { agg := user.NewAggregate(userID, resourceOwner) - cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, prepareRemoveMachineSecret(agg)) + //nolint:staticcheck + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, prepareRemoveMachineSecret(agg, permissionCheck)) if err != nil { return nil, err } @@ -81,16 +83,16 @@ func (c *Commands) RemoveMachineSecret(ctx context.Context, userID string, resou }, nil } -func prepareRemoveMachineSecret(a *user.Aggregate) preparation.Validation { +func prepareRemoveMachineSecret(a *user.Aggregate, check PermissionCheck) preparation.Validation { return func() (_ preparation.CreateCommands, err error) { - if a.ResourceOwner == "" { - return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-0qp2hus", "Errors.ResourceOwnerMissing") + if a.ResourceOwner == "" && check == nil { + return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-x0992n", "Errors.ResourceOwnerMissing") } if a.ID == "" { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-bzosjs", "Errors.User.UserIDMissing") } return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { - writeModel, err := getMachineWriteModel(ctx, a.ID, a.ResourceOwner, filter) + writeModel, err := getMachineWriteModel(ctx, a.ID, a.ResourceOwner, filter, check) if err != nil { return nil, err } diff --git a/internal/command/user_machine_secret_test.go b/internal/command/user_machine_secret_test.go index 4c6d16960c..8e839efe07 100644 --- a/internal/command/user_machine_secret_test.go +++ b/internal/command/user_machine_secret_test.go @@ -44,7 +44,7 @@ func TestCommandSide_GenerateMachineSecret(t *testing.T) { ctx: context.Background(), userID: "", resourceOwner: "org1", - set: nil, + set: new(GenerateMachineSecret), }, res: res{ err: zerrors.IsErrorInvalidArgument, @@ -59,7 +59,7 @@ func TestCommandSide_GenerateMachineSecret(t *testing.T) { ctx: context.Background(), userID: "user1", resourceOwner: "", - set: nil, + set: new(GenerateMachineSecret), }, res: res{ err: zerrors.IsErrorInvalidArgument, @@ -76,7 +76,7 @@ func TestCommandSide_GenerateMachineSecret(t *testing.T) { ctx: context.Background(), userID: "user1", resourceOwner: "org1", - set: nil, + set: new(GenerateMachineSecret), }, res: res{ err: zerrors.IsPreconditionFailed, @@ -289,7 +289,7 @@ func TestCommandSide_RemoveMachineSecret(t *testing.T) { r := &Commands{ eventstore: tt.fields.eventstore, } - got, err := r.RemoveMachineSecret(tt.args.ctx, tt.args.userID, tt.args.resourceOwner) + got, err := r.RemoveMachineSecret(tt.args.ctx, tt.args.userID, tt.args.resourceOwner, nil) if tt.res.err == nil { assert.NoError(t, err) } diff --git a/internal/command/user_machine_test.go b/internal/command/user_machine_test.go index c7b4b8caf4..6d94154a42 100644 --- a/internal/command/user_machine_test.go +++ b/internal/command/user_machine_test.go @@ -24,6 +24,9 @@ func TestCommandSide_AddMachine(t *testing.T) { type args struct { ctx context.Context machine *Machine + state *domain.UserState + check PermissionCheck + options func(*Commands) []addMachineOption } type res struct { want *domain.ObjectDetails @@ -194,14 +197,348 @@ func TestCommandSide_AddMachine(t *testing.T) { }, }, }, + { + name: "with username fallback to given username", + fields: fields{ + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "aggregateID"), + eventstore: eventstoreExpect( + t, + expectFilter(), + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &user.NewAggregate("aggregateID", "org1").Aggregate, + true, + true, + true, + ), + ), + ), + expectPush( + user.NewMachineAddedEvent(context.Background(), + &user.NewAggregate("aggregateID", "org1").Aggregate, + "username", + "name", + "", + true, + domain.OIDCTokenTypeBearer, + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + machine: &Machine{ + ObjectRoot: models.ObjectRoot{ + ResourceOwner: "org1", + }, + Name: "name", + Username: "username", + }, + options: func(commands *Commands) []addMachineOption { + return []addMachineOption{ + AddMachineWithUsernameToIDFallback(), + } + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "with username fallback to generated id", + fields: fields{ + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "aggregateID"), + eventstore: eventstoreExpect( + t, + expectFilter(), + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &user.NewAggregate("aggregateID", "org1").Aggregate, + true, + true, + true, + ), + ), + ), + expectPush( + user.NewMachineAddedEvent(context.Background(), + &user.NewAggregate("aggregateID", "org1").Aggregate, + "aggregateID", + "name", + "", + true, + domain.OIDCTokenTypeBearer, + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + machine: &Machine{ + ObjectRoot: models.ObjectRoot{ + ResourceOwner: "org1", + }, + Name: "name", + }, + options: func(commands *Commands) []addMachineOption { + return []addMachineOption{ + AddMachineWithUsernameToIDFallback(), + } + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "with username fallback to given id", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &user.NewAggregate("aggregateID", "org1").Aggregate, + true, + true, + true, + ), + ), + ), + expectPush( + user.NewMachineAddedEvent(context.Background(), + &user.NewAggregate("aggregateID", "org1").Aggregate, + "aggregateID", + "name", + "", + true, + domain.OIDCTokenTypeBearer, + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + machine: &Machine{ + ObjectRoot: models.ObjectRoot{ + ResourceOwner: "org1", + AggregateID: "aggregateID", + }, + Name: "name", + }, + options: func(commands *Commands) []addMachineOption { + return []addMachineOption{ + AddMachineWithUsernameToIDFallback(), + } + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "with succeeding permission check, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + true, + true, + true, + ), + ), + ), + expectPush( + user.NewMachineAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "name", + "description", + true, + domain.OIDCTokenTypeBearer, + ), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + }, + args: args{ + ctx: context.Background(), + machine: &Machine{ + ObjectRoot: models.ObjectRoot{ + ResourceOwner: "org1", + }, + Description: "description", + Name: "name", + Username: "username", + }, + check: func(resourceOwner, aggregateID string) error { + return nil + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "with failing permission check, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + }, + args: args{ + ctx: context.Background(), + machine: &Machine{ + ObjectRoot: models.ObjectRoot{ + ResourceOwner: "org1", + }, + Description: "description", + Name: "name", + Username: "username", + }, + check: func(resourceOwner, aggregateID string) error { + return zerrors.ThrowPermissionDenied(nil, "", "") + }, + }, + res: res{ + err: zerrors.IsPermissionDenied, + }, + }, + { + name: "add machine, ok + deactive state", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + true, + true, + true, + ), + ), + ), + expectPush( + user.NewMachineAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "name", + "description", + true, + domain.OIDCTokenTypeBearer, + ), + user.NewUserDeactivatedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + ), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + }, + args: args{ + ctx: context.Background(), + machine: &Machine{ + ObjectRoot: models.ObjectRoot{ + ResourceOwner: "org1", + }, + Description: "description", + Name: "name", + Username: "username", + }, + state: func() *domain.UserState { + state := domain.UserStateInactive + return &state + }(), + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "add machine, ok + locked state", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + true, + true, + true, + ), + ), + ), + expectPush( + user.NewMachineAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "name", + "description", + true, + domain.OIDCTokenTypeBearer, + ), + user.NewUserLockedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + ), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + }, + args: args{ + ctx: context.Background(), + machine: &Machine{ + ObjectRoot: models.ObjectRoot{ + ResourceOwner: "org1", + }, + Description: "description", + Name: "name", + Username: "username", + }, + state: func() *domain.UserState { + state := domain.UserStateLocked + return &state + }(), + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, - idGenerator: tt.fields.idGenerator, + eventstore: tt.fields.eventstore, + idGenerator: tt.fields.idGenerator, + checkPermission: newMockPermissionCheckAllowed(), } - got, err := r.AddMachine(tt.args.ctx, tt.args.machine) + var options []addMachineOption + if tt.args.options != nil { + options = tt.args.options(r) + } + got, err := r.AddMachine(tt.args.ctx, tt.args.machine, tt.args.state, tt.args.check, options...) if tt.res.err == nil { assert.NoError(t, err) } @@ -391,7 +728,7 @@ func TestCommandSide_ChangeMachine(t *testing.T) { } func newMachineChangedEvent(ctx context.Context, userID, resourceOwner, name, description string) *user.MachineChangedEvent { - event, _ := user.NewMachineChangedEvent(ctx, + event := user.NewMachineChangedEvent(ctx, &user.NewAggregate(userID, resourceOwner).Aggregate, []user.MachineChanges{ user.ChangeName(name), diff --git a/internal/command/user_metadata.go b/internal/command/user_metadata.go index d47c5b61d0..294866e23b 100644 --- a/internal/command/user_metadata.go +++ b/internal/command/user_metadata.go @@ -1,6 +1,7 @@ package command import ( + "bytes" "context" "github.com/zitadel/zitadel/internal/domain" @@ -14,12 +15,25 @@ func (c *Commands) SetUserMetadata(ctx context.Context, metadata *domain.Metadat ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - err = c.checkUserExists(ctx, userID, resourceOwner) + userResourceOwner, err := c.checkUserExists(ctx, userID, resourceOwner) + if err != nil { + return nil, err + } + + if err := c.checkPermissionUpdateUser(ctx, userResourceOwner, userID); err != nil { + return nil, err + } + + setMetadata, err := c.getUserMetadataModelByID(ctx, userID, userResourceOwner, metadata.Key) if err != nil { return nil, err } - setMetadata := NewUserMetadataWriteModel(userID, resourceOwner, metadata.Key) userAgg := UserAggregateFromWriteModel(&setMetadata.WriteModel) + // return if no change in the metadata + if bytes.Equal(setMetadata.Value, metadata.Value) { + return writeModelToUserMetadata(setMetadata), nil + } + event, err := c.setUserMetadata(ctx, userAgg, metadata) if err != nil { return nil, err @@ -40,20 +54,35 @@ func (c *Commands) BulkSetUserMetadata(ctx context.Context, userID, resourceOwne if len(metadatas) == 0 { return nil, zerrors.ThrowPreconditionFailed(nil, "META-9mm2d", "Errors.Metadata.NoData") } - err = c.checkUserExists(ctx, userID, resourceOwner) + userResourceOwner, err := c.checkUserExists(ctx, userID, resourceOwner) if err != nil { return nil, err } - events := make([]eventstore.Command, len(metadatas)) - setMetadata := NewUserMetadataListWriteModel(userID, resourceOwner) + if err := c.checkPermissionUpdateUser(ctx, userResourceOwner, userID); err != nil { + return nil, err + } + + events := make([]eventstore.Command, 0) + setMetadata, err := c.getUserMetadataListModelByID(ctx, userID, userResourceOwner) + if err != nil { + return nil, err + } userAgg := UserAggregateFromWriteModel(&setMetadata.WriteModel) - for i, data := range metadatas { + for _, data := range metadatas { + // if no change to metadata no event has to be pushed + if existingValue, ok := setMetadata.metadataList[data.Key]; ok && bytes.Equal(existingValue, data.Value) { + continue + } event, err := c.setUserMetadata(ctx, userAgg, data) if err != nil { return nil, err } - events[i] = event + events = append(events, event) + } + // no changes for the metadata + if len(events) == 0 { + return writeModelToObjectDetails(&setMetadata.WriteModel), nil } pushedEvents, err := c.eventstore.Push(ctx, events...) @@ -84,11 +113,16 @@ func (c *Commands) RemoveUserMetadata(ctx context.Context, metadataKey, userID, if metadataKey == "" { return nil, zerrors.ThrowInvalidArgument(nil, "META-2n0fs", "Errors.Metadata.Invalid") } - err = c.checkUserExists(ctx, userID, resourceOwner) + userResourceOwner, err := c.checkUserExists(ctx, userID, resourceOwner) if err != nil { return nil, err } - removeMetadata, err := c.getUserMetadataModelByID(ctx, userID, resourceOwner, metadataKey) + + if err := c.checkPermissionUpdateUser(ctx, userResourceOwner, userID); err != nil { + return nil, err + } + + removeMetadata, err := c.getUserMetadataModelByID(ctx, userID, userResourceOwner, metadataKey) if err != nil { return nil, err } @@ -116,13 +150,17 @@ func (c *Commands) BulkRemoveUserMetadata(ctx context.Context, userID, resourceO if len(metadataKeys) == 0 { return nil, zerrors.ThrowPreconditionFailed(nil, "META-9mm2d", "Errors.Metadata.NoData") } - err = c.checkUserExists(ctx, userID, resourceOwner) + userResourceOwner, err := c.checkUserExists(ctx, userID, resourceOwner) if err != nil { return nil, err } + if err := c.checkPermissionUpdateUser(ctx, userResourceOwner, userID); err != nil { + return nil, err + } + events := make([]eventstore.Command, len(metadataKeys)) - removeMetadata, err := c.getUserMetadataListModelByID(ctx, userID, resourceOwner) + removeMetadata, err := c.getUserMetadataListModelByID(ctx, userID, userResourceOwner) if err != nil { return nil, err } @@ -153,24 +191,6 @@ func (c *Commands) BulkRemoveUserMetadata(ctx context.Context, userID, resourceO return writeModelToObjectDetails(&removeMetadata.WriteModel), nil } -func (c *Commands) removeUserMetadataFromOrg(ctx context.Context, resourceOwner string) ([]eventstore.Command, error) { - existingUserMetadata, err := c.getUserMetadataByOrgListModelByID(ctx, resourceOwner) - if err != nil { - return nil, err - } - if len(existingUserMetadata.UserMetadata) == 0 { - return nil, nil - } - events := make([]eventstore.Command, 0) - for key, value := range existingUserMetadata.UserMetadata { - if len(value) == 0 { - continue - } - events = append(events, user.NewMetadataRemovedAllEvent(ctx, &user.NewAggregate(key, resourceOwner).Aggregate)) - } - return events, nil -} - func (c *Commands) removeUserMetadata(ctx context.Context, userAgg *eventstore.Aggregate, metadataKey string) (command eventstore.Command, err error) { command = user.NewMetadataRemovedEvent( ctx, @@ -197,12 +217,3 @@ func (c *Commands) getUserMetadataListModelByID(ctx context.Context, userID, res } return userMetadataWriteModel, nil } - -func (c *Commands) getUserMetadataByOrgListModelByID(ctx context.Context, resourceOwner string) (*UserMetadataByOrgListWriteModel, error) { - userMetadataWriteModel := NewUserMetadataByOrgListWriteModel(resourceOwner) - err := c.eventstore.FilterToQueryReducer(ctx, userMetadataWriteModel) - if err != nil { - return nil, err - } - return userMetadataWriteModel, nil -} diff --git a/internal/command/user_metadata_test.go b/internal/command/user_metadata_test.go index b3ffa7b823..e8fe25acd9 100644 --- a/internal/command/user_metadata_test.go +++ b/internal/command/user_metadata_test.go @@ -16,7 +16,8 @@ import ( func TestCommandSide_SetUserMetadata(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck } type ( args struct { @@ -39,10 +40,10 @@ func TestCommandSide_SetUserMetadata(t *testing.T) { { name: "user not existing, pre condition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -60,8 +61,7 @@ func TestCommandSide_SetUserMetadata(t *testing.T) { { name: "invalid metadata, pre condition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -78,7 +78,17 @@ func TestCommandSide_SetUserMetadata(t *testing.T) { ), ), ), + expectFilter( + eventFromEventPusher( + user.NewMetadataSetEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "key", + []byte("value"), + ), + ), + ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -93,10 +103,9 @@ func TestCommandSide_SetUserMetadata(t *testing.T) { }, }, { - name: "add metadata, ok", + name: "add metadata, no permission", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -113,6 +122,43 @@ func TestCommandSide_SetUserMetadata(t *testing.T) { ), ), ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userID: "user1", + metadata: &domain.Metadata{ + Key: "key", + Value: []byte("value"), + }, + }, + res: res{ + err: zerrors.IsPermissionDenied, + }, + }, + { + name: "add metadata, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "", + "firstname lastname", + language.Und, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + expectFilter(), expectPush( user.NewMetadataSetEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, @@ -121,6 +167,7 @@ func TestCommandSide_SetUserMetadata(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -143,11 +190,116 @@ func TestCommandSide_SetUserMetadata(t *testing.T) { }, }, }, + { + name: "add metadata, reset, invalid", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "", + "firstname lastname", + language.Und, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + user.NewMetadataSetEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "key", + []byte("value"), + ), + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userID: "user1", + metadata: &domain.Metadata{ + Key: "key", + }, + }, + res: res{ + err: zerrors.IsErrorInvalidArgument, + }, + }, + { + name: "add metadata, reset, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "", + "firstname lastname", + language.Und, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + user.NewMetadataSetEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "key", + []byte("value"), + ), + ), + ), + expectPush( + user.NewMetadataSetEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "key", + []byte("value2"), + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userID: "user1", + metadata: &domain.Metadata{ + Key: "key", + Value: []byte("value2"), + }, + }, + res: res{ + want: &domain.Metadata{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + ResourceOwner: "org1", + }, + Key: "key", + Value: []byte("value2"), + State: domain.MetadataStateActive, + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, } got, err := r.SetUserMetadata(tt.args.ctx, tt.args.metadata, tt.args.userID, tt.args.orgID) if tt.res.err == nil { @@ -165,7 +317,8 @@ func TestCommandSide_SetUserMetadata(t *testing.T) { func TestCommandSide_BulkSetUserMetadata(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck } type ( args struct { @@ -188,9 +341,7 @@ func TestCommandSide_BulkSetUserMetadata(t *testing.T) { { name: "empty meta data list, pre condition error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ ctx: context.Background(), @@ -204,8 +355,7 @@ func TestCommandSide_BulkSetUserMetadata(t *testing.T) { { name: "user not existing, pre condition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -225,8 +375,7 @@ func TestCommandSide_BulkSetUserMetadata(t *testing.T) { { name: "invalid metadata, pre condition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -243,7 +392,9 @@ func TestCommandSide_BulkSetUserMetadata(t *testing.T) { ), ), ), + expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -259,10 +410,9 @@ func TestCommandSide_BulkSetUserMetadata(t *testing.T) { }, }, { - name: "add metadata, ok", + name: "add metadata, no permission", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -279,6 +429,43 @@ func TestCommandSide_BulkSetUserMetadata(t *testing.T) { ), ), ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userID: "user1", + metadataList: []*domain.Metadata{ + {Key: "key", Value: []byte("value")}, + {Key: "key1", Value: []byte("value1")}, + }, + }, + res: res{ + err: zerrors.IsPermissionDenied, + }, + }, + { + name: "add metadata, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "", + "firstname lastname", + language.Und, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + expectFilter(), expectPush( user.NewMetadataSetEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, @@ -292,6 +479,7 @@ func TestCommandSide_BulkSetUserMetadata(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -312,7 +500,8 @@ func TestCommandSide_BulkSetUserMetadata(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, } got, err := r.BulkSetUserMetadata(tt.args.ctx, tt.args.userID, tt.args.orgID, tt.args.metadataList...) if tt.res.err == nil { @@ -330,7 +519,8 @@ func TestCommandSide_BulkSetUserMetadata(t *testing.T) { func TestCommandSide_UserRemoveMetadata(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck } type ( args struct { @@ -353,8 +543,7 @@ func TestCommandSide_UserRemoveMetadata(t *testing.T) { { name: "user not existing, pre condition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -371,9 +560,7 @@ func TestCommandSide_UserRemoveMetadata(t *testing.T) { { name: "invalid metadata, pre condition error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ ctx: context.Background(), @@ -388,8 +575,7 @@ func TestCommandSide_UserRemoveMetadata(t *testing.T) { { name: "meta data not existing, not found error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -408,6 +594,7 @@ func TestCommandSide_UserRemoveMetadata(t *testing.T) { ), expectFilter(), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -419,11 +606,43 @@ func TestCommandSide_UserRemoveMetadata(t *testing.T) { err: zerrors.IsNotFound, }, }, + { + name: "remove metadata, no permission", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "", + "firstname lastname", + language.Und, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userID: "user1", + metadataKey: "key", + }, + res: res{ + err: zerrors.IsPermissionDenied, + }, + }, { name: "remove metadata, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -456,6 +675,7 @@ func TestCommandSide_UserRemoveMetadata(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -473,7 +693,8 @@ func TestCommandSide_UserRemoveMetadata(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, } got, err := r.RemoveUserMetadata(tt.args.ctx, tt.args.metadataKey, tt.args.userID, tt.args.orgID) if tt.res.err == nil { @@ -491,7 +712,8 @@ func TestCommandSide_UserRemoveMetadata(t *testing.T) { func TestCommandSide_BulkRemoveUserMetadata(t *testing.T) { type fields struct { - eventstore *eventstore.Eventstore + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck } type ( args struct { @@ -514,9 +736,7 @@ func TestCommandSide_BulkRemoveUserMetadata(t *testing.T) { { name: "empty meta data list, pre condition error", fields: fields{ - eventstore: eventstoreExpect( - t, - ), + eventstore: expectEventstore(), }, args: args{ ctx: context.Background(), @@ -530,8 +750,7 @@ func TestCommandSide_BulkRemoveUserMetadata(t *testing.T) { { name: "user not existing, pre condition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter(), ), }, @@ -548,8 +767,7 @@ func TestCommandSide_BulkRemoveUserMetadata(t *testing.T) { { name: "remove metadata keys not existing, precondition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -576,6 +794,7 @@ func TestCommandSide_BulkRemoveUserMetadata(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -590,8 +809,7 @@ func TestCommandSide_BulkRemoveUserMetadata(t *testing.T) { { name: "invalid metadata, pre condition error", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -625,6 +843,7 @@ func TestCommandSide_BulkRemoveUserMetadata(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -636,11 +855,43 @@ func TestCommandSide_BulkRemoveUserMetadata(t *testing.T) { err: zerrors.IsErrorInvalidArgument, }, }, + { + name: "remove metadata, no permission", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "", + "firstname lastname", + language.Und, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userID: "user1", + metadataList: []string{"key", "key1"}, + }, + res: res{ + err: zerrors.IsPermissionDenied, + }, + }, { name: "remove metadata, ok", fields: fields{ - eventstore: eventstoreExpect( - t, + eventstore: expectEventstore( expectFilter( eventFromEventPusher( user.NewHumanAddedEvent(context.Background(), @@ -684,6 +935,7 @@ func TestCommandSide_BulkRemoveUserMetadata(t *testing.T) { ), ), ), + checkPermission: newMockPermissionCheckAllowed(), }, args: args{ ctx: context.Background(), @@ -701,7 +953,8 @@ func TestCommandSide_BulkRemoveUserMetadata(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := &Commands{ - eventstore: tt.fields.eventstore, + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, } got, err := r.BulkRemoveUserMetadata(tt.args.ctx, tt.args.userID, tt.args.orgID, tt.args.metadataList...) if tt.res.err == nil { diff --git a/internal/command/user_personal_access_token.go b/internal/command/user_personal_access_token.go index 0faf85d5eb..f37953f3d6 100644 --- a/internal/command/user_personal_access_token.go +++ b/internal/command/user_personal_access_token.go @@ -21,6 +21,7 @@ type AddPat struct { type PersonalAccessToken struct { models.ObjectRoot + PermissionCheck PermissionCheck ExpirationDate time.Time Scopes []string @@ -43,7 +44,7 @@ func NewPersonalAccessToken(resourceOwner string, userID string, expirationDate } func (pat *PersonalAccessToken) content() error { - if pat.ResourceOwner == "" { + if pat.ResourceOwner == "" && pat.PermissionCheck == nil { return zerrors.ThrowInvalidArgument(nil, "COMMAND-xs0k2n", "Errors.ResourceOwnerMissing") } if pat.AggregateID == "" { @@ -109,11 +110,10 @@ func prepareAddPersonalAccessToken(pat *PersonalAccessToken, algorithm crypto.En if err := pat.checkAggregate(ctx, filter); err != nil { return nil, err } - writeModel, err := getPersonalAccessTokenWriteModelByID(ctx, filter, pat.AggregateID, pat.TokenID, pat.ResourceOwner) + writeModel, err := getPersonalAccessTokenWriteModelByID(ctx, filter, pat.AggregateID, pat.TokenID, pat.ResourceOwner, pat.PermissionCheck) if err != nil { return nil, err } - pat.Token, err = createToken(algorithm, writeModel.TokenID, writeModel.AggregateID) if err != nil { return nil, err @@ -155,7 +155,7 @@ func prepareRemovePersonalAccessToken(pat *PersonalAccessToken) preparation.Vali return nil, err } return func(ctx context.Context, filter preparation.FilterToQueryReducer) (_ []eventstore.Command, err error) { - writeModel, err := getPersonalAccessTokenWriteModelByID(ctx, filter, pat.AggregateID, pat.TokenID, pat.ResourceOwner) + writeModel, err := getPersonalAccessTokenWriteModelByID(ctx, filter, pat.AggregateID, pat.TokenID, pat.ResourceOwner, pat.PermissionCheck) if err != nil { return nil, err } @@ -181,16 +181,18 @@ func createToken(algorithm crypto.EncryptionAlgorithm, tokenID, userID string) ( return base64.RawURLEncoding.EncodeToString(encrypted), nil } -func getPersonalAccessTokenWriteModelByID(ctx context.Context, filter preparation.FilterToQueryReducer, userID, tokenID, resourceOwner string) (_ *PersonalAccessTokenWriteModel, err error) { +func getPersonalAccessTokenWriteModelByID(ctx context.Context, filter preparation.FilterToQueryReducer, userID, tokenID, resourceOwner string, check PermissionCheck) (_ *PersonalAccessTokenWriteModel, err error) { writeModel := NewPersonalAccessTokenWriteModel(userID, tokenID, resourceOwner) events, err := filter(ctx, writeModel.Query()) if err != nil { return nil, err } - if len(events) == 0 { - return writeModel, nil - } writeModel.AppendEvents(events...) - err = writeModel.Reduce() + if err = writeModel.Reduce(); err != nil { + return nil, err + } + if check != nil { + err = check(writeModel.ResourceOwner, writeModel.AggregateID) + } return writeModel, err } diff --git a/internal/command/user_test.go b/internal/command/user_test.go index 9abae187c1..6a1597fc8b 100644 --- a/internal/command/user_test.go +++ b/internal/command/user_test.go @@ -1813,7 +1813,7 @@ func TestExistsUser(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - gotExists, err := ExistsUser(context.Background(), tt.args.filter, tt.args.id, tt.args.resourceOwner) + gotExists, err := ExistsUser(context.Background(), tt.args.filter, tt.args.id, tt.args.resourceOwner, false) if (err != nil) != tt.wantErr { t.Errorf("ExistsUser() error = %v, wantErr %v", err, tt.wantErr) return diff --git a/internal/command/user_v2.go b/internal/command/user_v2.go index 00ca85aaf4..d6c5e7de53 100644 --- a/internal/command/user_v2.go +++ b/internal/command/user_v2.go @@ -2,10 +2,8 @@ package command import ( "context" - "github.com/zitadel/logging" - "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/repository/user" @@ -117,36 +115,6 @@ func (c *Commands) ReactivateUserV2(ctx context.Context, userID string) (*domain return writeModelToObjectDetails(&existingHuman.WriteModel), nil } -func (c *Commands) checkPermissionUpdateUser(ctx context.Context, resourceOwner, userID string) error { - if userID != "" && userID == authz.GetCtxData(ctx).UserID { - return nil - } - if err := c.checkPermission(ctx, domain.PermissionUserWrite, resourceOwner, userID); err != nil { - return err - } - return nil -} - -func (c *Commands) checkPermissionUpdateUserCredentials(ctx context.Context, resourceOwner, userID string) error { - if userID != "" && userID == authz.GetCtxData(ctx).UserID { - return nil - } - if err := c.checkPermission(ctx, domain.PermissionUserCredentialWrite, resourceOwner, userID); err != nil { - return err - } - return nil -} - -func (c *Commands) checkPermissionDeleteUser(ctx context.Context, resourceOwner, userID string) error { - if userID != "" && userID == authz.GetCtxData(ctx).UserID { - return nil - } - if err := c.checkPermission(ctx, domain.PermissionUserDelete, resourceOwner, userID); err != nil { - return err - } - return nil -} - func (c *Commands) userStateWriteModel(ctx context.Context, userID string) (writeModel *UserV2WriteModel, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -163,7 +131,6 @@ func (c *Commands) RemoveUserV2(ctx context.Context, userID, resourceOwner strin if userID == "" { return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-vaipl7s13l", "Errors.User.UserIDMissing") } - existingUser, err := c.userRemoveWriteModel(ctx, userID, resourceOwner) if err != nil { return nil, err @@ -174,7 +141,6 @@ func (c *Commands) RemoveUserV2(ctx context.Context, userID, resourceOwner strin if err := c.checkPermissionDeleteUser(ctx, existingUser.ResourceOwner, existingUser.AggregateID); err != nil { return nil, err } - domainPolicy, err := c.domainPolicyWriteModel(ctx, existingUser.ResourceOwner) if err != nil { return nil, zerrors.ThrowPreconditionFailed(err, "COMMAND-l40ykb3xh2", "Errors.Org.DomainPolicy.NotExisting") @@ -183,7 +149,7 @@ func (c *Commands) RemoveUserV2(ctx context.Context, userID, resourceOwner strin events = append(events, user.NewUserRemovedEvent(ctx, &existingUser.Aggregate().Aggregate, existingUser.UserName, existingUser.IDPLinks, domainPolicy.UserLoginMustBeDomain)) for _, grantID := range cascadingGrantIDs { - removeEvent, _, err := c.removeUserGrant(ctx, grantID, "", true) + removeEvent, _, err := c.removeUserGrant(ctx, grantID, "", true, true, nil) if err != nil { logging.WithFields("usergrantid", grantID).WithError(err).Warn("could not cascade remove role on user grant") continue diff --git a/internal/command/user_v2_human.go b/internal/command/user_v2_human.go index f88e2017d5..0945ae7578 100644 --- a/internal/command/user_v2_human.go +++ b/internal/command/user_v2_human.go @@ -5,6 +5,7 @@ import ( "golang.org/x/text/language" + "github.com/zitadel/zitadel/internal/command/preparation" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" @@ -121,7 +122,10 @@ func (c *Commands) AddUserHuman(ctx context.Context, resourceOwner string, human if resourceOwner == "" { return zerrors.ThrowInvalidArgument(nil, "COMMA-095xh8fll1", "Errors.Internal") } - + if human.Details == nil { + human.Details = &domain.ObjectDetails{} + } + human.Details.ResourceOwner = resourceOwner if err := human.Validate(c.userPasswordHasher); err != nil { return err } @@ -132,7 +136,12 @@ func (c *Commands) AddUserHuman(ctx context.Context, resourceOwner string, human return err } } - + // check for permission to create user on resourceOwner + if !human.Register { + if err := c.checkPermissionUpdateUser(ctx, resourceOwner, human.ID); err != nil { + return err + } + } // only check if user is already existing existingHuman, err := c.userExistsWriteModel( ctx, @@ -144,12 +153,6 @@ func (c *Commands) AddUserHuman(ctx context.Context, resourceOwner string, human if isUserStateExists(existingHuman.UserState) { return zerrors.ThrowPreconditionFailed(nil, "COMMAND-7yiox1isql", "Errors.User.AlreadyExisting") } - // check for permission to create user on resourceOwner - if !human.Register { - if err := c.checkPermission(ctx, domain.PermissionUserWrite, resourceOwner, human.ID); err != nil { - return err - } - } // add resourceowner for the events with the aggregate existingHuman.ResourceOwner = resourceOwner @@ -161,6 +164,7 @@ func (c *Commands) AddUserHuman(ctx context.Context, resourceOwner string, human if err = c.userValidateDomain(ctx, resourceOwner, human.Username, domainPolicy.UserLoginMustBeDomain); err != nil { return err } + var createCmd humanCreationCommand if human.Register { createCmd = user.NewHumanRegisteredEvent( @@ -203,17 +207,33 @@ func (c *Commands) AddUserHuman(ctx context.Context, resourceOwner string, human return err } - cmds := make([]eventstore.Command, 0, 3) - cmds = append(cmds, createCmd) - - cmds, err = c.addHumanCommandEmail(ctx, filter, cmds, existingHuman.Aggregate(), human, alg, allowInitMail) + cmds, err := c.addUserHumanCommands(ctx, filter, existingHuman, human, allowInitMail, alg, createCmd) if err != nil { return err } + if len(cmds) == 0 { + human.Details = writeModelToObjectDetails(&existingHuman.WriteModel) + return nil + } + err = c.pushAppendAndReduce(ctx, existingHuman, cmds...) + if err != nil { + return err + } + human.Details = writeModelToObjectDetails(&existingHuman.WriteModel) + return nil +} + +func (c *Commands) addUserHumanCommands(ctx context.Context, filter preparation.FilterToQueryReducer, existingHuman *UserV2WriteModel, human *AddHuman, allowInitMail bool, alg crypto.EncryptionAlgorithm, addUserCommand eventstore.Command) ([]eventstore.Command, error) { + cmds := []eventstore.Command{addUserCommand} + var err error + cmds, err = c.addHumanCommandEmail(ctx, filter, cmds, existingHuman.Aggregate(), human, alg, allowInitMail) + if err != nil { + return nil, err + } cmds, err = c.addHumanCommandPhone(ctx, filter, cmds, existingHuman.Aggregate(), human, alg) if err != nil { - return err + return nil, err } for _, metadataEntry := range human.Metadata { @@ -227,7 +247,7 @@ func (c *Commands) AddUserHuman(ctx context.Context, resourceOwner string, human for _, link := range human.Links { cmd, err := addLink(ctx, filter, existingHuman.Aggregate(), link) if err != nil { - return err + return nil, err } cmds = append(cmds, cmd) } @@ -235,7 +255,7 @@ func (c *Commands) AddUserHuman(ctx context.Context, resourceOwner string, human if human.TOTPSecret != "" { encryptedSecret, err := crypto.Encrypt([]byte(human.TOTPSecret), c.multifactors.OTP.CryptoMFA) if err != nil { - return err + return nil, err } cmds = append(cmds, user.NewHumanOTPAddedEvent(ctx, &existingHuman.Aggregate().Aggregate, encryptedSecret), @@ -246,18 +266,7 @@ func (c *Commands) AddUserHuman(ctx context.Context, resourceOwner string, human if human.SetInactive { cmds = append(cmds, user.NewUserDeactivatedEvent(ctx, &existingHuman.Aggregate().Aggregate)) } - - if len(cmds) == 0 { - human.Details = writeModelToObjectDetails(&existingHuman.WriteModel) - return nil - } - - err = c.pushAppendAndReduce(ctx, existingHuman, cmds...) - if err != nil { - return err - } - human.Details = writeModelToObjectDetails(&existingHuman.WriteModel) - return nil + return cmds, nil } func (c *Commands) ChangeUserHuman(ctx context.Context, human *ChangeHuman, alg crypto.EncryptionAlgorithm) (err error) { @@ -341,7 +350,6 @@ func (c *Commands) ChangeUserHuman(ctx context.Context, human *ChangeHuman, alg if human.State != nil { // only allow toggling between active and inactive // any other target state is not supported - // the existing human's state has to be the switch { case isUserStateActive(*human.State): if isUserStateActive(existingHuman.UserState) { diff --git a/internal/command/user_v2_human_test.go b/internal/command/user_v2_human_test.go index 2b4399fb2a..e44e182b92 100644 --- a/internal/command/user_v2_human_test.go +++ b/internal/command/user_v2_human_test.go @@ -302,9 +302,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) { { name: "add human (with initial code), no permission", fields: fields{ - eventstore: expectEventstore( - expectFilter(), - ), + eventstore: expectEventstore(), checkPermission: newMockPermissionCheckNotAllowed(), idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), newCode: mockEncryptedCode("userinit", time.Hour), @@ -326,9 +324,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) { codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), }, res: res{ - err: func(err error) bool { - return errors.Is(err, zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied")) - }, + err: zerrors.IsPermissionDenied, }, }, { diff --git a/internal/command/user_v2_invite.go b/internal/command/user_v2_invite.go index 430ba8c7d1..10948164e9 100644 --- a/internal/command/user_v2_invite.go +++ b/internal/command/user_v2_invite.go @@ -6,6 +6,7 @@ import ( "github.com/zitadel/logging" + "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" @@ -50,8 +51,10 @@ func (c *Commands) sendInviteCode(ctx context.Context, invite *CreateUserInvite, if err != nil { return nil, nil, err } - if err := c.checkPermission(ctx, domain.PermissionUserWrite, wm.ResourceOwner, wm.AggregateID); err != nil { - return nil, nil, err + if wm.AggregateID != authz.GetCtxData(ctx).UserID { + if err := c.checkPermission(ctx, domain.PermissionUserWrite, wm.ResourceOwner, wm.AggregateID); err != nil { + return nil, nil, err + } } if !wm.UserState.Exists() { return nil, nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-Wgvn4", "Errors.User.NotFound") diff --git a/internal/command/user_v2_invite_model.go b/internal/command/user_v2_invite_model.go index 6b2ab62e0d..91726498f8 100644 --- a/internal/command/user_v2_invite_model.go +++ b/internal/command/user_v2_invite_model.go @@ -65,8 +65,11 @@ func (wm *UserV2InviteWriteModel) Reduce() error { wm.EmptyInviteCode() case *user.HumanInviteCheckFailedEvent: wm.InviteCheckFailureCount++ - if wm.InviteCheckFailureCount >= 3 { //TODO: config? - wm.UserState = domain.UserStateDeleted + if wm.InviteCheckFailureCount >= 3 || crypto.IsCodeExpired(wm.InviteCodeCreationDate, wm.InviteCodeExpiry) { //TODO: make failure count comparison with wm.InviteCheckFailureCount configurable? + // invalidate the invite code after attempting to verify an expired code, or a wrong code three or more times + // so that a new invite code can be created for this user + wm.EmptyInviteCode() + wm.CodeReturned = false } case *user.HumanEmailVerifiedEvent: wm.EmailVerified = true diff --git a/internal/command/user_v2_invite_test.go b/internal/command/user_v2_invite_test.go index 8d49465778..49a2e78249 100644 --- a/internal/command/user_v2_invite_test.go +++ b/internal/command/user_v2_invite_test.go @@ -11,6 +11,7 @@ import ( "go.uber.org/mock/gomock" "golang.org/x/text/language" + "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" @@ -20,6 +21,7 @@ import ( ) func TestCommands_CreateInviteCode(t *testing.T) { + t.Parallel() type fields struct { checkPermission domain.PermissionCheck newEncryptedCodeWithDefault encryptedCodeWithDefaultFunc @@ -205,6 +207,64 @@ func TestCommands_CreateInviteCode(t *testing.T) { returnCode: gu.Ptr("code"), }, }, + { + "return ok, with same user requests code", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + "username", "firstName", + "lastName", + "nickName", + "displayName", + language.Afrikaans, + domain.GenderUnspecified, + "email", + false, + ), + ), + ), + expectPush( + eventFromEventPusher( + user.NewHumanInviteCodeAddedEvent(authz.SetCtxData(context.Background(), authz.CtxData{UserID: "userID"}), + &user.NewAggregate("userID", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("code"), + }, + time.Hour, + "", + true, + "", + "", + ), + ), + ), + ), + // we do not run checkPermission() because the same user is requesting the code as the user to which the code is intended for + checkPermission: nil, + newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("code", time.Hour), + defaultSecretGenerators: &SecretGenerators{}, + }, + args{ + ctx: authz.SetCtxData(context.Background(), authz.CtxData{UserID: "userID"}), + invite: &CreateUserInvite{ + UserID: "userID", + ReturnCode: true, + }, + }, + want{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + ID: "userID", + }, + returnCode: gu.Ptr("code"), + }, + }, { "with template and application name ok", fields{ @@ -264,9 +324,348 @@ func TestCommands_CreateInviteCode(t *testing.T) { returnCode: nil, }, }, + { + "create ok after three verification failures", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + "username", "firstName", + "lastName", + "nickName", + "displayName", + language.Afrikaans, + domain.GenderUnspecified, + "email", + false, + ), + ), + // first invite code generated and returned + eventFromEventPusherWithCreationDateNow( + user.NewHumanInviteCodeAddedEvent(context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("code1"), + }, + time.Hour, + "", + true, + "", + "", + ), + ), + // simulate three failed verification attempts + eventFromEventPusher( + user.NewHumanInviteCheckFailedEvent(context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + ), + ), + eventFromEventPusher( + user.NewHumanInviteCheckFailedEvent(context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + ), + ), + eventFromEventPusher( + user.NewHumanInviteCheckFailedEvent(context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + ), + ), + ), + expectPush( + eventFromEventPusher( + user.NewHumanInviteCodeAddedEvent(context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("code2"), + }, + time.Hour, + "", + false, + "", + "", + ), + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("code2", time.Hour), + defaultSecretGenerators: &SecretGenerators{}, + }, + args{ + ctx: context.Background(), + invite: &CreateUserInvite{ + UserID: "userID", + }, + }, + want{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + ID: "userID", + }, + returnCode: nil, + }, + }, + { + "return ok after three verification failures", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + "username", "firstName", + "lastName", + "nickName", + "displayName", + language.Afrikaans, + domain.GenderUnspecified, + "email", + false, + ), + ), + // first invite code generated and returned + eventFromEventPusherWithCreationDateNow( + user.NewHumanInviteCodeAddedEvent(context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("code1"), + }, + time.Hour, + "", + true, + "", + "", + ), + ), + // simulate three failed verification attempts + eventFromEventPusher( + user.NewHumanInviteCheckFailedEvent(context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + ), + ), + eventFromEventPusher( + user.NewHumanInviteCheckFailedEvent(context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + ), + ), + eventFromEventPusher( + user.NewHumanInviteCheckFailedEvent(context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + ), + ), + ), + expectPush( + eventFromEventPusher( + user.NewHumanInviteCodeAddedEvent(context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("code2"), + }, + time.Hour, + "", + true, + "", + "", + ), + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("code2", time.Hour), + defaultSecretGenerators: &SecretGenerators{}, + }, + args{ + ctx: context.Background(), + invite: &CreateUserInvite{ + UserID: "userID", + ReturnCode: true, + }, + }, + want{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + ID: "userID", + }, + returnCode: gu.Ptr("code2"), + }, + }, + { + "create ok after verification fails due to invite code expiration", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + "username", "firstName", + "lastName", + "nickName", + "displayName", + language.Afrikaans, + domain.GenderUnspecified, + "email", + false, + ), + ), + // first invite code generated and returned + eventFromEventPusherWithCreationDateNow( + user.NewHumanInviteCodeAddedEvent(context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("code1"), + }, + -5*time.Minute, // expired code + "", + true, + "", + "", + ), + ), + // simulate a failed verification attempt due to expiry + eventFromEventPusher( + user.NewHumanInviteCheckFailedEvent(context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + ), + ), + ), + expectPush( + eventFromEventPusher( + user.NewHumanInviteCodeAddedEvent(context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("code2"), + }, + time.Hour, + "", + false, + "", + "", + ), + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("code2", time.Hour), + defaultSecretGenerators: &SecretGenerators{}, + }, + args{ + ctx: context.Background(), + invite: &CreateUserInvite{ + UserID: "userID", + }, + }, + want{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + ID: "userID", + }, + returnCode: nil, + }, + }, + { + "return ok after verification fails due to invite code expiration", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + "username", "firstName", + "lastName", + "nickName", + "displayName", + language.Afrikaans, + domain.GenderUnspecified, + "email", + false, + ), + ), + // first invite code generated and returned + eventFromEventPusherWithCreationDateNow( + user.NewHumanInviteCodeAddedEvent(context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("code1"), + }, + -5*time.Minute, // expired code + "", + true, + "", + "", + ), + ), + // simulate a failed verification attempt due to expiry + eventFromEventPusher( + user.NewHumanInviteCheckFailedEvent(context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + ), + ), + ), + expectPush( + eventFromEventPusher( + user.NewHumanInviteCodeAddedEvent(context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("code2"), + }, + time.Hour, + "", + true, + "", + "", + ), + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("code2", time.Hour), + defaultSecretGenerators: &SecretGenerators{}, + }, + args{ + ctx: context.Background(), + invite: &CreateUserInvite{ + UserID: "userID", + ReturnCode: true, + }, + }, + want{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + ID: "userID", + }, + returnCode: gu.Ptr("code2"), + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() c := &Commands{ checkPermission: tt.fields.checkPermission, newEncryptedCodeWithDefault: tt.fields.newEncryptedCodeWithDefault, @@ -283,6 +682,7 @@ func TestCommands_CreateInviteCode(t *testing.T) { } func TestCommands_ResendInviteCode(t *testing.T) { + t.Parallel() type fields struct { checkPermission domain.PermissionCheck newEncryptedCodeWithDefault encryptedCodeWithDefaultFunc @@ -322,7 +722,21 @@ func TestCommands_ResendInviteCode(t *testing.T) { "missing permission", fields{ eventstore: expectEventstore( - expectFilter(), + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + "username", "firstName", + "lastName", + "nickName", + "displayName", + language.Afrikaans, + domain.GenderUnspecified, + "email", + false, + ), + ), + ), ), checkPermission: newMockPermissionCheckNotAllowed(), }, @@ -338,6 +752,7 @@ func TestCommands_ResendInviteCode(t *testing.T) { "user does not exist", fields{ eventstore: expectEventstore( + // The write model doesn't query any events expectFilter(), ), checkPermission: newMockPermissionCheckAllowed(), @@ -495,6 +910,77 @@ func TestCommands_ResendInviteCode(t *testing.T) { }, }, }, + { + "return ok, with same user requests code", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + "username", "firstName", + "lastName", + "nickName", + "displayName", + language.Afrikaans, + domain.GenderUnspecified, + "email", + false, + ), + ), + eventFromEventPusher( + user.NewHumanInviteCodeAddedEvent(context.Background(), + &user.NewAggregate("userID", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("code"), + }, + time.Hour, + "", + false, + "", + "authRequestID", + ), + ), + ), + expectPush( + eventFromEventPusher( + user.NewHumanInviteCodeAddedEvent(authz.SetCtxData(context.Background(), authz.CtxData{UserID: "userID"}), + &user.NewAggregate("userID", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("code"), + }, + time.Hour, + "", + false, + "", + "authRequestID", + ), + ), + ), + ), + // we do not run checkPermission() because the same user is requesting the code as the user to which the code is intended for + checkPermission: nil, + newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("code", time.Hour), + defaultSecretGenerators: &SecretGenerators{}, + }, + args{ + // ctx: context.Background(), + ctx: authz.SetCtxData(context.Background(), authz.CtxData{UserID: "userID"}), + userID: "userID", + }, + want{ + details: &domain.ObjectDetails{ + ResourceOwner: "org1", + ID: "userID", + }, + }, + }, { "resend with new auth requestID ok", fields{ @@ -568,6 +1054,7 @@ func TestCommands_ResendInviteCode(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() c := &Commands{ checkPermission: tt.fields.checkPermission, newEncryptedCodeWithDefault: tt.fields.newEncryptedCodeWithDefault, @@ -582,6 +1069,7 @@ func TestCommands_ResendInviteCode(t *testing.T) { } func TestCommands_InviteCodeSent(t *testing.T) { + t.Parallel() type fields struct { eventstore func(*testing.T) *eventstore.Eventstore } @@ -700,6 +1188,7 @@ func TestCommands_InviteCodeSent(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() c := &Commands{ eventstore: tt.fields.eventstore(t), } @@ -710,6 +1199,7 @@ func TestCommands_InviteCodeSent(t *testing.T) { } func TestCommands_VerifyInviteCode(t *testing.T) { + t.Parallel() type fields struct { eventstore func(*testing.T) *eventstore.Eventstore userEncryption crypto.EncryptionAlgorithm @@ -795,6 +1285,7 @@ func TestCommands_VerifyInviteCode(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() c := &Commands{ eventstore: tt.fields.eventstore(t), userEncryption: tt.fields.userEncryption, @@ -807,6 +1298,7 @@ func TestCommands_VerifyInviteCode(t *testing.T) { } func TestCommands_VerifyInviteCodeSetPassword(t *testing.T) { + t.Parallel() type fields struct { eventstore func(*testing.T) *eventstore.Eventstore userEncryption crypto.EncryptionAlgorithm @@ -1123,6 +1615,7 @@ func TestCommands_VerifyInviteCodeSetPassword(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() c := &Commands{ eventstore: tt.fields.eventstore(t), userEncryption: tt.fields.userEncryption, diff --git a/internal/command/user_v2_machine.go b/internal/command/user_v2_machine.go new file mode 100644 index 0000000000..34079b7e6f --- /dev/null +++ b/internal/command/user_v2_machine.go @@ -0,0 +1,94 @@ +package command + +import ( + "context" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/repository/user" + "github.com/zitadel/zitadel/internal/telemetry/tracing" + "github.com/zitadel/zitadel/internal/zerrors" +) + +type ChangeMachine struct { + ID string + ResourceOwner string + Username *string + Name *string + Description *string + + // Details are set after a successful execution of the command + Details *domain.ObjectDetails +} + +func (h *ChangeMachine) Changed() bool { + if h.Username != nil { + return true + } + if h.Name != nil { + return true + } + if h.Description != nil { + return true + } + return false +} + +func (c *Commands) ChangeUserMachine(ctx context.Context, machine *ChangeMachine) (err error) { + existingMachine, err := c.UserMachineWriteModel( + ctx, + machine.ID, + machine.ResourceOwner, + false, + ) + if err != nil { + return err + } + if machine.Changed() { + if err := c.checkPermissionUpdateUser(ctx, existingMachine.ResourceOwner, existingMachine.AggregateID); err != nil { + return err + } + } + + cmds := make([]eventstore.Command, 0) + if machine.Username != nil { + cmds, err = c.changeUsername(ctx, cmds, existingMachine, *machine.Username) + if err != nil { + return err + } + } + var machineChanges []user.MachineChanges + if machine.Name != nil && *machine.Name != existingMachine.Name { + machineChanges = append(machineChanges, user.ChangeName(*machine.Name)) + } + if machine.Description != nil && *machine.Description != existingMachine.Description { + machineChanges = append(machineChanges, user.ChangeDescription(*machine.Description)) + } + if len(machineChanges) > 0 { + cmds = append(cmds, user.NewMachineChangedEvent(ctx, &existingMachine.Aggregate().Aggregate, machineChanges)) + } + if len(cmds) == 0 { + machine.Details = writeModelToObjectDetails(&existingMachine.WriteModel) + return nil + } + err = c.pushAppendAndReduce(ctx, existingMachine, cmds...) + if err != nil { + return err + } + machine.Details = writeModelToObjectDetails(&existingMachine.WriteModel) + return nil +} + +func (c *Commands) UserMachineWriteModel(ctx context.Context, userID, resourceOwner string, metadataWM bool) (writeModel *UserV2WriteModel, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + writeModel = NewUserMachineWriteModel(userID, resourceOwner, metadataWM) + err = c.eventstore.FilterToQueryReducer(ctx, writeModel) + if err != nil { + return nil, err + } + if !isUserStateExists(writeModel.UserState) { + return nil, zerrors.ThrowNotFound(nil, "COMMAND-ugjs0upun6", "Errors.User.NotFound") + } + return writeModel, nil +} diff --git a/internal/command/user_v2_machine_test.go b/internal/command/user_v2_machine_test.go new file mode 100644 index 0000000000..14df4bfae7 --- /dev/null +++ b/internal/command/user_v2_machine_test.go @@ -0,0 +1,260 @@ +package command + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/repository/org" + "github.com/zitadel/zitadel/internal/repository/user" + "github.com/zitadel/zitadel/internal/zerrors" +) + +func TestCommandSide_ChangeUserMachine(t *testing.T) { + type fields struct { + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck + } + type args struct { + ctx context.Context + orgID string + machine *ChangeMachine + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + + userAgg := user.NewAggregate("user1", "org1") + userAddedEvent := user.NewMachineAddedEvent(context.Background(), + &userAgg.Aggregate, + "username", + "name", + "description", + true, + domain.OIDCTokenTypeBearer, + ) + + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "change machine username, no permission", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(userAddedEvent), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + machine: &ChangeMachine{ + Username: gu.Ptr("changed"), + }, + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied")) + }, + }, + }, + { + name: "change machine username, not found", + fields: fields{ + eventstore: expectEventstore( + expectFilter(), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + machine: &ChangeMachine{ + Username: gu.Ptr("changed"), + }, + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowNotFound(nil, "COMMAND-ugjs0upun6", "Errors.User.NotFound")) + }, + }, + }, + { + name: "change machine username, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(userAddedEvent), + ), + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &userAgg.Aggregate, + true, + true, + true, + ), + ), + ), + expectPush( + user.NewUsernameChangedEvent(context.Background(), + &userAgg.Aggregate, + "username", + "changed", + true, + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + machine: &ChangeMachine{ + Username: gu.Ptr("changed"), + }, + }, + res: res{ + want: &domain.ObjectDetails{ + Sequence: 0, + EventDate: time.Time{}, + ResourceOwner: "org1", + }, + }, + }, + { + name: "change machine username, no change", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(userAddedEvent), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + machine: &ChangeMachine{ + Username: gu.Ptr("username"), + }, + }, + res: res{ + want: &domain.ObjectDetails{ + Sequence: 0, + EventDate: time.Time{}, + ResourceOwner: "org1", + }, + }, + }, + { + name: "change machine description, no permission", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(userAddedEvent), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + machine: &ChangeMachine{ + Description: gu.Ptr("changed"), + }, + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied")) + }, + }, + }, + { + name: "change machine description, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(userAddedEvent), + ), + expectPush( + user.NewMachineChangedEvent(context.Background(), + &userAgg.Aggregate, + []user.MachineChanges{ + user.ChangeDescription("changed"), + }, + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + machine: &ChangeMachine{ + Description: gu.Ptr("changed"), + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "change machine description, no change", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(userAddedEvent), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + machine: &ChangeMachine{ + Description: gu.Ptr("description"), + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, + } + err := r.ChangeUserMachine(tt.args.ctx, tt.args.machine) + if tt.res.err == nil { + if !assert.NoError(t, err) { + t.FailNow() + } + } else if !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + return + } + if tt.res.err == nil { + assertObjectDetails(t, tt.res.want, tt.args.machine.Details) + } + }) + } +} diff --git a/internal/command/user_v2_model.go b/internal/command/user_v2_model.go index 214a2a5f9d..92346bf3b6 100644 --- a/internal/command/user_v2_model.go +++ b/internal/command/user_v2_model.go @@ -118,6 +118,14 @@ func NewUserHumanWriteModel(userID, resourceOwner string, profileWM, emailWM, ph return newUserV2WriteModel(userID, resourceOwner, opts...) } +func NewUserMachineWriteModel(userID, resourceOwner string, metadataListWM bool) *UserV2WriteModel { + opts := []UserV2WMOption{WithMachine(), WithState()} + if metadataListWM { + opts = append(opts, WithMetadata()) + } + return newUserV2WriteModel(userID, resourceOwner, opts...) +} + func newUserV2WriteModel(userID, resourceOwner string, opts ...UserV2WMOption) *UserV2WriteModel { wm := &UserV2WriteModel{ WriteModel: eventstore.WriteModel{ diff --git a/internal/command/user_v2_test.go b/internal/command/user_v2_test.go index 3eb9ecd6f7..685ad95253 100644 --- a/internal/command/user_v2_test.go +++ b/internal/command/user_v2_test.go @@ -9,6 +9,7 @@ import ( "github.com/stretchr/testify/assert" "golang.org/x/text/language" + "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/repository/org" @@ -1081,13 +1082,14 @@ func TestCommandSide_ReactivateUserV2(t *testing.T) { } func TestCommandSide_RemoveUserV2(t *testing.T) { + ctxUserID := "ctxUserID" + ctx := authz.SetCtxData(context.Background(), authz.CtxData{UserID: ctxUserID}) type fields struct { eventstore func(*testing.T) *eventstore.Eventstore checkPermission domain.PermissionCheck } type ( args struct { - ctx context.Context userID string cascadingMemberships []*CascadingMembership grantIDs []string @@ -1110,7 +1112,6 @@ func TestCommandSide_RemoveUserV2(t *testing.T) { checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - ctx: context.Background(), userID: "", }, res: res{ @@ -1128,7 +1129,6 @@ func TestCommandSide_RemoveUserV2(t *testing.T) { checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - ctx: context.Background(), userID: "user1", }, res: res{ @@ -1143,7 +1143,7 @@ func TestCommandSide_RemoveUserV2(t *testing.T) { eventstore: expectEventstore( expectFilter( eventFromEventPusher( - user.NewHumanAddedEvent(context.Background(), + user.NewHumanAddedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, "username", "firstname", @@ -1157,7 +1157,7 @@ func TestCommandSide_RemoveUserV2(t *testing.T) { ), ), eventFromEventPusher( - user.NewUserRemovedEvent(context.Background(), + user.NewUserRemovedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, "username", nil, @@ -1169,7 +1169,6 @@ func TestCommandSide_RemoveUserV2(t *testing.T) { checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - ctx: context.Background(), userID: "user1", }, res: res{ @@ -1184,7 +1183,7 @@ func TestCommandSide_RemoveUserV2(t *testing.T) { eventstore: expectEventstore( expectFilter( eventFromEventPusher( - user.NewHumanAddedEvent(context.Background(), + user.NewHumanAddedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, "username", "firstname", @@ -1200,7 +1199,7 @@ func TestCommandSide_RemoveUserV2(t *testing.T) { ), expectFilter( eventFromEventPusher( - org.NewDomainPolicyAddedEvent(context.Background(), + org.NewDomainPolicyAddedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, true, true, @@ -1209,7 +1208,7 @@ func TestCommandSide_RemoveUserV2(t *testing.T) { ), ), expectPush( - user.NewUserRemovedEvent(context.Background(), + user.NewUserRemovedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, "username", nil, @@ -1220,7 +1219,6 @@ func TestCommandSide_RemoveUserV2(t *testing.T) { checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - ctx: context.Background(), userID: "user1", }, res: res{ @@ -1235,7 +1233,7 @@ func TestCommandSide_RemoveUserV2(t *testing.T) { eventstore: expectEventstore( expectFilter( eventFromEventPusher( - user.NewHumanAddedEvent(context.Background(), + user.NewHumanAddedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, "username", "firstname", @@ -1249,7 +1247,7 @@ func TestCommandSide_RemoveUserV2(t *testing.T) { ), ), eventFromEventPusher( - user.NewHumanInitializedCheckSucceededEvent(context.Background(), + user.NewHumanInitializedCheckSucceededEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, ), ), @@ -1258,13 +1256,10 @@ func TestCommandSide_RemoveUserV2(t *testing.T) { checkPermission: newMockPermissionCheckNotAllowed(), }, args: args{ - ctx: context.Background(), userID: "user1", }, res: res{ - err: func(err error) bool { - return errors.Is(err, zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied")) - }, + err: zerrors.IsPermissionDenied, }, }, { @@ -1273,7 +1268,7 @@ func TestCommandSide_RemoveUserV2(t *testing.T) { eventstore: expectEventstore( expectFilter( eventFromEventPusher( - user.NewMachineAddedEvent(context.Background(), + user.NewMachineAddedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, "username", "name", @@ -1283,7 +1278,7 @@ func TestCommandSide_RemoveUserV2(t *testing.T) { ), ), eventFromEventPusher( - user.NewUserRemovedEvent(context.Background(), + user.NewUserRemovedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, "username", nil, @@ -1292,10 +1287,8 @@ func TestCommandSide_RemoveUserV2(t *testing.T) { ), ), ), - checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - ctx: context.Background(), userID: "user1", }, res: res{ @@ -1310,7 +1303,7 @@ func TestCommandSide_RemoveUserV2(t *testing.T) { eventstore: expectEventstore( expectFilter( eventFromEventPusher( - user.NewMachineAddedEvent(context.Background(), + user.NewMachineAddedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, "username", "name", @@ -1322,7 +1315,7 @@ func TestCommandSide_RemoveUserV2(t *testing.T) { ), expectFilter( eventFromEventPusher( - org.NewDomainPolicyAddedEvent(context.Background(), + org.NewDomainPolicyAddedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, true, true, @@ -1331,7 +1324,7 @@ func TestCommandSide_RemoveUserV2(t *testing.T) { ), ), expectPush( - user.NewUserRemovedEvent(context.Background(), + user.NewUserRemovedEvent(ctx, &user.NewAggregate("user1", "org1").Aggregate, "username", nil, @@ -1342,7 +1335,6 @@ func TestCommandSide_RemoveUserV2(t *testing.T) { checkPermission: newMockPermissionCheckAllowed(), }, args: args{ - ctx: context.Background(), userID: "user1", }, res: res{ @@ -1351,6 +1343,56 @@ func TestCommandSide_RemoveUserV2(t *testing.T) { }, }, }, + { + name: "remove self, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(ctx, + &user.NewAggregate(ctxUserID, "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(ctx, + &user.NewAggregate(ctxUserID, "org1").Aggregate, + true, + true, + true, + ), + ), + ), + expectPush( + user.NewUserRemovedEvent(ctx, + &user.NewAggregate(ctxUserID, "org1").Aggregate, + "username", + nil, + true, + ), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args: args{ + userID: ctxUserID, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -1358,7 +1400,8 @@ func TestCommandSide_RemoveUserV2(t *testing.T) { eventstore: tt.fields.eventstore(t), checkPermission: tt.fields.checkPermission, } - got, err := r.RemoveUserV2(tt.args.ctx, tt.args.userID, "", tt.args.cascadingMemberships, tt.args.grantIDs...) + + got, err := r.RemoveUserV2(ctx, tt.args.userID, "", tt.args.cascadingMemberships, tt.args.grantIDs...) if tt.res.err == nil { assert.NoError(t, err) } diff --git a/internal/crypto/rsa.go b/internal/crypto/rsa.go index 198610d8aa..3fd9a77569 100644 --- a/internal/crypto/rsa.go +++ b/internal/crypto/rsa.go @@ -21,14 +21,6 @@ func GenerateKeyPair(bits int) (*rsa.PrivateKey, *rsa.PublicKey, error) { return privkey, &privkey.PublicKey, nil } -func GenerateEncryptedKeyPair(bits int, alg EncryptionAlgorithm) (*CryptoValue, *CryptoValue, error) { - privateKey, publicKey, err := GenerateKeyPair(bits) - if err != nil { - return nil, nil, err - } - return EncryptKeys(privateKey, publicKey, alg) -} - type CertificateInformations struct { SerialNumber *big.Int Organisation []string diff --git a/internal/crypto/web_key.go b/internal/crypto/web_key.go index c769cb1213..286305259b 100644 --- a/internal/crypto/web_key.go +++ b/internal/crypto/web_key.go @@ -7,6 +7,7 @@ import ( "crypto/rand" "crypto/rsa" "encoding/json" + "fmt" "github.com/go-jose/go-jose/v4" "github.com/muhlemmer/gu" @@ -219,6 +220,8 @@ func generateWebKey(keyID string, genConfig WebKeyConfig) (private, public *jose key, err = ecdsa.GenerateKey(conf.GetCurve(), rand.Reader) case *WebKeyED25519Config: _, key, err = ed25519.GenerateKey(rand.Reader) + default: + return nil, nil, fmt.Errorf("unknown webkey config type %T", genConfig) } if err != nil { return nil, nil, err diff --git a/internal/database/mock/sql_mock.go b/internal/database/mock/sql_mock.go index b8030b269f..cd30cd9cf0 100644 --- a/internal/database/mock/sql_mock.go +++ b/internal/database/mock/sql_mock.go @@ -14,9 +14,9 @@ type SQLMock struct { mock sqlmock.Sqlmock } -type expectation func(m sqlmock.Sqlmock) +type Expectation func(m sqlmock.Sqlmock) -func NewSQLMock(t *testing.T, expectations ...expectation) *SQLMock { +func NewSQLMock(t *testing.T, expectations ...Expectation) *SQLMock { db, mock, err := sqlmock.New( sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual), sqlmock.ValueConverterOption(new(TypeConverter)), @@ -45,7 +45,7 @@ func (m *SQLMock) Assert(t *testing.T) { m.DB.Close() } -func ExpectBegin(err error) expectation { +func ExpectBegin(err error) Expectation { return func(m sqlmock.Sqlmock) { e := m.ExpectBegin() if err != nil { @@ -54,7 +54,7 @@ func ExpectBegin(err error) expectation { } } -func ExpectCommit(err error) expectation { +func ExpectCommit(err error) Expectation { return func(m sqlmock.Sqlmock) { e := m.ExpectCommit() if err != nil { @@ -89,7 +89,7 @@ func WithExecRowsAffected(affected driver.RowsAffected) ExecOpt { } } -func ExcpectExec(stmt string, opts ...ExecOpt) expectation { +func ExcpectExec(stmt string, opts ...ExecOpt) Expectation { return func(m sqlmock.Sqlmock) { e := m.ExpectExec(stmt) for _, opt := range opts { @@ -122,7 +122,7 @@ func WithQueryResult(columns []string, rows [][]driver.Value) QueryOpt { } } -func ExpectQuery(stmt string, opts ...QueryOpt) expectation { +func ExpectQuery(stmt string, opts ...QueryOpt) Expectation { return func(m sqlmock.Sqlmock) { e := m.ExpectQuery(stmt) for _, opt := range opts { diff --git a/internal/database/type.go b/internal/database/type.go index 6a781288a9..bd07e0bfde 100644 --- a/internal/database/type.go +++ b/internal/database/type.go @@ -225,3 +225,37 @@ func (d *NullDuration) Scan(src any) error { d.Duration, d.Valid = time.Duration(*duration), true return nil } + +// JSONArray allows sending and receiving JSON arrays to and from the database. +// It implements the [database/sql.Scanner] and [database/sql/driver.Valuer] interfaces. +// Values are marshaled and unmarshaled using the [encoding/json] package. +type JSONArray[T any] []T + +// NewJSONArray wraps an existing slice into a JSONArray. +func NewJSONArray[T any](a []T) JSONArray[T] { + return JSONArray[T](a) +} + +// Scan implements the [database/sql.Scanner] interface. +func (a *JSONArray[T]) Scan(src any) error { + if src == nil { + *a = nil + return nil + } + + bytes := src.([]byte) + if len(bytes) == 0 { + *a = nil + return nil + } + + return json.Unmarshal(bytes, a) +} + +// Value implements the [database/sql/driver.Valuer] interface. +func (a JSONArray[T]) Value() (driver.Value, error) { + if a == nil { + return nil, nil + } + return json.Marshal(a) +} diff --git a/internal/database/type_test.go b/internal/database/type_test.go index e56cdced76..7fab568a4e 100644 --- a/internal/database/type_test.go +++ b/internal/database/type_test.go @@ -5,6 +5,7 @@ import ( "database/sql/driver" "testing" + "github.com/muhlemmer/gu" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -452,3 +453,89 @@ func TestDuration_Scan(t *testing.T) { }) } } + +func TestJSONArray_Scan(t *testing.T) { + type args struct { + src any + } + tests := []struct { + name string + args args + want *JSONArray[string] + wantErr bool + }{ + { + name: "nil", + args: args{src: nil}, + want: new(JSONArray[string]), + wantErr: false, + }, + { + name: "zero bytes", + args: args{src: []byte("")}, + want: new(JSONArray[string]), + wantErr: false, + }, + { + name: "empty", + args: args{src: []byte("[]")}, + want: gu.Ptr(JSONArray[string]{}), + wantErr: false, + }, + { + name: "ok", + args: args{src: []byte("[\"a\", \"b\"]")}, + want: gu.Ptr(JSONArray[string]{"a", "b"}), + wantErr: false, + }, + { + name: "json error", + args: args{src: []byte("{\"a\": \"b\"}")}, + want: new(JSONArray[string]), + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := new(JSONArray[string]) + err := got.Scan(tt.args.src) + if tt.wantErr { + assert.Error(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestJSONArray_Value(t *testing.T) { + tests := []struct { + name string + a []string + want driver.Value + }{ + { + name: "nil", + a: nil, + want: nil, + }, + { + name: "empty", + a: []string{}, + want: []byte("[]"), + }, + { + name: "ok", + a: []string{"a", "b"}, + want: []byte("[\"a\",\"b\"]"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := NewJSONArray(tt.a).Value() + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/internal/domain/application_oidc.go b/internal/domain/application_oidc.go index 5d466c689d..10a70a1776 100644 --- a/internal/domain/application_oidc.go +++ b/internal/domain/application_oidc.go @@ -1,6 +1,7 @@ package domain import ( + "slices" "strings" "time" @@ -32,22 +33,22 @@ type OIDCApp struct { RedirectUris []string ResponseTypes []OIDCResponseType GrantTypes []OIDCGrantType - ApplicationType OIDCApplicationType - AuthMethodType OIDCAuthMethodType + ApplicationType *OIDCApplicationType + AuthMethodType *OIDCAuthMethodType PostLogoutRedirectUris []string - OIDCVersion OIDCVersion + OIDCVersion *OIDCVersion Compliance *Compliance - DevMode bool - AccessTokenType OIDCTokenType - AccessTokenRoleAssertion bool - IDTokenRoleAssertion bool - IDTokenUserinfoAssertion bool - ClockSkew time.Duration + DevMode *bool + AccessTokenType *OIDCTokenType + AccessTokenRoleAssertion *bool + IDTokenRoleAssertion *bool + IDTokenUserinfoAssertion *bool + ClockSkew *time.Duration AdditionalOrigins []string - SkipNativeAppSuccessPage bool - BackChannelLogoutURI string - LoginVersion LoginVersion - LoginBaseURI string + SkipNativeAppSuccessPage *bool + BackChannelLogoutURI *string + LoginVersion *LoginVersion + LoginBaseURI *string State AppState } @@ -69,7 +70,7 @@ func (a *OIDCApp) setClientSecret(encodedHash string) { } func (a *OIDCApp) requiresClientSecret() bool { - return a.AuthMethodType == OIDCAuthMethodTypeBasic || a.AuthMethodType == OIDCAuthMethodTypePost + return a.AuthMethodType != nil && (*a.AuthMethodType == OIDCAuthMethodTypeBasic || *a.AuthMethodType == OIDCAuthMethodTypePost) } type OIDCVersion int32 @@ -137,7 +138,7 @@ const ( ) func (a *OIDCApp) IsValid() bool { - if a.ClockSkew > time.Second*5 || a.ClockSkew < time.Second*0 || !a.OriginsValid() { + if (a.ClockSkew != nil && (*a.ClockSkew > time.Second*5 || *a.ClockSkew < time.Second*0)) || !a.OriginsValid() { return false } grantTypes := a.getRequiredGrantTypes() @@ -204,30 +205,25 @@ func ContainsOIDCGrantTypes(shouldContain, list []OIDCGrantType) bool { } func containsOIDCGrantType(grantTypes []OIDCGrantType, grantType OIDCGrantType) bool { - for _, gt := range grantTypes { - if gt == grantType { - return true - } - } - return false + return slices.Contains(grantTypes, grantType) } func (a *OIDCApp) FillCompliance() { a.Compliance = GetOIDCCompliance(a.OIDCVersion, a.ApplicationType, a.GrantTypes, a.ResponseTypes, a.AuthMethodType, a.RedirectUris) } -func GetOIDCCompliance(version OIDCVersion, appType OIDCApplicationType, grantTypes []OIDCGrantType, responseTypes []OIDCResponseType, authMethod OIDCAuthMethodType, redirectUris []string) *Compliance { - switch version { - case OIDCVersionV1: +func GetOIDCCompliance(version *OIDCVersion, appType *OIDCApplicationType, grantTypes []OIDCGrantType, responseTypes []OIDCResponseType, authMethod *OIDCAuthMethodType, redirectUris []string) *Compliance { + if version != nil && *version == OIDCVersionV1 { return GetOIDCV1Compliance(appType, grantTypes, authMethod, redirectUris) } + return &Compliance{ NoneCompliant: true, Problems: []string{"Application.OIDC.UnsupportedVersion"}, } } -func GetOIDCV1Compliance(appType OIDCApplicationType, grantTypes []OIDCGrantType, authMethod OIDCAuthMethodType, redirectUris []string) *Compliance { +func GetOIDCV1Compliance(appType *OIDCApplicationType, grantTypes []OIDCGrantType, authMethod *OIDCAuthMethodType, redirectUris []string) *Compliance { compliance := &Compliance{NoneCompliant: false} checkGrantTypesCombination(compliance, grantTypes) @@ -247,7 +243,7 @@ func checkGrantTypesCombination(compliance *Compliance, grantTypes []OIDCGrantTy } } -func checkRedirectURIs(compliance *Compliance, grantTypes []OIDCGrantType, appType OIDCApplicationType, redirectUris []string) { +func checkRedirectURIs(compliance *Compliance, grantTypes []OIDCGrantType, appType *OIDCApplicationType, redirectUris []string) { // See #5684 for OIDCGrantTypeDeviceCode and redirectUris further explanation if len(redirectUris) == 0 && (!containsOIDCGrantType(grantTypes, OIDCGrantTypeDeviceCode) || (containsOIDCGrantType(grantTypes, OIDCGrantTypeDeviceCode) && containsOIDCGrantType(grantTypes, OIDCGrantTypeAuthorizationCode))) { compliance.NoneCompliant = true @@ -266,53 +262,58 @@ func checkRedirectURIs(compliance *Compliance, grantTypes []OIDCGrantType, appTy } } -func checkApplicationType(compliance *Compliance, appType OIDCApplicationType, authMethod OIDCAuthMethodType) { - switch appType { - case OIDCApplicationTypeNative: - GetOIDCV1NativeApplicationCompliance(compliance, authMethod) - case OIDCApplicationTypeUserAgent: - GetOIDCV1UserAgentApplicationCompliance(compliance, authMethod) +func checkApplicationType(compliance *Compliance, appType *OIDCApplicationType, authMethod *OIDCAuthMethodType) { + if appType != nil { + switch *appType { + case OIDCApplicationTypeNative: + GetOIDCV1NativeApplicationCompliance(compliance, authMethod) + case OIDCApplicationTypeUserAgent: + GetOIDCV1UserAgentApplicationCompliance(compliance, authMethod) + case OIDCApplicationTypeWeb: + return + } } + if compliance.NoneCompliant { compliance.Problems = append([]string{"Application.OIDC.V1.NotCompliant"}, compliance.Problems...) } } -func GetOIDCV1NativeApplicationCompliance(compliance *Compliance, authMethod OIDCAuthMethodType) { - if authMethod != OIDCAuthMethodTypeNone { +func GetOIDCV1NativeApplicationCompliance(compliance *Compliance, authMethod *OIDCAuthMethodType) { + if authMethod != nil && *authMethod != OIDCAuthMethodTypeNone { compliance.NoneCompliant = true compliance.Problems = append(compliance.Problems, "Application.OIDC.V1.Native.AuthMethodType.NotNone") } } -func GetOIDCV1UserAgentApplicationCompliance(compliance *Compliance, authMethod OIDCAuthMethodType) { - if authMethod != OIDCAuthMethodTypeNone { +func GetOIDCV1UserAgentApplicationCompliance(compliance *Compliance, authMethod *OIDCAuthMethodType) { + if authMethod != nil && *authMethod != OIDCAuthMethodTypeNone { compliance.NoneCompliant = true compliance.Problems = append(compliance.Problems, "Application.OIDC.V1.UserAgent.AuthMethodType.NotNone") } } -func CheckRedirectUrisCode(compliance *Compliance, appType OIDCApplicationType, redirectUris []string) { +func CheckRedirectUrisCode(compliance *Compliance, appType *OIDCApplicationType, redirectUris []string) { if urlsAreHttps(redirectUris) { return } if urlContainsPrefix(redirectUris, http) { - if appType == OIDCApplicationTypeUserAgent { + if appType != nil && *appType == OIDCApplicationTypeUserAgent { compliance.NoneCompliant = true compliance.Problems = append(compliance.Problems, "Application.OIDC.V1.Code.RedirectUris.HttpOnlyForWeb") } - if appType == OIDCApplicationTypeNative && !onlyLocalhostIsHttp(redirectUris) { + if appType != nil && *appType == OIDCApplicationTypeNative && !onlyLocalhostIsHttp(redirectUris) { compliance.NoneCompliant = true compliance.Problems = append(compliance.Problems, "Application.OIDC.V1.Native.RedirectUris.MustBeHttpLocalhost") } } - if containsCustom(redirectUris) && appType != OIDCApplicationTypeNative { + if containsCustom(redirectUris) && appType != nil && *appType != OIDCApplicationTypeNative { compliance.NoneCompliant = true compliance.Problems = append(compliance.Problems, "Application.OIDC.V1.Code.RedirectUris.CustomOnlyForNative") } } -func CheckRedirectUrisImplicit(compliance *Compliance, appType OIDCApplicationType, redirectUris []string) { +func CheckRedirectUrisImplicit(compliance *Compliance, appType *OIDCApplicationType, redirectUris []string) { if urlsAreHttps(redirectUris) { return } @@ -321,7 +322,7 @@ func CheckRedirectUrisImplicit(compliance *Compliance, appType OIDCApplicationTy compliance.Problems = append(compliance.Problems, "Application.OIDC.V1.Implicit.RedirectUris.CustomNotAllowed") } if urlContainsPrefix(redirectUris, http) { - if appType == OIDCApplicationTypeNative { + if appType != nil && *appType == OIDCApplicationTypeNative { if !onlyLocalhostIsHttp(redirectUris) { compliance.NoneCompliant = true compliance.Problems = append(compliance.Problems, "Application.OIDC.V1.Native.RedirectUris.MustBeHttpLocalhost") @@ -333,20 +334,20 @@ func CheckRedirectUrisImplicit(compliance *Compliance, appType OIDCApplicationTy } } -func CheckRedirectUrisImplicitAndCode(compliance *Compliance, appType OIDCApplicationType, redirectUris []string) { +func CheckRedirectUrisImplicitAndCode(compliance *Compliance, appType *OIDCApplicationType, redirectUris []string) { if urlsAreHttps(redirectUris) { return } - if containsCustom(redirectUris) && appType != OIDCApplicationTypeNative { + if containsCustom(redirectUris) && appType != nil && *appType != OIDCApplicationTypeNative { compliance.NoneCompliant = true compliance.Problems = append(compliance.Problems, "Application.OIDC.V1.Implicit.RedirectUris.CustomNotAllowed") } if urlContainsPrefix(redirectUris, http) { - if appType == OIDCApplicationTypeUserAgent { + if appType != nil && *appType == OIDCApplicationTypeUserAgent { compliance.NoneCompliant = true compliance.Problems = append(compliance.Problems, "Application.OIDC.V1.Code.RedirectUris.HttpOnlyForWeb") } - if !onlyLocalhostIsHttp(redirectUris) && appType == OIDCApplicationTypeNative { + if !onlyLocalhostIsHttp(redirectUris) && appType != nil && *appType == OIDCApplicationTypeNative { compliance.NoneCompliant = true compliance.Problems = append(compliance.Problems, "Application.OIDC.V1.Native.RedirectUris.MustBeHttpLocalhost") } diff --git a/internal/domain/application_oidc_test.go b/internal/domain/application_oidc_test.go index b3d9488827..4208917cdd 100644 --- a/internal/domain/application_oidc_test.go +++ b/internal/domain/application_oidc_test.go @@ -6,6 +6,8 @@ import ( "testing" "time" + "github.com/muhlemmer/gu" + "github.com/zitadel/zitadel/internal/eventstore/v1/models" ) @@ -25,7 +27,7 @@ func TestApplicationValid(t *testing.T) { ObjectRoot: models.ObjectRoot{AggregateID: "AggregateID"}, AppID: "AppID", AppName: "AppName", - ClockSkew: time.Minute * 1, + ClockSkew: gu.Ptr(time.Minute * 1), ResponseTypes: []OIDCResponseType{OIDCResponseTypeCode}, GrantTypes: []OIDCGrantType{OIDCGrantTypeAuthorizationCode}, }, @@ -39,7 +41,7 @@ func TestApplicationValid(t *testing.T) { ObjectRoot: models.ObjectRoot{AggregateID: "AggregateID"}, AppID: "AppID", AppName: "AppName", - ClockSkew: time.Minute * -1, + ClockSkew: gu.Ptr(time.Minute * -1), ResponseTypes: []OIDCResponseType{OIDCResponseTypeCode}, GrantTypes: []OIDCGrantType{OIDCGrantTypeAuthorizationCode}, }, @@ -190,9 +192,9 @@ func TestApplicationValid(t *testing.T) { func TestGetOIDCV1Compliance(t *testing.T) { type args struct { - appType OIDCApplicationType + appType *OIDCApplicationType grantTypes []OIDCGrantType - authMethod OIDCAuthMethodType + authMethod *OIDCAuthMethodType redirectUris []string } tests := []struct { @@ -266,7 +268,7 @@ func Test_checkGrantTypesCombination(t *testing.T) { func Test_checkRedirectURIs(t *testing.T) { type args struct { grantTypes []OIDCGrantType - appType OIDCApplicationType + appType *OIDCApplicationType redirectUris []string } tests := []struct { @@ -304,7 +306,7 @@ func Test_checkRedirectURIs(t *testing.T) { args: args{ redirectUris: []string{"http://redirect.to/me"}, grantTypes: []OIDCGrantType{OIDCGrantTypeImplicit}, - appType: OIDCApplicationTypeUserAgent, + appType: gu.Ptr(OIDCApplicationTypeUserAgent), }, }, { @@ -316,7 +318,7 @@ func Test_checkRedirectURIs(t *testing.T) { args: args{ redirectUris: []string{"http://redirect.to/me"}, grantTypes: []OIDCGrantType{OIDCGrantTypeAuthorizationCode}, - appType: OIDCApplicationTypeUserAgent, + appType: gu.Ptr(OIDCApplicationTypeUserAgent), }, }, } @@ -338,7 +340,7 @@ func Test_checkRedirectURIs(t *testing.T) { func Test_CheckRedirectUrisImplicitAndCode(t *testing.T) { type args struct { - appType OIDCApplicationType + appType *OIDCApplicationType redirectUris []string } tests := []struct { @@ -356,17 +358,6 @@ func Test_CheckRedirectUrisImplicitAndCode(t *testing.T) { redirectUris: []string{"https://redirect.to/me"}, }, }, - // { - // name: "custom protocol, not native", - // want: &Compliance{ - // NoneCompliant: true, - // Problems: []string{"Application.OIDC.V1.Implicit.RedirectUris.CustomNotAllowed"}, - // }, - // args: args{ - // redirectUris: []string{"protocol://redirect.to/me"}, - // appType: OIDCApplicationTypeWeb, - // }, - // }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -386,7 +377,7 @@ func Test_CheckRedirectUrisImplicitAndCode(t *testing.T) { func TestCheckRedirectUrisImplicitAndCode(t *testing.T) { type args struct { - appType OIDCApplicationType + appType *OIDCApplicationType redirectUris []string } tests := []struct { @@ -402,7 +393,7 @@ func TestCheckRedirectUrisImplicitAndCode(t *testing.T) { { name: "custom protocol not native app", args: args{ - appType: OIDCApplicationTypeWeb, + appType: gu.Ptr(OIDCApplicationTypeWeb), redirectUris: []string{"custom://nirvana.com"}, }, want: &Compliance{ @@ -413,7 +404,7 @@ func TestCheckRedirectUrisImplicitAndCode(t *testing.T) { { name: "http localhost user agent app", args: args{ - appType: OIDCApplicationTypeUserAgent, + appType: gu.Ptr(OIDCApplicationTypeUserAgent), redirectUris: []string{"http://localhost:9009"}, }, want: &Compliance{ @@ -424,7 +415,7 @@ func TestCheckRedirectUrisImplicitAndCode(t *testing.T) { { name: "http, not only localhost native app", args: args{ - appType: OIDCApplicationTypeNative, + appType: gu.Ptr(OIDCApplicationTypeNative), redirectUris: []string{"http://nirvana.com", "http://localhost:9009"}, }, want: &Compliance{ @@ -435,7 +426,7 @@ func TestCheckRedirectUrisImplicitAndCode(t *testing.T) { { name: "not allowed combination", args: args{ - appType: OIDCApplicationTypeNative, + appType: gu.Ptr(OIDCApplicationTypeNative), redirectUris: []string{"https://nirvana.com", "cutom://nirvana.com"}, }, want: &Compliance{ @@ -461,7 +452,7 @@ func TestCheckRedirectUrisImplicitAndCode(t *testing.T) { func TestCheckRedirectUrisImplicit(t *testing.T) { type args struct { - appType OIDCApplicationType + appType *OIDCApplicationType redirectUris []string } tests := []struct { @@ -488,7 +479,7 @@ func TestCheckRedirectUrisImplicit(t *testing.T) { name: "only http protocol, app type native, not only localhost", args: args{ redirectUris: []string{"http://nirvana.com"}, - appType: OIDCApplicationTypeNative, + appType: gu.Ptr(OIDCApplicationTypeNative), }, want: &Compliance{ NoneCompliant: true, @@ -499,7 +490,7 @@ func TestCheckRedirectUrisImplicit(t *testing.T) { name: "only http protocol, app type native, only localhost", args: args{ redirectUris: []string{"http://localhost:8080"}, - appType: OIDCApplicationTypeNative, + appType: gu.Ptr(OIDCApplicationTypeNative), }, want: &Compliance{ NoneCompliant: false, @@ -510,7 +501,7 @@ func TestCheckRedirectUrisImplicit(t *testing.T) { name: "only http protocol, app type web", args: args{ redirectUris: []string{"http://nirvana.com"}, - appType: OIDCApplicationTypeWeb, + appType: gu.Ptr(OIDCApplicationTypeWeb), }, want: &Compliance{ NoneCompliant: true, @@ -535,7 +526,7 @@ func TestCheckRedirectUrisImplicit(t *testing.T) { func TestCheckRedirectUrisCode(t *testing.T) { type args struct { - appType OIDCApplicationType + appType *OIDCApplicationType redirectUris []string } tests := []struct { @@ -552,7 +543,7 @@ func TestCheckRedirectUrisCode(t *testing.T) { name: "custom prefix, app type web", args: args{ redirectUris: []string{"custom://nirvana.com"}, - appType: OIDCApplicationTypeWeb, + appType: gu.Ptr(OIDCApplicationTypeWeb), }, want: &Compliance{ NoneCompliant: true, @@ -563,7 +554,7 @@ func TestCheckRedirectUrisCode(t *testing.T) { name: "only http protocol, app type user agent", args: args{ redirectUris: []string{"http://nirvana.com"}, - appType: OIDCApplicationTypeUserAgent, + appType: gu.Ptr(OIDCApplicationTypeUserAgent), }, want: &Compliance{ NoneCompliant: true, @@ -574,7 +565,7 @@ func TestCheckRedirectUrisCode(t *testing.T) { name: "only http protocol, app type native, only localhost", args: args{ redirectUris: []string{"http://localhost:8080", "http://nirvana.com:8080"}, - appType: OIDCApplicationTypeNative, + appType: gu.Ptr(OIDCApplicationTypeNative), }, want: &Compliance{ NoneCompliant: true, @@ -585,7 +576,7 @@ func TestCheckRedirectUrisCode(t *testing.T) { name: "custom protocol, not native", args: args{ redirectUris: []string{"custom://nirvana.com"}, - appType: OIDCApplicationTypeWeb, + appType: gu.Ptr(OIDCApplicationTypeWeb), }, want: &Compliance{ NoneCompliant: true, diff --git a/internal/domain/application_saml.go b/internal/domain/application_saml.go index de7ef789ee..aff1875c7e 100644 --- a/internal/domain/application_saml.go +++ b/internal/domain/application_saml.go @@ -11,9 +11,9 @@ type SAMLApp struct { AppName string EntityID string Metadata []byte - MetadataURL string - LoginVersion LoginVersion - LoginBaseURI string + MetadataURL *string + LoginVersion *LoginVersion + LoginBaseURI *string State AppState } @@ -31,11 +31,14 @@ func (a *SAMLApp) GetMetadata() []byte { } func (a *SAMLApp) GetMetadataURL() string { - return a.MetadataURL + if a.MetadataURL != nil { + return *a.MetadataURL + } + return "" } func (a *SAMLApp) IsValid() bool { - if a.MetadataURL == "" && a.Metadata == nil { + if (a.MetadataURL == nil || *a.MetadataURL == "") && a.Metadata == nil { return false } return true diff --git a/internal/domain/count_trigger.go b/internal/domain/count_trigger.go new file mode 100644 index 0000000000..a29d125fe9 --- /dev/null +++ b/internal/domain/count_trigger.go @@ -0,0 +1,9 @@ +package domain + +//go:generate enumer -type CountParentType -transform lower -trimprefix CountParentType -sql +type CountParentType int + +const ( + CountParentTypeInstance CountParentType = iota + CountParentTypeOrganization +) diff --git a/internal/domain/countparenttype_enumer.go b/internal/domain/countparenttype_enumer.go new file mode 100644 index 0000000000..8691d97e62 --- /dev/null +++ b/internal/domain/countparenttype_enumer.go @@ -0,0 +1,109 @@ +// Code generated by "enumer -type CountParentType -transform lower -trimprefix CountParentType -sql"; DO NOT EDIT. + +package domain + +import ( + "database/sql/driver" + "fmt" + "strings" +) + +const _CountParentTypeName = "instanceorganization" + +var _CountParentTypeIndex = [...]uint8{0, 8, 20} + +const _CountParentTypeLowerName = "instanceorganization" + +func (i CountParentType) String() string { + if i < 0 || i >= CountParentType(len(_CountParentTypeIndex)-1) { + return fmt.Sprintf("CountParentType(%d)", i) + } + return _CountParentTypeName[_CountParentTypeIndex[i]:_CountParentTypeIndex[i+1]] +} + +// An "invalid array index" compiler error signifies that the constant values have changed. +// Re-run the stringer command to generate them again. +func _CountParentTypeNoOp() { + var x [1]struct{} + _ = x[CountParentTypeInstance-(0)] + _ = x[CountParentTypeOrganization-(1)] +} + +var _CountParentTypeValues = []CountParentType{CountParentTypeInstance, CountParentTypeOrganization} + +var _CountParentTypeNameToValueMap = map[string]CountParentType{ + _CountParentTypeName[0:8]: CountParentTypeInstance, + _CountParentTypeLowerName[0:8]: CountParentTypeInstance, + _CountParentTypeName[8:20]: CountParentTypeOrganization, + _CountParentTypeLowerName[8:20]: CountParentTypeOrganization, +} + +var _CountParentTypeNames = []string{ + _CountParentTypeName[0:8], + _CountParentTypeName[8:20], +} + +// CountParentTypeString retrieves an enum value from the enum constants string name. +// Throws an error if the param is not part of the enum. +func CountParentTypeString(s string) (CountParentType, error) { + if val, ok := _CountParentTypeNameToValueMap[s]; ok { + return val, nil + } + + if val, ok := _CountParentTypeNameToValueMap[strings.ToLower(s)]; ok { + return val, nil + } + return 0, fmt.Errorf("%s does not belong to CountParentType values", s) +} + +// CountParentTypeValues returns all values of the enum +func CountParentTypeValues() []CountParentType { + return _CountParentTypeValues +} + +// CountParentTypeStrings returns a slice of all String values of the enum +func CountParentTypeStrings() []string { + strs := make([]string, len(_CountParentTypeNames)) + copy(strs, _CountParentTypeNames) + return strs +} + +// IsACountParentType returns "true" if the value is listed in the enum definition. "false" otherwise +func (i CountParentType) IsACountParentType() bool { + for _, v := range _CountParentTypeValues { + if i == v { + return true + } + } + return false +} + +func (i CountParentType) Value() (driver.Value, error) { + return i.String(), nil +} + +func (i *CountParentType) Scan(value interface{}) error { + if value == nil { + return nil + } + + var str string + switch v := value.(type) { + case []byte: + str = string(v) + case string: + str = v + case fmt.Stringer: + str = v.String() + default: + return fmt.Errorf("invalid value of CountParentType: %[1]T(%[1]v)", value) + } + + val, err := CountParentTypeString(str) + if err != nil { + return err + } + + *i = val + return nil +} diff --git a/internal/domain/member.go b/internal/domain/member.go index 821e65dfe3..76854a6568 100644 --- a/internal/domain/member.go +++ b/internal/domain/member.go @@ -25,10 +25,6 @@ func (i *Member) IsValid() bool { return i.AggregateID != "" && i.UserID != "" && len(i.Roles) != 0 } -func (i *Member) IsIAMValid() bool { - return i.UserID != "" && len(i.Roles) != 0 -} - type MemberState int32 const ( @@ -42,3 +38,7 @@ const ( func (f MemberState) Valid() bool { return f >= 0 && f < memberStateCount } + +func (f MemberState) Exists() bool { + return f != MemberStateRemoved && f != MemberStateUnspecified +} diff --git a/internal/domain/mock/permission.go b/internal/domain/mock/permission.go new file mode 100644 index 0000000000..9a3c6478c9 --- /dev/null +++ b/internal/domain/mock/permission.go @@ -0,0 +1,22 @@ +package permissionmock + +import ( + "golang.org/x/net/context" + + "github.com/zitadel/zitadel/internal/domain" +) + +// MockPermissionCheckErr returns a permission check function that will fail +// and return the input error +func MockPermissionCheckErr(err error) domain.PermissionCheck { + return func(_ context.Context, _, _, _ string) error { + return err + } +} + +// MockPermissionCheckOK returns a permission check function that will succeed +func MockPermissionCheckOK() domain.PermissionCheck { + return func(_ context.Context, _, _, _ string) (err error) { + return nil + } +} diff --git a/internal/domain/org.go b/internal/domain/org.go index d016dde99c..f9b1140d2b 100644 --- a/internal/domain/org.go +++ b/internal/domain/org.go @@ -43,3 +43,7 @@ const ( func (s OrgState) Valid() bool { return s > OrgStateUnspecified && s < orgStateMax } + +func (s OrgState) Exists() bool { + return s != OrgStateRemoved && s != OrgStateUnspecified +} diff --git a/internal/domain/permission.go b/internal/domain/permission.go index 0ddf08a664..1405991dae 100644 --- a/internal/domain/permission.go +++ b/internal/domain/permission.go @@ -24,20 +24,47 @@ func (p *Permissions) appendPermission(ctxID, permission string) { p.Permissions = append(p.Permissions, permission) } -type PermissionCheck func(ctx context.Context, permission, orgID, resourceID string) (err error) +type PermissionCheck func(ctx context.Context, permission, resourceOwnerID, aggregateID string) (err error) const ( - PermissionUserWrite = "user.write" - PermissionUserRead = "user.read" - PermissionUserDelete = "user.delete" - PermissionUserCredentialWrite = "user.credential.write" - PermissionSessionWrite = "session.write" - PermissionSessionRead = "session.read" - PermissionSessionLink = "session.link" - PermissionSessionDelete = "session.delete" - PermissionOrgRead = "org.read" - PermissionIDPRead = "iam.idp.read" - PermissionOrgIDPRead = "org.idp.read" + PermissionUserWrite = "user.write" + PermissionUserRead = "user.read" + PermissionUserDelete = "user.delete" + PermissionUserCredentialWrite = "user.credential.write" + PermissionSessionWrite = "session.write" + PermissionSessionRead = "session.read" + PermissionSessionLink = "session.link" + PermissionSessionDelete = "session.delete" + PermissionOrgRead = "org.read" + PermissionIDPRead = "iam.idp.read" + PermissionOrgIDPRead = "org.idp.read" + PermissionProjectWrite = "project.write" + PermissionProjectRead = "project.read" + PermissionProjectDelete = "project.delete" + PermissionProjectGrantWrite = "project.grant.write" + PermissionProjectGrantRead = "project.grant.read" + PermissionProjectGrantDelete = "project.grant.delete" + PermissionProjectRoleWrite = "project.role.write" + PermissionProjectRoleRead = "project.role.read" + PermissionProjectRoleDelete = "project.role.delete" + PermissionProjectAppWrite = "project.app.write" + PermissionProjectAppDelete = "project.app.delete" + PermissionProjectAppRead = "project.app.read" + PermissionInstanceMemberWrite = "iam.member.write" + PermissionInstanceMemberDelete = "iam.member.delete" + PermissionInstanceMemberRead = "iam.member.read" + PermissionOrgMemberWrite = "org.member.write" + PermissionOrgMemberDelete = "org.member.delete" + PermissionOrgMemberRead = "org.member.read" + PermissionProjectMemberWrite = "project.member.write" + PermissionProjectMemberDelete = "project.member.delete" + PermissionProjectMemberRead = "project.member.read" + PermissionProjectGrantMemberWrite = "project.grant.member.write" + PermissionProjectGrantMemberDelete = "project.grant.member.delete" + PermissionProjectGrantMemberRead = "project.grant.member.read" + PermissionUserGrantWrite = "user.grant.write" + PermissionUserGrantRead = "user.grant.read" + PermissionUserGrantDelete = "user.grant.delete" ) // ProjectPermissionCheck is used as a check for preconditions dependent on application, project, user resourceowner and usergrants. diff --git a/internal/domain/project_grant.go b/internal/domain/project_grant.go index 31c6d9edbc..6376c9ed74 100644 --- a/internal/domain/project_grant.go +++ b/internal/domain/project_grant.go @@ -26,17 +26,12 @@ func (s ProjectGrantState) Valid() bool { return s > ProjectGrantStateUnspecified && s < projectGrantStateMax } -func (p *ProjectGrant) IsValid() bool { - return p.GrantedOrgID != "" +func (s ProjectGrantState) Exists() bool { + return s != ProjectGrantStateUnspecified && s != ProjectGrantStateRemoved } -func (g *ProjectGrant) HasInvalidRoles(validRoles []string) bool { - for _, roleKey := range g.RoleKeys { - if !containsRoleKey(roleKey, validRoles) { - return true - } - } - return false +func (p *ProjectGrant) IsValid() bool { + return p.GrantedOrgID != "" } func GetRemovedRoles(existingRoles, newRoles []string) []string { diff --git a/internal/domain/project_role.go b/internal/domain/project_role.go index e6c782bad1..c3593a1517 100644 --- a/internal/domain/project_role.go +++ b/internal/domain/project_role.go @@ -20,14 +20,23 @@ const ( ProjectRoleStateRemoved ) -func NewProjectRole(projectID, key string) *ProjectRole { - return &ProjectRole{ObjectRoot: models.ObjectRoot{AggregateID: projectID}, Key: key} +func (s ProjectRoleState) Exists() bool { + return s != ProjectRoleStateUnspecified && s != ProjectRoleStateRemoved } func (p *ProjectRole) IsValid() bool { return p.AggregateID != "" && p.Key != "" } +func HasInvalidRoles(validRoles, roles []string) bool { + for _, roleKey := range roles { + if !containsRoleKey(roleKey, validRoles) { + return true + } + } + return false +} + func containsRoleKey(roleKey string, validRoles []string) bool { for _, validRole := range validRoles { if roleKey == validRole { diff --git a/internal/domain/roles.go b/internal/domain/roles.go index b6bf2ffadd..2cebd26d30 100644 --- a/internal/domain/roles.go +++ b/internal/domain/roles.go @@ -14,8 +14,10 @@ const ( RoleOrgOwner = "ORG_OWNER" RoleOrgProjectCreator = "ORG_PROJECT_CREATOR" RoleIAMOwner = "IAM_OWNER" + RoleIAMLoginClient = "IAM_LOGIN_CLIENT" RoleProjectOwner = "PROJECT_OWNER" RoleProjectOwnerGlobal = "PROJECT_OWNER_GLOBAL" + RoleProjectGrantOwner = "PROJECT_GRANT_OWNER" RoleSelfManagementGlobal = "SELF_MANAGEMENT_GLOBAL" ) diff --git a/internal/domain/secretgeneratortype_enumer.go b/internal/domain/secretgeneratortype_enumer.go index f819bafc1f..db66715670 100644 --- a/internal/domain/secretgeneratortype_enumer.go +++ b/internal/domain/secretgeneratortype_enumer.go @@ -4,11 +4,14 @@ package domain import ( "fmt" + "strings" ) -const _SecretGeneratorTypeName = "unspecifiedinit_codeverify_email_codeverify_phone_codeverify_domainpassword_reset_codepasswordless_init_codeapp_secretotpsmsotp_emailinvite_codesecret_generator_type_count" +const _SecretGeneratorTypeName = "unspecifiedinit_codeverify_email_codeverify_phone_codeverify_domainpassword_reset_codepasswordless_init_codeapp_secretotpsmsotp_emailinvite_codesigning_keysecret_generator_type_count" -var _SecretGeneratorTypeIndex = [...]uint8{0, 11, 20, 37, 54, 67, 86, 108, 118, 124, 133, 144, 171} +var _SecretGeneratorTypeIndex = [...]uint8{0, 11, 20, 37, 54, 67, 86, 108, 118, 124, 133, 144, 155, 182} + +const _SecretGeneratorTypeLowerName = "unspecifiedinit_codeverify_email_codeverify_phone_codeverify_domainpassword_reset_codepasswordless_init_codeapp_secretotpsmsotp_emailinvite_codesigning_keysecret_generator_type_count" func (i SecretGeneratorType) String() string { if i < 0 || i >= SecretGeneratorType(len(_SecretGeneratorTypeIndex)-1) { @@ -17,21 +20,70 @@ func (i SecretGeneratorType) String() string { return _SecretGeneratorTypeName[_SecretGeneratorTypeIndex[i]:_SecretGeneratorTypeIndex[i+1]] } -var _SecretGeneratorTypeValues = []SecretGeneratorType{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11} +// An "invalid array index" compiler error signifies that the constant values have changed. +// Re-run the stringer command to generate them again. +func _SecretGeneratorTypeNoOp() { + var x [1]struct{} + _ = x[SecretGeneratorTypeUnspecified-(0)] + _ = x[SecretGeneratorTypeInitCode-(1)] + _ = x[SecretGeneratorTypeVerifyEmailCode-(2)] + _ = x[SecretGeneratorTypeVerifyPhoneCode-(3)] + _ = x[SecretGeneratorTypeVerifyDomain-(4)] + _ = x[SecretGeneratorTypePasswordResetCode-(5)] + _ = x[SecretGeneratorTypePasswordlessInitCode-(6)] + _ = x[SecretGeneratorTypeAppSecret-(7)] + _ = x[SecretGeneratorTypeOTPSMS-(8)] + _ = x[SecretGeneratorTypeOTPEmail-(9)] + _ = x[SecretGeneratorTypeInviteCode-(10)] + _ = x[SecretGeneratorTypeSigningKey-(11)] + _ = x[secretGeneratorTypeCount-(12)] +} + +var _SecretGeneratorTypeValues = []SecretGeneratorType{SecretGeneratorTypeUnspecified, SecretGeneratorTypeInitCode, SecretGeneratorTypeVerifyEmailCode, SecretGeneratorTypeVerifyPhoneCode, SecretGeneratorTypeVerifyDomain, SecretGeneratorTypePasswordResetCode, SecretGeneratorTypePasswordlessInitCode, SecretGeneratorTypeAppSecret, SecretGeneratorTypeOTPSMS, SecretGeneratorTypeOTPEmail, SecretGeneratorTypeInviteCode, SecretGeneratorTypeSigningKey, secretGeneratorTypeCount} var _SecretGeneratorTypeNameToValueMap = map[string]SecretGeneratorType{ - _SecretGeneratorTypeName[0:11]: 0, - _SecretGeneratorTypeName[11:20]: 1, - _SecretGeneratorTypeName[20:37]: 2, - _SecretGeneratorTypeName[37:54]: 3, - _SecretGeneratorTypeName[54:67]: 4, - _SecretGeneratorTypeName[67:86]: 5, - _SecretGeneratorTypeName[86:108]: 6, - _SecretGeneratorTypeName[108:118]: 7, - _SecretGeneratorTypeName[118:124]: 8, - _SecretGeneratorTypeName[124:133]: 9, - _SecretGeneratorTypeName[133:144]: 10, - _SecretGeneratorTypeName[144:171]: 11, + _SecretGeneratorTypeName[0:11]: SecretGeneratorTypeUnspecified, + _SecretGeneratorTypeLowerName[0:11]: SecretGeneratorTypeUnspecified, + _SecretGeneratorTypeName[11:20]: SecretGeneratorTypeInitCode, + _SecretGeneratorTypeLowerName[11:20]: SecretGeneratorTypeInitCode, + _SecretGeneratorTypeName[20:37]: SecretGeneratorTypeVerifyEmailCode, + _SecretGeneratorTypeLowerName[20:37]: SecretGeneratorTypeVerifyEmailCode, + _SecretGeneratorTypeName[37:54]: SecretGeneratorTypeVerifyPhoneCode, + _SecretGeneratorTypeLowerName[37:54]: SecretGeneratorTypeVerifyPhoneCode, + _SecretGeneratorTypeName[54:67]: SecretGeneratorTypeVerifyDomain, + _SecretGeneratorTypeLowerName[54:67]: SecretGeneratorTypeVerifyDomain, + _SecretGeneratorTypeName[67:86]: SecretGeneratorTypePasswordResetCode, + _SecretGeneratorTypeLowerName[67:86]: SecretGeneratorTypePasswordResetCode, + _SecretGeneratorTypeName[86:108]: SecretGeneratorTypePasswordlessInitCode, + _SecretGeneratorTypeLowerName[86:108]: SecretGeneratorTypePasswordlessInitCode, + _SecretGeneratorTypeName[108:118]: SecretGeneratorTypeAppSecret, + _SecretGeneratorTypeLowerName[108:118]: SecretGeneratorTypeAppSecret, + _SecretGeneratorTypeName[118:124]: SecretGeneratorTypeOTPSMS, + _SecretGeneratorTypeLowerName[118:124]: SecretGeneratorTypeOTPSMS, + _SecretGeneratorTypeName[124:133]: SecretGeneratorTypeOTPEmail, + _SecretGeneratorTypeLowerName[124:133]: SecretGeneratorTypeOTPEmail, + _SecretGeneratorTypeName[133:144]: SecretGeneratorTypeInviteCode, + _SecretGeneratorTypeLowerName[133:144]: SecretGeneratorTypeInviteCode, + _SecretGeneratorTypeName[144:155]: SecretGeneratorTypeSigningKey, + _SecretGeneratorTypeLowerName[144:155]: SecretGeneratorTypeSigningKey, + _SecretGeneratorTypeName[155:182]: secretGeneratorTypeCount, + _SecretGeneratorTypeLowerName[155:182]: secretGeneratorTypeCount, +} + +var _SecretGeneratorTypeNames = []string{ + _SecretGeneratorTypeName[0:11], + _SecretGeneratorTypeName[11:20], + _SecretGeneratorTypeName[20:37], + _SecretGeneratorTypeName[37:54], + _SecretGeneratorTypeName[54:67], + _SecretGeneratorTypeName[67:86], + _SecretGeneratorTypeName[86:108], + _SecretGeneratorTypeName[108:118], + _SecretGeneratorTypeName[118:124], + _SecretGeneratorTypeName[124:133], + _SecretGeneratorTypeName[133:144], + _SecretGeneratorTypeName[144:155], + _SecretGeneratorTypeName[155:182], } // SecretGeneratorTypeString retrieves an enum value from the enum constants string name. @@ -40,6 +92,10 @@ func SecretGeneratorTypeString(s string) (SecretGeneratorType, error) { if val, ok := _SecretGeneratorTypeNameToValueMap[s]; ok { return val, nil } + + if val, ok := _SecretGeneratorTypeNameToValueMap[strings.ToLower(s)]; ok { + return val, nil + } return 0, fmt.Errorf("%s does not belong to SecretGeneratorType values", s) } @@ -48,6 +104,13 @@ func SecretGeneratorTypeValues() []SecretGeneratorType { return _SecretGeneratorTypeValues } +// SecretGeneratorTypeStrings returns a slice of all String values of the enum +func SecretGeneratorTypeStrings() []string { + strs := make([]string, len(_SecretGeneratorTypeNames)) + copy(strs, _SecretGeneratorTypeNames) + return strs +} + // IsASecretGeneratorType returns "true" if the value is listed in the enum definition. "false" otherwise func (i SecretGeneratorType) IsASecretGeneratorType() bool { for _, v := range _SecretGeneratorTypeValues { diff --git a/internal/eventstore/write_model.go b/internal/eventstore/write_model.go index 277e65ed82..965fb16d0e 100644 --- a/internal/eventstore/write_model.go +++ b/internal/eventstore/write_model.go @@ -1,6 +1,8 @@ package eventstore -import "time" +import ( + "time" +) // WriteModel is the minimum representation of a command side write model. // It implements a basic reducer @@ -27,21 +29,25 @@ func (wm *WriteModel) Reduce() error { return nil } + latestEvent := wm.Events[len(wm.Events)-1] if wm.AggregateID == "" { - wm.AggregateID = wm.Events[0].Aggregate().ID - } - if wm.ResourceOwner == "" { - wm.ResourceOwner = wm.Events[0].Aggregate().ResourceOwner - } - if wm.InstanceID == "" { - wm.InstanceID = wm.Events[0].Aggregate().InstanceID + wm.AggregateID = latestEvent.Aggregate().ID } - wm.ProcessedSequence = wm.Events[len(wm.Events)-1].Sequence() - wm.ChangeDate = wm.Events[len(wm.Events)-1].CreatedAt() + if wm.ResourceOwner == "" { + wm.ResourceOwner = latestEvent.Aggregate().ResourceOwner + } + + if wm.InstanceID == "" { + wm.InstanceID = latestEvent.Aggregate().InstanceID + } + + wm.ProcessedSequence = latestEvent.Sequence() + wm.ChangeDate = latestEvent.CreatedAt() // all events processed and not needed anymore wm.Events = nil wm.Events = []Event{} + return nil } diff --git a/internal/feature/feature.go b/internal/feature/feature.go index b5f5a901d4..5e28338904 100644 --- a/internal/feature/feature.go +++ b/internal/feature/feature.go @@ -9,22 +9,20 @@ import ( type Key int const ( - KeyUnspecified Key = iota - KeyLoginDefaultOrg - KeyTriggerIntrospectionProjections - KeyLegacyIntrospection - KeyUserSchema - KeyTokenExchange - KeyActionsDeprecated - KeyImprovedPerformance - KeyWebKey - KeyDebugOIDCParentError - KeyOIDCSingleV1SessionTermination - KeyDisableUserTokenEvent - KeyEnableBackChannelLogout - KeyLoginV2 - KeyPermissionCheckV2 - KeyConsoleUseV2UserApi + // Reserved: 2, 3, 6, 8 + + KeyUnspecified Key = 0 + KeyLoginDefaultOrg Key = 1 + KeyUserSchema Key = 4 + KeyTokenExchange Key = 5 + KeyImprovedPerformance Key = 7 + KeyDebugOIDCParentError Key = 9 + KeyOIDCSingleV1SessionTermination Key = 10 + KeyDisableUserTokenEvent Key = 11 + KeyEnableBackChannelLogout Key = 12 + KeyLoginV2 Key = 13 + KeyPermissionCheckV2 Key = 14 + KeyConsoleUseV2UserApi Key = 15 ) //go:generate enumer -type Level -transform snake -trimprefix Level @@ -41,20 +39,17 @@ const ( ) type Features struct { - LoginDefaultOrg bool `json:"login_default_org,omitempty"` - TriggerIntrospectionProjections bool `json:"trigger_introspection_projections,omitempty"` - LegacyIntrospection bool `json:"legacy_introspection,omitempty"` - UserSchema bool `json:"user_schema,omitempty"` - TokenExchange bool `json:"token_exchange,omitempty"` - ImprovedPerformance []ImprovedPerformanceType `json:"improved_performance,omitempty"` - WebKey bool `json:"web_key,omitempty"` - DebugOIDCParentError bool `json:"debug_oidc_parent_error,omitempty"` - OIDCSingleV1SessionTermination bool `json:"oidc_single_v1_session_termination,omitempty"` - DisableUserTokenEvent bool `json:"disable_user_token_event,omitempty"` - EnableBackChannelLogout bool `json:"enable_back_channel_logout,omitempty"` - LoginV2 LoginV2 `json:"login_v2,omitempty"` - PermissionCheckV2 bool `json:"permission_check_v2,omitempty"` - ConsoleUseV2UserApi bool `json:"console_use_v2_user_api,omitempty"` + LoginDefaultOrg bool `json:"login_default_org,omitempty"` + UserSchema bool `json:"user_schema,omitempty"` + TokenExchange bool `json:"token_exchange,omitempty"` + ImprovedPerformance []ImprovedPerformanceType `json:"improved_performance,omitempty"` + DebugOIDCParentError bool `json:"debug_oidc_parent_error,omitempty"` + OIDCSingleV1SessionTermination bool `json:"oidc_single_v1_session_termination,omitempty"` + DisableUserTokenEvent bool `json:"disable_user_token_event,omitempty"` + EnableBackChannelLogout bool `json:"enable_back_channel_logout,omitempty"` + LoginV2 LoginV2 `json:"login_v2,omitempty"` + PermissionCheckV2 bool `json:"permission_check_v2,omitempty"` + ConsoleUseV2UserApi bool `json:"console_use_v2_user_api,omitempty"` } /* Note: do not generate the stringer or enumer for this type, is it breaks existing events */ diff --git a/internal/feature/feature_test.go b/internal/feature/feature_test.go index 70e3ec9ffb..d9a459e3db 100644 --- a/internal/feature/feature_test.go +++ b/internal/feature/feature_test.go @@ -11,8 +11,6 @@ func TestKey(t *testing.T) { tests := []string{ "unspecified", "login_default_org", - "trigger_introspection_projections", - "legacy_introspection", } for _, want := range tests { t.Run(want, func(t *testing.T) { diff --git a/internal/feature/key_enumer.go b/internal/feature/key_enumer.go index a47b3eb4d9..9d6f5877e0 100644 --- a/internal/feature/key_enumer.go +++ b/internal/feature/key_enumer.go @@ -7,17 +7,39 @@ import ( "strings" ) -const _KeyName = "unspecifiedlogin_default_orgtrigger_introspection_projectionslegacy_introspectionuser_schematoken_exchangeactions_deprecatedimproved_performanceweb_keydebug_oidc_parent_erroroidc_single_v1_session_terminationdisable_user_token_eventenable_back_channel_logoutlogin_v2permission_check_v2console_use_v2_user_api" +const ( + _KeyName_0 = "unspecifiedlogin_default_org" + _KeyLowerName_0 = "unspecifiedlogin_default_org" + _KeyName_1 = "user_schematoken_exchange" + _KeyLowerName_1 = "user_schematoken_exchange" + _KeyName_2 = "improved_performance" + _KeyLowerName_2 = "improved_performance" + _KeyName_3 = "debug_oidc_parent_erroroidc_single_v1_session_terminationdisable_user_token_eventenable_back_channel_logoutlogin_v2permission_check_v2console_use_v2_user_api" + _KeyLowerName_3 = "debug_oidc_parent_erroroidc_single_v1_session_terminationdisable_user_token_eventenable_back_channel_logoutlogin_v2permission_check_v2console_use_v2_user_api" +) -var _KeyIndex = [...]uint16{0, 11, 28, 61, 81, 92, 106, 124, 144, 151, 174, 208, 232, 258, 266, 285, 308} - -const _KeyLowerName = "unspecifiedlogin_default_orgtrigger_introspection_projectionslegacy_introspectionuser_schematoken_exchangeactions_deprecatedimproved_performanceweb_keydebug_oidc_parent_erroroidc_single_v1_session_terminationdisable_user_token_eventenable_back_channel_logoutlogin_v2permission_check_v2console_use_v2_user_api" +var ( + _KeyIndex_0 = [...]uint8{0, 11, 28} + _KeyIndex_1 = [...]uint8{0, 11, 25} + _KeyIndex_2 = [...]uint8{0, 20} + _KeyIndex_3 = [...]uint8{0, 23, 57, 81, 107, 115, 134, 157} +) func (i Key) String() string { - if i < 0 || i >= Key(len(_KeyIndex)-1) { + switch { + case 0 <= i && i <= 1: + return _KeyName_0[_KeyIndex_0[i]:_KeyIndex_0[i+1]] + case 4 <= i && i <= 5: + i -= 4 + return _KeyName_1[_KeyIndex_1[i]:_KeyIndex_1[i+1]] + case i == 7: + return _KeyName_2 + case 9 <= i && i <= 15: + i -= 9 + return _KeyName_3[_KeyIndex_3[i]:_KeyIndex_3[i+1]] + default: return fmt.Sprintf("Key(%d)", i) } - return _KeyName[_KeyIndex[i]:_KeyIndex[i+1]] } // An "invalid array index" compiler error signifies that the constant values have changed. @@ -26,13 +48,9 @@ func _KeyNoOp() { var x [1]struct{} _ = x[KeyUnspecified-(0)] _ = x[KeyLoginDefaultOrg-(1)] - _ = x[KeyTriggerIntrospectionProjections-(2)] - _ = x[KeyLegacyIntrospection-(3)] _ = x[KeyUserSchema-(4)] _ = x[KeyTokenExchange-(5)] - _ = x[KeyActionsDeprecated-(6)] _ = x[KeyImprovedPerformance-(7)] - _ = x[KeyWebKey-(8)] _ = x[KeyDebugOIDCParentError-(9)] _ = x[KeyOIDCSingleV1SessionTermination-(10)] _ = x[KeyDisableUserTokenEvent-(11)] @@ -42,60 +60,48 @@ func _KeyNoOp() { _ = x[KeyConsoleUseV2UserApi-(15)] } -var _KeyValues = []Key{KeyUnspecified, KeyLoginDefaultOrg, KeyTriggerIntrospectionProjections, KeyLegacyIntrospection, KeyUserSchema, KeyTokenExchange, KeyActionsDeprecated, KeyImprovedPerformance, KeyWebKey, KeyDebugOIDCParentError, KeyOIDCSingleV1SessionTermination, KeyDisableUserTokenEvent, KeyEnableBackChannelLogout, KeyLoginV2, KeyPermissionCheckV2, KeyConsoleUseV2UserApi} +var _KeyValues = []Key{KeyUnspecified, KeyLoginDefaultOrg, KeyUserSchema, KeyTokenExchange, KeyImprovedPerformance, KeyDebugOIDCParentError, KeyOIDCSingleV1SessionTermination, KeyDisableUserTokenEvent, KeyEnableBackChannelLogout, KeyLoginV2, KeyPermissionCheckV2, KeyConsoleUseV2UserApi} var _KeyNameToValueMap = map[string]Key{ - _KeyName[0:11]: KeyUnspecified, - _KeyLowerName[0:11]: KeyUnspecified, - _KeyName[11:28]: KeyLoginDefaultOrg, - _KeyLowerName[11:28]: KeyLoginDefaultOrg, - _KeyName[28:61]: KeyTriggerIntrospectionProjections, - _KeyLowerName[28:61]: KeyTriggerIntrospectionProjections, - _KeyName[61:81]: KeyLegacyIntrospection, - _KeyLowerName[61:81]: KeyLegacyIntrospection, - _KeyName[81:92]: KeyUserSchema, - _KeyLowerName[81:92]: KeyUserSchema, - _KeyName[92:106]: KeyTokenExchange, - _KeyLowerName[92:106]: KeyTokenExchange, - _KeyName[106:124]: KeyActionsDeprecated, - _KeyLowerName[106:124]: KeyActionsDeprecated, - _KeyName[124:144]: KeyImprovedPerformance, - _KeyLowerName[124:144]: KeyImprovedPerformance, - _KeyName[144:151]: KeyWebKey, - _KeyLowerName[144:151]: KeyWebKey, - _KeyName[151:174]: KeyDebugOIDCParentError, - _KeyLowerName[151:174]: KeyDebugOIDCParentError, - _KeyName[174:208]: KeyOIDCSingleV1SessionTermination, - _KeyLowerName[174:208]: KeyOIDCSingleV1SessionTermination, - _KeyName[208:232]: KeyDisableUserTokenEvent, - _KeyLowerName[208:232]: KeyDisableUserTokenEvent, - _KeyName[232:258]: KeyEnableBackChannelLogout, - _KeyLowerName[232:258]: KeyEnableBackChannelLogout, - _KeyName[258:266]: KeyLoginV2, - _KeyLowerName[258:266]: KeyLoginV2, - _KeyName[266:285]: KeyPermissionCheckV2, - _KeyLowerName[266:285]: KeyPermissionCheckV2, - _KeyName[285:308]: KeyConsoleUseV2UserApi, - _KeyLowerName[285:308]: KeyConsoleUseV2UserApi, + _KeyName_0[0:11]: KeyUnspecified, + _KeyLowerName_0[0:11]: KeyUnspecified, + _KeyName_0[11:28]: KeyLoginDefaultOrg, + _KeyLowerName_0[11:28]: KeyLoginDefaultOrg, + _KeyName_1[0:11]: KeyUserSchema, + _KeyLowerName_1[0:11]: KeyUserSchema, + _KeyName_1[11:25]: KeyTokenExchange, + _KeyLowerName_1[11:25]: KeyTokenExchange, + _KeyName_2[0:20]: KeyImprovedPerformance, + _KeyLowerName_2[0:20]: KeyImprovedPerformance, + _KeyName_3[0:23]: KeyDebugOIDCParentError, + _KeyLowerName_3[0:23]: KeyDebugOIDCParentError, + _KeyName_3[23:57]: KeyOIDCSingleV1SessionTermination, + _KeyLowerName_3[23:57]: KeyOIDCSingleV1SessionTermination, + _KeyName_3[57:81]: KeyDisableUserTokenEvent, + _KeyLowerName_3[57:81]: KeyDisableUserTokenEvent, + _KeyName_3[81:107]: KeyEnableBackChannelLogout, + _KeyLowerName_3[81:107]: KeyEnableBackChannelLogout, + _KeyName_3[107:115]: KeyLoginV2, + _KeyLowerName_3[107:115]: KeyLoginV2, + _KeyName_3[115:134]: KeyPermissionCheckV2, + _KeyLowerName_3[115:134]: KeyPermissionCheckV2, + _KeyName_3[134:157]: KeyConsoleUseV2UserApi, + _KeyLowerName_3[134:157]: KeyConsoleUseV2UserApi, } var _KeyNames = []string{ - _KeyName[0:11], - _KeyName[11:28], - _KeyName[28:61], - _KeyName[61:81], - _KeyName[81:92], - _KeyName[92:106], - _KeyName[106:124], - _KeyName[124:144], - _KeyName[144:151], - _KeyName[151:174], - _KeyName[174:208], - _KeyName[208:232], - _KeyName[232:258], - _KeyName[258:266], - _KeyName[266:285], - _KeyName[285:308], + _KeyName_0[0:11], + _KeyName_0[11:28], + _KeyName_1[0:11], + _KeyName_1[11:25], + _KeyName_2[0:20], + _KeyName_3[0:23], + _KeyName_3[23:57], + _KeyName_3[57:81], + _KeyName_3[81:107], + _KeyName_3[107:115], + _KeyName_3[115:134], + _KeyName_3[134:157], } // KeyString retrieves an enum value from the enum constants string name. diff --git a/internal/idp/providers/apple/apple_test.go b/internal/idp/providers/apple/apple_test.go index f3b7e81a1a..7d1f3a8481 100644 --- a/internal/idp/providers/apple/apple_test.go +++ b/internal/idp/providers/apple/apple_test.go @@ -62,10 +62,10 @@ func TestProvider_BeginAuth(t *testing.T) { ctx := context.Background() session, err := provider.BeginAuth(ctx, "testState") r.NoError(err) - content, redirect := session.GetAuth(ctx) - contentExpected, redirectExpected := tt.want.GetAuth(ctx) - a.Equal(redirectExpected, redirect) - a.Equal(contentExpected, content) + auth, err := session.GetAuth(ctx) + authExpected, errExpected := tt.want.GetAuth(ctx) + a.ErrorIs(err, errExpected) + a.Equal(authExpected, auth) }) } } diff --git a/internal/idp/providers/azuread/azuread_test.go b/internal/idp/providers/azuread/azuread_test.go index 122a70bb07..e46815cc8e 100644 --- a/internal/idp/providers/azuread/azuread_test.go +++ b/internal/idp/providers/azuread/azuread_test.go @@ -81,10 +81,10 @@ func TestProvider_BeginAuth(t *testing.T) { session, err := provider.BeginAuth(ctx, "testState") r.NoError(err) - wantHeaders, wantContent := tt.want.GetAuth(ctx) - gotHeaders, gotContent := session.GetAuth(ctx) - a.Equal(wantHeaders, gotHeaders) - a.Equal(wantContent, gotContent) + wantAuth, wantErr := tt.want.GetAuth(ctx) + gotAuth, gotErr := session.GetAuth(ctx) + a.Equal(wantAuth, gotAuth) + a.ErrorIs(gotErr, wantErr) }) } } diff --git a/internal/idp/providers/azuread/session.go b/internal/idp/providers/azuread/session.go index 169784fb58..f417897893 100644 --- a/internal/idp/providers/azuread/session.go +++ b/internal/idp/providers/azuread/session.go @@ -28,7 +28,7 @@ func NewSession(provider *Provider, code string) *Session { } // GetAuth implements the [idp.Provider] interface by calling the wrapped [oauth.Session]. -func (s *Session) GetAuth(ctx context.Context) (content string, redirect bool) { +func (s *Session) GetAuth(ctx context.Context) (idp.Auth, error) { return s.oauth().GetAuth(ctx) } diff --git a/internal/idp/providers/github/github_test.go b/internal/idp/providers/github/github_test.go index 6274b51841..42f03c050d 100644 --- a/internal/idp/providers/github/github_test.go +++ b/internal/idp/providers/github/github_test.go @@ -48,10 +48,10 @@ func TestProvider_BeginAuth(t *testing.T) { session, err := provider.BeginAuth(ctx, "testState") r.NoError(err) - wantHeaders, wantContent := tt.want.GetAuth(ctx) - gotHeaders, gotContent := session.GetAuth(ctx) - a.Equal(wantHeaders, gotHeaders) - a.Equal(wantContent, gotContent) + wantAuth, wantErr := tt.want.GetAuth(ctx) + gotAuth, gotErr := session.GetAuth(ctx) + a.Equal(wantAuth, gotAuth) + a.ErrorIs(gotErr, wantErr) }) } } diff --git a/internal/idp/providers/gitlab/gitlab_test.go b/internal/idp/providers/gitlab/gitlab_test.go index 24b813bc81..99b28c5003 100644 --- a/internal/idp/providers/gitlab/gitlab_test.go +++ b/internal/idp/providers/gitlab/gitlab_test.go @@ -59,10 +59,10 @@ func TestProvider_BeginAuth(t *testing.T) { session, err := provider.BeginAuth(ctx, "testState") r.NoError(err) - wantHeaders, wantContent := tt.want.GetAuth(ctx) - gotHeaders, gotContent := session.GetAuth(ctx) - a.Equal(wantHeaders, gotHeaders) - a.Equal(wantContent, gotContent) + wantAuth, wantErr := tt.want.GetAuth(ctx) + gotAuth, gotErr := session.GetAuth(ctx) + a.Equal(wantAuth, gotAuth) + a.ErrorIs(gotErr, wantErr) }) } } diff --git a/internal/idp/providers/google/google_test.go b/internal/idp/providers/google/google_test.go index b95f8eaf9f..b8f31b86e3 100644 --- a/internal/idp/providers/google/google_test.go +++ b/internal/idp/providers/google/google_test.go @@ -48,10 +48,10 @@ func TestProvider_BeginAuth(t *testing.T) { session, err := provider.BeginAuth(ctx, "testState") r.NoError(err) - wantHeaders, wantContent := tt.want.GetAuth(ctx) - gotHeaders, gotContent := session.GetAuth(ctx) - a.Equal(wantHeaders, gotHeaders) - a.Equal(wantContent, gotContent) + wantAuth, wantErr := tt.want.GetAuth(ctx) + gotAuth, gotErr := session.GetAuth(ctx) + a.Equal(wantAuth, gotAuth) + a.ErrorIs(gotErr, wantErr) }) } } diff --git a/internal/idp/providers/jwt/jwt_test.go b/internal/idp/providers/jwt/jwt_test.go index 5756c58e07..aba337d2ee 100644 --- a/internal/idp/providers/jwt/jwt_test.go +++ b/internal/idp/providers/jwt/jwt_test.go @@ -119,10 +119,10 @@ func TestProvider_BeginAuth(t *testing.T) { } if tt.want.err == nil { a.NoError(err) - wantHeaders, wantContent := tt.want.session.GetAuth(ctx) - gotHeaders, gotContent := session.GetAuth(ctx) - a.Equal(wantHeaders, gotHeaders) - a.Equal(wantContent, gotContent) + wantAuth, wantErr := tt.want.session.GetAuth(ctx) + gotAuth, gotErr := session.GetAuth(ctx) + a.Equal(wantAuth, gotAuth) + a.ErrorIs(gotErr, wantErr) } }) } diff --git a/internal/idp/providers/jwt/session.go b/internal/idp/providers/jwt/session.go index 85b164a9c5..0d91986fc9 100644 --- a/internal/idp/providers/jwt/session.go +++ b/internal/idp/providers/jwt/session.go @@ -42,7 +42,7 @@ func NewSessionFromRequest(provider *Provider, r *http.Request) *Session { } // GetAuth implements the [idp.Session] interface. -func (s *Session) GetAuth(ctx context.Context) (string, bool) { +func (s *Session) GetAuth(ctx context.Context) (idp.Auth, error) { return idp.Redirect(s.AuthURL) } diff --git a/internal/idp/providers/ldap/session.go b/internal/idp/providers/ldap/session.go index a78dd02d73..6a56cd6132 100644 --- a/internal/idp/providers/ldap/session.go +++ b/internal/idp/providers/ldap/session.go @@ -39,7 +39,7 @@ func NewSession(provider *Provider, username, password string) *Session { } // GetAuth implements the [idp.Session] interface. -func (s *Session) GetAuth(ctx context.Context) (string, bool) { +func (s *Session) GetAuth(ctx context.Context) (idp.Auth, error) { return idp.Redirect(s.loginUrl) } diff --git a/internal/idp/providers/oauth/oauth2_test.go b/internal/idp/providers/oauth/oauth2_test.go index 984315ac1f..93a0dd404f 100644 --- a/internal/idp/providers/oauth/oauth2_test.go +++ b/internal/idp/providers/oauth/oauth2_test.go @@ -80,10 +80,10 @@ func TestProvider_BeginAuth(t *testing.T) { session, err := provider.BeginAuth(ctx, "testState") r.NoError(err) - wantHeaders, wantContent := tt.want.GetAuth(ctx) - gotHeaders, gotContent := session.GetAuth(ctx) - a.Equal(wantHeaders, gotHeaders) - a.Equal(wantContent, gotContent) + wantAuth, wantErr := tt.want.GetAuth(ctx) + gotAuth, gotErr := session.GetAuth(ctx) + a.Equal(wantAuth, gotAuth) + a.ErrorIs(gotErr, wantErr) }) } } diff --git a/internal/idp/providers/oauth/session.go b/internal/idp/providers/oauth/session.go index c9e175d1cf..27d38b1740 100644 --- a/internal/idp/providers/oauth/session.go +++ b/internal/idp/providers/oauth/session.go @@ -37,7 +37,7 @@ func NewSession(provider *Provider, code string, idpArguments map[string]any) *S } // GetAuth implements the [idp.Session] interface. -func (s *Session) GetAuth(ctx context.Context) (string, bool) { +func (s *Session) GetAuth(ctx context.Context) (idp.Auth, error) { return idp.Redirect(s.AuthURL) } diff --git a/internal/idp/providers/oidc/oidc_test.go b/internal/idp/providers/oidc/oidc_test.go index a46f09f13f..86e23f95d2 100644 --- a/internal/idp/providers/oidc/oidc_test.go +++ b/internal/idp/providers/oidc/oidc_test.go @@ -98,10 +98,10 @@ func TestProvider_BeginAuth(t *testing.T) { session, err := provider.BeginAuth(ctx, "testState") r.NoError(err) - wantHeaders, wantContent := tt.want.GetAuth(ctx) - gotHeaders, gotContent := session.GetAuth(ctx) - a.Equal(wantHeaders, gotHeaders) - a.Equal(wantContent, gotContent) + wantAuth, wantErr := tt.want.GetAuth(ctx) + gotAuth, gotErr := session.GetAuth(ctx) + a.Equal(wantAuth, gotAuth) + a.ErrorIs(gotErr, wantErr) }) } } diff --git a/internal/idp/providers/oidc/session.go b/internal/idp/providers/oidc/session.go index 9e1e55baf5..08e277a9cc 100644 --- a/internal/idp/providers/oidc/session.go +++ b/internal/idp/providers/oidc/session.go @@ -33,7 +33,7 @@ func NewSession(provider *Provider, code string, idpArguments map[string]any) *S } // GetAuth implements the [idp.Session] interface. -func (s *Session) GetAuth(ctx context.Context) (string, bool) { +func (s *Session) GetAuth(ctx context.Context) (idp.Auth, error) { return idp.Redirect(s.AuthURL) } diff --git a/internal/idp/providers/saml/saml_test.go b/internal/idp/providers/saml/saml_test.go index 69ff231ccc..5e76e6dcaa 100644 --- a/internal/idp/providers/saml/saml_test.go +++ b/internal/idp/providers/saml/saml_test.go @@ -1,7 +1,9 @@ package saml import ( + "context" "encoding/xml" + "net/url" "testing" "time" @@ -11,10 +13,138 @@ import ( "github.com/stretchr/testify/require" "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/idp" "github.com/zitadel/zitadel/internal/idp/providers/saml/requesttracker" "github.com/zitadel/zitadel/internal/zerrors" ) +func TestProvider_BeginAuth(t *testing.T) { + requestTracker := requesttracker.New( + func(ctx context.Context, authRequestID, samlRequestID string) error { + assert.Equal(t, "state", authRequestID) + return nil + }, + func(ctx context.Context, authRequestID string) (*samlsp.TrackedRequest, error) { + return &samlsp.TrackedRequest{ + SAMLRequestID: "state", + Index: authRequestID, + }, nil + }, + ) + type fields struct { + name string + rootURL string + metadata []byte + certificate []byte + key []byte + options []ProviderOpts + } + type args struct { + state string + } + type want struct { + err func(error) bool + authType idp.Auth + ssoURL string + relayState string + } + tests := []struct { + name string + fields fields + args args + want want + }{ + { + name: "redirect binding, success", + fields: fields{ + name: "saml", + rootURL: "https://localhost:8080", + metadata: []byte("\n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n \n urn:oasis:names:tc:SAML:2.0:nameid-format:transient\n \n \n \n"), + certificate: []byte("-----BEGIN CERTIFICATE-----\nMIIC2zCCAcOgAwIBAgIIAy/jm1gAAdEwDQYJKoZIhvcNAQELBQAwEjEQMA4GA1UE\nChMHWklUQURFTDAeFw0yMzA4MzAwNzExMTVaFw0yNDA4MjkwNzExMTVaMBIxEDAO\nBgNVBAoTB1pJVEFERUwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDE\nd3TztGgSb3LBVZn8f60NbFCyZW+F9HPiMCr9F9T45Zc0fgmMwxId0WzRD5Y/3yc1\ndHJzt+Bsxvw12aUHbIPiothqk3lINoFzl2H/cSfIW3nehKyNOUqdBQ8B4mvaqH81\njTjoJ/JTJAwzglHk6JAWjhOyx9aep1yBqYa3QASeTaW9sxkpB0Co1L2UPNhuMwZq\n8RA9NkTfmYVcVBeNqihler5MhruFtqrv+J0ftwc1stw8uCN89ADyr4Ni+e+FeWar\nQs9Bkfc6KLF/5IXa9HCsHNPaaoYPY6I6RSaG4/DKoSKIEe1/GSVG1FTpZ8trUZxv\nU+xXS6gEalXcrJsiX8aXAgMBAAGjNTAzMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUE\nDDAKBggrBgEFBQcDATAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4IBAQCx\n/dRNIj0N/16zJhZR/ahkc2AkvDXYxyr4JRT5wK9GQDNl/oaX3debRuSi/tfaXFIX\naJA6PxM4J49ZaiEpLrKfxMz5kAhjKchCBEMcH3mGt+iNZH7EOyTvHjpGrP2OZrsh\nO17yrvN3HuQxIU6roJlqtZz2iAADsoPtwOO4D7hupm9XTMkSnAmlMWOo/q46Jz89\n1sMxB+dXmH/zV0wgwh0omZfLV0u89mvdq269VhcjNBpBYSnN1ccqYWd5iwziob3I\nvaavGHGfkbvRUn/tKftYuTK30q03R+e9YbmlWZ0v695owh2e/apCzowQsCKfSVC8\nOxVyt5XkHq1tWwVyBmFp\n-----END CERTIFICATE-----\n"), + key: []byte("-----BEGIN RSA PRIVATE KEY-----\nMIIEogIBAAKCAQEAxHd087RoEm9ywVWZ/H+tDWxQsmVvhfRz4jAq/RfU+OWXNH4J\njMMSHdFs0Q+WP98nNXRyc7fgbMb8NdmlB2yD4qLYapN5SDaBc5dh/3EnyFt53oSs\njTlKnQUPAeJr2qh/NY046CfyUyQMM4JR5OiQFo4TssfWnqdcgamGt0AEnk2lvbMZ\nKQdAqNS9lDzYbjMGavEQPTZE35mFXFQXjaooZXq+TIa7hbaq7/idH7cHNbLcPLgj\nfPQA8q+DYvnvhXlmq0LPQZH3Oiixf+SF2vRwrBzT2mqGD2OiOkUmhuPwyqEiiBHt\nfxklRtRU6WfLa1Gcb1PsV0uoBGpV3KybIl/GlwIDAQABAoIBAEQjDduLgOCL6Gem\n0X3hpdnW6/HC/jed/Sa//9jBECq2LYeWAqff64ON40hqOHi0YvvGA/+gEOSI6mWe\nsv5tIxxRz+6+cLybsq+tG96kluCE4TJMHy/nY7orS/YiWbd+4odnEApr+D3fbZ/b\nnZ1fDsHTyn8hkYx6jLmnWsJpIHDp7zxD76y7k2Bbg6DZrCGiVxngiLJk23dvz79W\np03lHLM7XE92aFwXQmhfxHGxrbuoB/9eY4ai5IHp36H4fw0vL6NXdNQAo/bhe0p9\nAYB7y0ZumF8Hg0Z/BmMeEzLy6HrYB+VE8cO93pNjhSyH+p2yDB/BlUyTiRLQAoM0\nVTmOZXECgYEA7NGlzpKNhyQEJihVqt0MW0LhKIO/xbBn+XgYfX6GpqPa/ucnMx5/\nVezpl3gK8IU4wPUhAyXXAHJiqNBcEeyxrw0MXLujDVMJgYaLysCLJdvMVgoY08mS\nK5IQivpbozpf4+0y3mOnA+Sy1kbfxv2X8xiWLODRQW3f3q/xoklwOR8CgYEA1GEe\nfaibOFTQAYcIVj77KXtBfYZsX3EGAyfAN9O7cKHq5oaxVstwnF47WxpuVtoKZxCZ\nbNm9D5WvQ9b+Ztpioe42tzwE7Bff/Osj868GcDdRPK7nFlh9N2yVn/D514dOYVwR\n4MBr1KrJzgRWt4QqS4H+to1GzudDTSNlG7gnK4kCgYBUi6AbOHzoYzZL/RhgcJwp\ntJ23nhmH1Su5h2OO4e3mbhcP66w19sxU+8iFN+kH5zfUw26utgKk+TE5vXExQQRK\nT2k7bg2PAzcgk80ybD0BHhA8I0yrx4m0nmfjhe/TPVLgh10iwgbtP+eM0i6v1vc5\nZWyvxu9N4ZEL6lpkqr0y1wKBgG/NAIQd8jhhTW7Aav8cAJQBsqQl038avJOEpYe+\nCnpsgoAAf/K0/f8TDCQVceh+t+MxtdK7fO9rWOxZjWsPo8Si5mLnUaAHoX4/OpnZ\nlYYVWMqdOEFnK+O1Yb7k2GFBdV2DXlX2dc1qavntBsls5ecB89id3pyk2aUN8Pf6\npYQhAoGAMGtrHFely9wyaxI0RTCyfmJbWZHGVGkv6ELK8wneJjdjl82XOBUGCg5q\naRCrTZ3dPitKwrUa6ibJCIFCIziiriBmjDvTHzkMvoJEap2TVxYNDR6IfINVsQ57\nlOsiC4A2uGq4Lbfld+gjoplJ5GX6qXtTgZ6m7eo0y7U6zm2tkN0=\n-----END RSA PRIVATE KEY-----\n"), + options: []ProviderOpts{ + WithCustomRequestTracker(requestTracker), + }, + }, + args: args{ + state: "state", + }, + want: want{ + authType: &idp.RedirectAuth{}, + ssoURL: "http://localhost:8000/sso", + relayState: "state", + }, + }, + { + name: "post binding, success", + fields: fields{ + name: "saml", + rootURL: "https://localhost:8080", + metadata: []byte("\n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n \n urn:oasis:names:tc:SAML:2.0:nameid-format:transient\n \n \n \n"), + certificate: []byte("-----BEGIN CERTIFICATE-----\nMIIC2zCCAcOgAwIBAgIIAy/jm1gAAdEwDQYJKoZIhvcNAQELBQAwEjEQMA4GA1UE\nChMHWklUQURFTDAeFw0yMzA4MzAwNzExMTVaFw0yNDA4MjkwNzExMTVaMBIxEDAO\nBgNVBAoTB1pJVEFERUwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDE\nd3TztGgSb3LBVZn8f60NbFCyZW+F9HPiMCr9F9T45Zc0fgmMwxId0WzRD5Y/3yc1\ndHJzt+Bsxvw12aUHbIPiothqk3lINoFzl2H/cSfIW3nehKyNOUqdBQ8B4mvaqH81\njTjoJ/JTJAwzglHk6JAWjhOyx9aep1yBqYa3QASeTaW9sxkpB0Co1L2UPNhuMwZq\n8RA9NkTfmYVcVBeNqihler5MhruFtqrv+J0ftwc1stw8uCN89ADyr4Ni+e+FeWar\nQs9Bkfc6KLF/5IXa9HCsHNPaaoYPY6I6RSaG4/DKoSKIEe1/GSVG1FTpZ8trUZxv\nU+xXS6gEalXcrJsiX8aXAgMBAAGjNTAzMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUE\nDDAKBggrBgEFBQcDATAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4IBAQCx\n/dRNIj0N/16zJhZR/ahkc2AkvDXYxyr4JRT5wK9GQDNl/oaX3debRuSi/tfaXFIX\naJA6PxM4J49ZaiEpLrKfxMz5kAhjKchCBEMcH3mGt+iNZH7EOyTvHjpGrP2OZrsh\nO17yrvN3HuQxIU6roJlqtZz2iAADsoPtwOO4D7hupm9XTMkSnAmlMWOo/q46Jz89\n1sMxB+dXmH/zV0wgwh0omZfLV0u89mvdq269VhcjNBpBYSnN1ccqYWd5iwziob3I\nvaavGHGfkbvRUn/tKftYuTK30q03R+e9YbmlWZ0v695owh2e/apCzowQsCKfSVC8\nOxVyt5XkHq1tWwVyBmFp\n-----END CERTIFICATE-----\n"), + key: []byte("-----BEGIN RSA PRIVATE KEY-----\nMIIEogIBAAKCAQEAxHd087RoEm9ywVWZ/H+tDWxQsmVvhfRz4jAq/RfU+OWXNH4J\njMMSHdFs0Q+WP98nNXRyc7fgbMb8NdmlB2yD4qLYapN5SDaBc5dh/3EnyFt53oSs\njTlKnQUPAeJr2qh/NY046CfyUyQMM4JR5OiQFo4TssfWnqdcgamGt0AEnk2lvbMZ\nKQdAqNS9lDzYbjMGavEQPTZE35mFXFQXjaooZXq+TIa7hbaq7/idH7cHNbLcPLgj\nfPQA8q+DYvnvhXlmq0LPQZH3Oiixf+SF2vRwrBzT2mqGD2OiOkUmhuPwyqEiiBHt\nfxklRtRU6WfLa1Gcb1PsV0uoBGpV3KybIl/GlwIDAQABAoIBAEQjDduLgOCL6Gem\n0X3hpdnW6/HC/jed/Sa//9jBECq2LYeWAqff64ON40hqOHi0YvvGA/+gEOSI6mWe\nsv5tIxxRz+6+cLybsq+tG96kluCE4TJMHy/nY7orS/YiWbd+4odnEApr+D3fbZ/b\nnZ1fDsHTyn8hkYx6jLmnWsJpIHDp7zxD76y7k2Bbg6DZrCGiVxngiLJk23dvz79W\np03lHLM7XE92aFwXQmhfxHGxrbuoB/9eY4ai5IHp36H4fw0vL6NXdNQAo/bhe0p9\nAYB7y0ZumF8Hg0Z/BmMeEzLy6HrYB+VE8cO93pNjhSyH+p2yDB/BlUyTiRLQAoM0\nVTmOZXECgYEA7NGlzpKNhyQEJihVqt0MW0LhKIO/xbBn+XgYfX6GpqPa/ucnMx5/\nVezpl3gK8IU4wPUhAyXXAHJiqNBcEeyxrw0MXLujDVMJgYaLysCLJdvMVgoY08mS\nK5IQivpbozpf4+0y3mOnA+Sy1kbfxv2X8xiWLODRQW3f3q/xoklwOR8CgYEA1GEe\nfaibOFTQAYcIVj77KXtBfYZsX3EGAyfAN9O7cKHq5oaxVstwnF47WxpuVtoKZxCZ\nbNm9D5WvQ9b+Ztpioe42tzwE7Bff/Osj868GcDdRPK7nFlh9N2yVn/D514dOYVwR\n4MBr1KrJzgRWt4QqS4H+to1GzudDTSNlG7gnK4kCgYBUi6AbOHzoYzZL/RhgcJwp\ntJ23nhmH1Su5h2OO4e3mbhcP66w19sxU+8iFN+kH5zfUw26utgKk+TE5vXExQQRK\nT2k7bg2PAzcgk80ybD0BHhA8I0yrx4m0nmfjhe/TPVLgh10iwgbtP+eM0i6v1vc5\nZWyvxu9N4ZEL6lpkqr0y1wKBgG/NAIQd8jhhTW7Aav8cAJQBsqQl038avJOEpYe+\nCnpsgoAAf/K0/f8TDCQVceh+t+MxtdK7fO9rWOxZjWsPo8Si5mLnUaAHoX4/OpnZ\nlYYVWMqdOEFnK+O1Yb7k2GFBdV2DXlX2dc1qavntBsls5ecB89id3pyk2aUN8Pf6\npYQhAoGAMGtrHFely9wyaxI0RTCyfmJbWZHGVGkv6ELK8wneJjdjl82XOBUGCg5q\naRCrTZ3dPitKwrUa6ibJCIFCIziiriBmjDvTHzkMvoJEap2TVxYNDR6IfINVsQ57\nlOsiC4A2uGq4Lbfld+gjoplJ5GX6qXtTgZ6m7eo0y7U6zm2tkN0=\n-----END RSA PRIVATE KEY-----\n"), + options: []ProviderOpts{ + WithCustomRequestTracker(requestTracker), + }, + }, + args: args{ + state: "state", + }, + want: want{ + authType: &idp.FormAuth{}, + ssoURL: "http://localhost:8000/sso", + relayState: "state", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := assert.New(t) + + provider, err := New( + tt.fields.name, + tt.fields.rootURL, + tt.fields.metadata, + tt.fields.certificate, + tt.fields.key, + tt.fields.options..., + ) + require.NoError(t, err) + + ctx := context.Background() + session, err := provider.BeginAuth(ctx, tt.args.state, nil) + if tt.want.err != nil && !tt.want.err(err) { + a.Fail("invalid error", err) + } + if tt.want.err == nil { + a.NoError(err) + gotAuth, gotErr := session.GetAuth(ctx) + a.NoError(gotErr) + a.IsType(tt.want.authType, gotAuth) + + var ssoURL, relayState, samlRequest string + switch auth := gotAuth.(type) { + case *idp.RedirectAuth: + gotRedirect, err := url.Parse(auth.RedirectURL) + a.NoError(err) + gotQuery := gotRedirect.Query() + + ssoURL = gotRedirect.Scheme + "://" + gotRedirect.Host + gotRedirect.Path + relayState = gotQuery.Get("RelayState") + samlRequest = gotQuery.Get("SAMLRequest") + case *idp.FormAuth: + ssoURL = auth.URL + relayState = auth.Fields["RelayState"] + samlRequest = auth.Fields["SAMLRequest"] + } + a.Equal(tt.want.ssoURL, ssoURL) + a.Equal(tt.want.relayState, relayState) + a.NotEmpty(samlRequest) + } + }) + } +} + func TestProvider_Options(t *testing.T) { type fields struct { name string diff --git a/internal/idp/providers/saml/session.go b/internal/idp/providers/saml/session.go index e2a1655a26..e1f32209b0 100644 --- a/internal/idp/providers/saml/session.go +++ b/internal/idp/providers/saml/session.go @@ -1,13 +1,14 @@ package saml import ( - "bytes" "context" + "encoding/base64" "errors" "net/http" "net/url" "time" + "github.com/beevik/etree" "github.com/crewjam/saml" "github.com/crewjam/saml/samlsp" @@ -43,22 +44,15 @@ func NewSession(provider *Provider, requestID string, request *http.Request) (*S } // GetAuth implements the [idp.Session] interface. -func (s *Session) GetAuth(ctx context.Context) (string, bool) { - url, _ := url.Parse(s.state) - resp := NewTempResponseWriter() - +func (s *Session) GetAuth(ctx context.Context) (idp.Auth, error) { + url, err := url.Parse(s.state) + if err != nil { + return nil, err + } request := &http.Request{ URL: url, } - s.ServiceProvider.HandleStartAuthFlow( - resp, - request.WithContext(ctx), - ) - - if location := resp.Header().Get("Location"); location != "" { - return idp.Redirect(location) - } - return idp.Form(resp.content.String()) + return s.auth(request.WithContext(ctx)) } // PersistentParameters implements the [idp.Session] interface. @@ -130,24 +124,57 @@ func (s *Session) transientMappingID() (string, error) { return "", zerrors.ThrowInvalidArgument(nil, "SAML-swwg2", "Errors.Intent.MissingSingleMappingAttribute") } -type TempResponseWriter struct { - header http.Header - content *bytes.Buffer -} - -func (w *TempResponseWriter) Header() http.Header { - return w.header -} - -func (w *TempResponseWriter) Write(content []byte) (int, error) { - return w.content.Write(content) -} - -func (w *TempResponseWriter) WriteHeader(statusCode int) {} - -func NewTempResponseWriter() *TempResponseWriter { - return &TempResponseWriter{ - header: map[string][]string{}, - content: bytes.NewBuffer([]byte{}), +// auth is a modified copy of the [samlsp.Middleware.HandleStartAuthFlow] method. +// Instead of writing the response to the http.ResponseWriter, it returns the auth request as an [idp.Auth]. +// In case of an error, it returns the error directly and does not write to the response. +func (s *Session) auth(r *http.Request) (idp.Auth, error) { + if r.URL.Path == s.ServiceProvider.ServiceProvider.AcsURL.Path { + // should never occur, but was handled in the original method, so we keep it here + return nil, zerrors.ThrowInvalidArgument(nil, "SAML-Eoi24", "don't wrap Middleware with RequireAccount") } + + var binding, bindingLocation string + if s.ServiceProvider.Binding != "" { + binding = s.ServiceProvider.Binding + bindingLocation = s.ServiceProvider.ServiceProvider.GetSSOBindingLocation(binding) + } else { + binding = saml.HTTPRedirectBinding + bindingLocation = s.ServiceProvider.ServiceProvider.GetSSOBindingLocation(binding) + if bindingLocation == "" { + binding = saml.HTTPPostBinding + bindingLocation = s.ServiceProvider.ServiceProvider.GetSSOBindingLocation(binding) + } + } + + authReq, err := s.ServiceProvider.ServiceProvider.MakeAuthenticationRequest(bindingLocation, binding, s.ServiceProvider.ResponseBinding) + if err != nil { + return nil, err + } + relayState, err := s.ServiceProvider.RequestTracker.TrackRequest(nil, r, authReq.ID) + if err != nil { + return nil, err + } + + if binding == saml.HTTPRedirectBinding { + redirectURL, err := authReq.Redirect(relayState, &s.ServiceProvider.ServiceProvider) + if err != nil { + return nil, err + } + return idp.Redirect(redirectURL.String()) + } + if binding == saml.HTTPPostBinding { + doc := etree.NewDocument() + doc.SetRoot(authReq.Element()) + reqBuf, err := doc.WriteToBytes() + if err != nil { + return nil, err + } + encodedReqBuf := base64.StdEncoding.EncodeToString(reqBuf) + return idp.Form(authReq.Destination, + map[string]string{ + "SAMLRequest": encodedReqBuf, + "RelayState": relayState, + }) + } + return nil, zerrors.ThrowInvalidArgument(nil, "SAML-Eoi24", "Errors.Intent.Invalid") } diff --git a/internal/idp/session.go b/internal/idp/session.go index fc593eb820..d0df3415bf 100644 --- a/internal/idp/session.go +++ b/internal/idp/session.go @@ -7,12 +7,29 @@ import ( // Session is the minimal implementation for a session of a 3rd party authentication [Provider] type Session interface { - GetAuth(ctx context.Context) (content string, redirect bool) + GetAuth(ctx context.Context) (Auth, error) PersistentParameters() map[string]any FetchUser(ctx context.Context) (User, error) ExpiresAt() time.Time } +type Auth interface { + auth() +} + +type RedirectAuth struct { + RedirectURL string +} + +func (r *RedirectAuth) auth() {} + +type FormAuth struct { + URL string + Fields map[string]string +} + +func (f *FormAuth) auth() {} + // SessionSupportsMigration is an optional extension to the Session interface. // It can be implemented to support migrating users, were the initial external id has changed because of a migration of the Provider type. // E.g. when a user was linked on a generic OIDC provider and this provider has now been migrated to an AzureAD provider. @@ -22,10 +39,13 @@ type SessionSupportsMigration interface { RetrievePreviousID() (previousID string, err error) } -func Redirect(redirectURL string) (string, bool) { - return redirectURL, true +func Redirect(redirectURL string) (*RedirectAuth, error) { + return &RedirectAuth{RedirectURL: redirectURL}, nil } -func Form(html string) (string, bool) { - return html, false +func Form(url string, fields map[string]string) (*FormAuth, error) { + return &FormAuth{ + URL: url, + Fields: fields, + }, nil } diff --git a/internal/integration/client.go b/internal/integration/client.go index 55c245a4d0..7fdb241a60 100644 --- a/internal/integration/client.go +++ b/internal/integration/client.go @@ -2,6 +2,7 @@ package integration import ( "context" + "encoding/base64" "fmt" "sync" "testing" @@ -16,16 +17,22 @@ import ( "google.golang.org/grpc/metadata" "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/structpb" + "google.golang.org/protobuf/types/known/timestamppb" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/integration/scim" - action "github.com/zitadel/zitadel/pkg/grpc/action/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/action/v2" + action_v2beta "github.com/zitadel/zitadel/pkg/grpc/action/v2beta" "github.com/zitadel/zitadel/pkg/grpc/admin" + app "github.com/zitadel/zitadel/pkg/grpc/app/v2beta" "github.com/zitadel/zitadel/pkg/grpc/auth" + authorization "github.com/zitadel/zitadel/pkg/grpc/authorization/v2beta" "github.com/zitadel/zitadel/pkg/grpc/feature/v2" feature_v2beta "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta" "github.com/zitadel/zitadel/pkg/grpc/idp" idp_pb "github.com/zitadel/zitadel/pkg/grpc/idp/v2" + instance "github.com/zitadel/zitadel/pkg/grpc/instance/v2beta" + internal_permission_v2beta "github.com/zitadel/zitadel/pkg/grpc/internal_permission/v2beta" mgmt "github.com/zitadel/zitadel/pkg/grpc/management" "github.com/zitadel/zitadel/pkg/grpc/object/v2" object_v3alpha "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha" @@ -33,6 +40,7 @@ import ( oidc_pb_v2beta "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta" "github.com/zitadel/zitadel/pkg/grpc/org/v2" org_v2beta "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" + project_v2beta "github.com/zitadel/zitadel/pkg/grpc/project/v2beta" user_v3alpha "github.com/zitadel/zitadel/pkg/grpc/resources/user/v3alpha" userschema_v3alpha "github.com/zitadel/zitadel/pkg/grpc/resources/userschema/v3alpha" saml_pb "github.com/zitadel/zitadel/pkg/grpc/saml/v2" @@ -43,33 +51,45 @@ import ( user_pb "github.com/zitadel/zitadel/pkg/grpc/user" user_v2 "github.com/zitadel/zitadel/pkg/grpc/user/v2" user_v2beta "github.com/zitadel/zitadel/pkg/grpc/user/v2beta" + webkey_v2 "github.com/zitadel/zitadel/pkg/grpc/webkey/v2" webkey_v2beta "github.com/zitadel/zitadel/pkg/grpc/webkey/v2beta" ) type Client struct { - CC *grpc.ClientConn - Admin admin.AdminServiceClient - Mgmt mgmt.ManagementServiceClient - Auth auth.AuthServiceClient - UserV2beta user_v2beta.UserServiceClient - UserV2 user_v2.UserServiceClient - SessionV2beta session_v2beta.SessionServiceClient - SessionV2 session.SessionServiceClient - SettingsV2beta settings_v2beta.SettingsServiceClient - SettingsV2 settings.SettingsServiceClient - OIDCv2beta oidc_pb_v2beta.OIDCServiceClient - OIDCv2 oidc_pb.OIDCServiceClient - OrgV2beta org_v2beta.OrganizationServiceClient - OrgV2 org.OrganizationServiceClient - ActionV2beta action.ActionServiceClient - FeatureV2beta feature_v2beta.FeatureServiceClient - FeatureV2 feature.FeatureServiceClient - UserSchemaV3 userschema_v3alpha.ZITADELUserSchemasClient - WebKeyV2Beta webkey_v2beta.WebKeyServiceClient - IDPv2 idp_pb.IdentityProviderServiceClient - UserV3Alpha user_v3alpha.ZITADELUsersClient - SAMLv2 saml_pb.SAMLServiceClient - SCIM *scim.Client + CC *grpc.ClientConn + Admin admin.AdminServiceClient + Mgmt mgmt.ManagementServiceClient + Auth auth.AuthServiceClient + UserV2beta user_v2beta.UserServiceClient + UserV2 user_v2.UserServiceClient + SessionV2beta session_v2beta.SessionServiceClient + SessionV2 session.SessionServiceClient + SettingsV2beta settings_v2beta.SettingsServiceClient + SettingsV2 settings.SettingsServiceClient + OIDCv2beta oidc_pb_v2beta.OIDCServiceClient + OIDCv2 oidc_pb.OIDCServiceClient + OrgV2beta org_v2beta.OrganizationServiceClient + OrgV2 org.OrganizationServiceClient + ActionV2beta action_v2beta.ActionServiceClient + ActionV2 action.ActionServiceClient + FeatureV2beta feature_v2beta.FeatureServiceClient + FeatureV2 feature.FeatureServiceClient + UserSchemaV3 userschema_v3alpha.ZITADELUserSchemasClient + WebKeyV2 webkey_v2.WebKeyServiceClient + WebKeyV2Beta webkey_v2beta.WebKeyServiceClient + IDPv2 idp_pb.IdentityProviderServiceClient + UserV3Alpha user_v3alpha.ZITADELUsersClient + SAMLv2 saml_pb.SAMLServiceClient + SCIM *scim.Client + Projectv2Beta project_v2beta.ProjectServiceClient + InstanceV2Beta instance.InstanceServiceClient + AppV2Beta app.AppServiceClient + InternalPermissionv2Beta internal_permission_v2beta.InternalPermissionServiceClient + AuthorizationV2Beta authorization.AuthorizationServiceClient +} + +func NewDefaultClient(ctx context.Context) (*Client, error) { + return newClient(ctx, loadedConfig.Host()) } func newClient(ctx context.Context, target string) (*Client, error) { @@ -80,29 +100,36 @@ func newClient(ctx context.Context, target string) (*Client, error) { return nil, err } client := &Client{ - CC: cc, - Admin: admin.NewAdminServiceClient(cc), - Mgmt: mgmt.NewManagementServiceClient(cc), - Auth: auth.NewAuthServiceClient(cc), - UserV2beta: user_v2beta.NewUserServiceClient(cc), - UserV2: user_v2.NewUserServiceClient(cc), - SessionV2beta: session_v2beta.NewSessionServiceClient(cc), - SessionV2: session.NewSessionServiceClient(cc), - SettingsV2beta: settings_v2beta.NewSettingsServiceClient(cc), - SettingsV2: settings.NewSettingsServiceClient(cc), - OIDCv2beta: oidc_pb_v2beta.NewOIDCServiceClient(cc), - OIDCv2: oidc_pb.NewOIDCServiceClient(cc), - OrgV2beta: org_v2beta.NewOrganizationServiceClient(cc), - OrgV2: org.NewOrganizationServiceClient(cc), - ActionV2beta: action.NewActionServiceClient(cc), - FeatureV2beta: feature_v2beta.NewFeatureServiceClient(cc), - FeatureV2: feature.NewFeatureServiceClient(cc), - UserSchemaV3: userschema_v3alpha.NewZITADELUserSchemasClient(cc), - WebKeyV2Beta: webkey_v2beta.NewWebKeyServiceClient(cc), - IDPv2: idp_pb.NewIdentityProviderServiceClient(cc), - UserV3Alpha: user_v3alpha.NewZITADELUsersClient(cc), - SAMLv2: saml_pb.NewSAMLServiceClient(cc), - SCIM: scim.NewScimClient(target), + CC: cc, + Admin: admin.NewAdminServiceClient(cc), + Mgmt: mgmt.NewManagementServiceClient(cc), + Auth: auth.NewAuthServiceClient(cc), + UserV2beta: user_v2beta.NewUserServiceClient(cc), + UserV2: user_v2.NewUserServiceClient(cc), + SessionV2beta: session_v2beta.NewSessionServiceClient(cc), + SessionV2: session.NewSessionServiceClient(cc), + SettingsV2beta: settings_v2beta.NewSettingsServiceClient(cc), + SettingsV2: settings.NewSettingsServiceClient(cc), + OIDCv2beta: oidc_pb_v2beta.NewOIDCServiceClient(cc), + OIDCv2: oidc_pb.NewOIDCServiceClient(cc), + OrgV2beta: org_v2beta.NewOrganizationServiceClient(cc), + OrgV2: org.NewOrganizationServiceClient(cc), + ActionV2beta: action_v2beta.NewActionServiceClient(cc), + ActionV2: action.NewActionServiceClient(cc), + FeatureV2beta: feature_v2beta.NewFeatureServiceClient(cc), + FeatureV2: feature.NewFeatureServiceClient(cc), + UserSchemaV3: userschema_v3alpha.NewZITADELUserSchemasClient(cc), + WebKeyV2: webkey_v2.NewWebKeyServiceClient(cc), + WebKeyV2Beta: webkey_v2beta.NewWebKeyServiceClient(cc), + IDPv2: idp_pb.NewIdentityProviderServiceClient(cc), + UserV3Alpha: user_v3alpha.NewZITADELUsersClient(cc), + SAMLv2: saml_pb.NewSAMLServiceClient(cc), + SCIM: scim.NewScimClient(target), + Projectv2Beta: project_v2beta.NewProjectServiceClient(cc), + InstanceV2Beta: instance.NewInstanceServiceClient(cc), + AppV2Beta: app.NewAppServiceClient(cc), + InternalPermissionv2Beta: internal_permission_v2beta.NewInternalPermissionServiceClient(cc), + AuthorizationV2Beta: authorization.NewAuthorizationServiceClient(cc), } return client, client.pollHealth(ctx) } @@ -131,6 +158,7 @@ func (c *Client) pollHealth(ctx context.Context) (err error) { } } +// Deprecated: use CreateUserTypeHuman instead func (i *Instance) CreateHumanUser(ctx context.Context) *user_v2.AddHumanUserResponse { resp, err := i.Client.UserV2.AddHumanUser(ctx, &user_v2.AddHumanUserRequest{ Organization: &object.Organization{ @@ -162,6 +190,7 @@ func (i *Instance) CreateHumanUser(ctx context.Context) *user_v2.AddHumanUserRes return resp } +// Deprecated: user CreateUserTypeHuman instead func (i *Instance) CreateHumanUserNoPhone(ctx context.Context) *user_v2.AddHumanUserResponse { resp, err := i.Client.UserV2.AddHumanUser(ctx, &user_v2.AddHumanUserRequest{ Organization: &object.Organization{ @@ -187,6 +216,7 @@ func (i *Instance) CreateHumanUserNoPhone(ctx context.Context) *user_v2.AddHuman return resp } +// Deprecated: user CreateUserTypeHuman instead func (i *Instance) CreateHumanUserWithTOTP(ctx context.Context, secret string) *user_v2.AddHumanUserResponse { resp, err := i.Client.UserV2.AddHumanUser(ctx, &user_v2.AddHumanUserRequest{ Organization: &object.Organization{ @@ -219,6 +249,74 @@ func (i *Instance) CreateHumanUserWithTOTP(ctx context.Context, secret string) * return resp } +func (i *Instance) SetUserMetadata(ctx context.Context, id, key, value string) *user_v2.SetUserMetadataResponse { + resp, err := i.Client.UserV2.SetUserMetadata(ctx, &user_v2.SetUserMetadataRequest{ + UserId: id, + Metadata: []*user_v2.Metadata{{ + Key: key, + Value: []byte(base64.StdEncoding.EncodeToString([]byte(value))), + }, + }, + }) + logging.OnError(err).Panic("set user metadata") + return resp +} + +func (i *Instance) DeleteUserMetadata(ctx context.Context, id, key string) *user_v2.DeleteUserMetadataResponse { + resp, err := i.Client.UserV2.DeleteUserMetadata(ctx, &user_v2.DeleteUserMetadataRequest{ + UserId: id, + Keys: []string{key}, + }) + logging.OnError(err).Panic("delete user metadata") + return resp +} + +func (i *Instance) CreateUserTypeHuman(ctx context.Context, email string) *user_v2.CreateUserResponse { + resp, err := i.Client.UserV2.CreateUser(ctx, &user_v2.CreateUserRequest{ + OrganizationId: i.DefaultOrg.GetId(), + UserType: &user_v2.CreateUserRequest_Human_{ + Human: &user_v2.CreateUserRequest_Human{ + Profile: &user_v2.SetHumanProfile{ + GivenName: "Mickey", + FamilyName: "Mouse", + }, + Email: &user_v2.SetHumanEmail{ + Email: email, + Verification: &user_v2.SetHumanEmail_ReturnCode{ + ReturnCode: &user_v2.ReturnEmailVerificationCode{}, + }, + }, + }, + }, + }) + logging.OnError(err).Panic("create human user") + i.TriggerUserByID(ctx, resp.GetId()) + return resp +} + +func (i *Instance) CreateUserTypeMachine(ctx context.Context, orgId string) *user_v2.CreateUserResponse { + resp, err := i.Client.UserV2.CreateUser(ctx, &user_v2.CreateUserRequest{ + OrganizationId: i.DefaultOrg.GetId(), + UserType: &user_v2.CreateUserRequest_Machine_{ + Machine: &user_v2.CreateUserRequest_Machine{ + Name: "machine", + }, + }, + }) + logging.OnError(err).Panic("create machine user") + i.TriggerUserByID(ctx, resp.GetId()) + return resp +} + +func (i *Instance) CreatePersonalAccessToken(ctx context.Context, userID string) *user_v2.AddPersonalAccessTokenResponse { + resp, err := i.Client.UserV2.AddPersonalAccessToken(ctx, &user_v2.AddPersonalAccessTokenRequest{ + UserId: userID, + ExpirationDate: timestamppb.New(time.Now().Add(30 * time.Minute)), + }) + logging.OnError(err).Panic("create pat") + return resp +} + // TriggerUserByID makes sure the user projection gets triggered after creation. func (i *Instance) TriggerUserByID(ctx context.Context, users ...string) { var wg sync.WaitGroup @@ -286,6 +384,15 @@ func SetOrgID(ctx context.Context, orgID string) context.Context { return metadata.NewOutgoingContext(ctx, md) } +func (i *Instance) CreateOrganizationWithCustomOrgID(ctx context.Context, name, orgID string) *org.AddOrganizationResponse { + resp, err := i.Client.OrgV2.AddOrganization(ctx, &org.AddOrganizationRequest{ + Name: name, + OrgId: gu.Ptr(orgID), + }) + logging.OnError(err).Fatal("create org") + return resp +} + func (i *Instance) CreateOrganizationWithUserID(ctx context.Context, name, userID string) *org.AddOrganizationResponse { resp, err := i.Client.OrgV2.AddOrganization(ctx, &org.AddOrganizationRequest{ Name: name, @@ -430,6 +537,70 @@ func (i *Instance) SetUserPassword(ctx context.Context, userID, password string, return resp.GetDetails() } +func (i *Instance) CreateProject(ctx context.Context, t *testing.T, orgID, name string, projectRoleCheck, hasProjectCheck bool) *project_v2beta.CreateProjectResponse { + if orgID == "" { + orgID = i.DefaultOrg.GetId() + } + + resp, err := i.Client.Projectv2Beta.CreateProject(ctx, &project_v2beta.CreateProjectRequest{ + OrganizationId: orgID, + Name: name, + AuthorizationRequired: projectRoleCheck, + ProjectAccessRequired: hasProjectCheck, + }) + require.NoError(t, err) + return resp +} + +func (i *Instance) DeleteProject(ctx context.Context, t *testing.T, projectID string) *project_v2beta.DeleteProjectResponse { + resp, err := i.Client.Projectv2Beta.DeleteProject(ctx, &project_v2beta.DeleteProjectRequest{ + Id: projectID, + }) + require.NoError(t, err) + return resp +} + +func (i *Instance) DeactivateProject(ctx context.Context, t *testing.T, projectID string) *project_v2beta.DeactivateProjectResponse { + resp, err := i.Client.Projectv2Beta.DeactivateProject(ctx, &project_v2beta.DeactivateProjectRequest{ + Id: projectID, + }) + require.NoError(t, err) + return resp +} + +func (i *Instance) ActivateProject(ctx context.Context, t *testing.T, projectID string) *project_v2beta.ActivateProjectResponse { + resp, err := i.Client.Projectv2Beta.ActivateProject(ctx, &project_v2beta.ActivateProjectRequest{ + Id: projectID, + }) + require.NoError(t, err) + return resp +} + +func (i *Instance) AddProjectRole(ctx context.Context, t *testing.T, projectID, roleKey, displayName, group string) *project_v2beta.AddProjectRoleResponse { + var groupP *string + if group != "" { + groupP = &group + } + + resp, err := i.Client.Projectv2Beta.AddProjectRole(ctx, &project_v2beta.AddProjectRoleRequest{ + ProjectId: projectID, + RoleKey: roleKey, + DisplayName: displayName, + Group: groupP, + }) + require.NoError(t, err) + return resp +} + +func (i *Instance) RemoveProjectRole(ctx context.Context, t *testing.T, projectID, roleKey string) *project_v2beta.RemoveProjectRoleResponse { + resp, err := i.Client.Projectv2Beta.RemoveProjectRole(ctx, &project_v2beta.RemoveProjectRoleRequest{ + ProjectId: projectID, + RoleKey: roleKey, + }) + require.NoError(t, err) + return resp +} + func (i *Instance) AddProviderToDefaultLoginPolicy(ctx context.Context, id string) { _, err := i.Client.Admin.AddIDPToLoginPolicy(ctx, &admin.AddIDPToLoginPolicyRequest{ IdpId: id, @@ -490,14 +661,6 @@ func (i *Instance) AddOrgGenericOAuthProvider(ctx context.Context, name string) }, }) logging.OnError(err).Panic("create generic OAuth idp") - /* - mustAwait(func() error { - _, err := i.Client.Mgmt.GetProviderByID(ctx, &mgmt.GetProviderByIDRequest{ - Id: resp.GetId(), - }) - return err - }) - */ return resp } @@ -707,47 +870,144 @@ func (i *Instance) CreateIntentSession(t *testing.T, ctx context.Context, userID createResp.GetDetails().GetChangeDate().AsTime(), createResp.GetDetails().GetChangeDate().AsTime() } -func (i *Instance) CreateProjectGrant(ctx context.Context, projectID, grantedOrgID string) *mgmt.AddProjectGrantResponse { - resp, err := i.Client.Mgmt.AddProjectGrant(ctx, &mgmt.AddProjectGrantRequest{ - GrantedOrgId: grantedOrgID, - ProjectId: projectID, +func (i *Instance) CreateProjectGrant(ctx context.Context, t *testing.T, projectID, grantedOrgID string, roles ...string) *project_v2beta.CreateProjectGrantResponse { + resp, err := i.Client.Projectv2Beta.CreateProjectGrant(ctx, &project_v2beta.CreateProjectGrantRequest{ + GrantedOrganizationId: grantedOrgID, + ProjectId: projectID, + RoleKeys: roles, }) - logging.OnError(err).Panic("create project grant") + require.NoError(t, err) return resp } -func (i *Instance) CreateProjectUserGrant(t *testing.T, ctx context.Context, projectID, userID string) string { +func (i *Instance) DeleteProjectGrant(ctx context.Context, t *testing.T, projectID, grantedOrgID string) *project_v2beta.DeleteProjectGrantResponse { + resp, err := i.Client.Projectv2Beta.DeleteProjectGrant(ctx, &project_v2beta.DeleteProjectGrantRequest{ + GrantedOrganizationId: grantedOrgID, + ProjectId: projectID, + }) + require.NoError(t, err) + return resp +} + +func (i *Instance) DeactivateProjectGrant(ctx context.Context, t *testing.T, projectID, grantedOrgID string) *project_v2beta.DeactivateProjectGrantResponse { + resp, err := i.Client.Projectv2Beta.DeactivateProjectGrant(ctx, &project_v2beta.DeactivateProjectGrantRequest{ + ProjectId: projectID, + GrantedOrganizationId: grantedOrgID, + }) + require.NoError(t, err) + return resp +} + +func (i *Instance) ActivateProjectGrant(ctx context.Context, t *testing.T, projectID, grantedOrgID string) *project_v2beta.ActivateProjectGrantResponse { + resp, err := i.Client.Projectv2Beta.ActivateProjectGrant(ctx, &project_v2beta.ActivateProjectGrantRequest{ + ProjectId: projectID, + GrantedOrganizationId: grantedOrgID, + }) + require.NoError(t, err) + return resp +} + +func (i *Instance) CreateProjectUserGrant(t *testing.T, ctx context.Context, projectID, userID string) *mgmt.AddUserGrantResponse { resp, err := i.Client.Mgmt.AddUserGrant(ctx, &mgmt.AddUserGrantRequest{ UserId: userID, ProjectId: projectID, }) require.NoError(t, err) - return resp.GetUserGrantId() + return resp } -func (i *Instance) CreateProjectGrantUserGrant(ctx context.Context, orgID, projectID, projectGrantID, userID string) string { +func (i *Instance) CreateProjectGrantUserGrant(ctx context.Context, orgID, projectID, projectGrantID, userID string) *mgmt.AddUserGrantResponse { resp, err := i.Client.Mgmt.AddUserGrant(SetOrgID(ctx, orgID), &mgmt.AddUserGrantRequest{ UserId: userID, ProjectId: projectID, ProjectGrantId: projectGrantID, }) logging.OnError(err).Panic("create project grant user grant") - return resp.GetUserGrantId() + return resp } -func (i *Instance) CreateOrgMembership(t *testing.T, ctx context.Context, userID string) { - _, err := i.Client.Mgmt.AddOrgMember(ctx, &mgmt.AddOrgMemberRequest{ +func (i *Instance) CreateInstanceMembership(t *testing.T, ctx context.Context, userID string) *internal_permission_v2beta.CreateAdministratorResponse { + resp, err := i.Client.InternalPermissionv2Beta.CreateAdministrator(ctx, &internal_permission_v2beta.CreateAdministratorRequest{ + Resource: &internal_permission_v2beta.ResourceType{ + Resource: &internal_permission_v2beta.ResourceType_Instance{Instance: true}, + }, + UserId: userID, + Roles: []string{domain.RoleIAMOwner}, + }) + require.NoError(t, err) + return resp +} + +func (i *Instance) DeleteInstanceMembership(t *testing.T, ctx context.Context, userID string) { + _, err := i.Client.Admin.RemoveIAMMember(ctx, &admin.RemoveIAMMemberRequest{ UserId: userID, - Roles: []string{domain.RoleOrgOwner}, }) require.NoError(t, err) } -func (i *Instance) CreateProjectMembership(t *testing.T, ctx context.Context, projectID, userID string) { - _, err := i.Client.Mgmt.AddProjectMember(ctx, &mgmt.AddProjectMemberRequest{ - ProjectId: projectID, - UserId: userID, - Roles: []string{domain.RoleProjectOwner}, +func (i *Instance) CreateOrgMembership(t *testing.T, ctx context.Context, orgID, userID string) *internal_permission_v2beta.CreateAdministratorResponse { + resp, err := i.Client.InternalPermissionv2Beta.CreateAdministrator(ctx, &internal_permission_v2beta.CreateAdministratorRequest{ + Resource: &internal_permission_v2beta.ResourceType{ + Resource: &internal_permission_v2beta.ResourceType_OrganizationId{OrganizationId: orgID}, + }, + UserId: userID, + Roles: []string{domain.RoleOrgOwner}, + }) + require.NoError(t, err) + return resp +} + +func (i *Instance) DeleteOrgMembership(t *testing.T, ctx context.Context, userID string) { + _, err := i.Client.Mgmt.RemoveOrgMember(ctx, &mgmt.RemoveOrgMemberRequest{ + UserId: userID, + }) + require.NoError(t, err) +} + +func (i *Instance) CreateProjectMembership(t *testing.T, ctx context.Context, projectID, userID string) *internal_permission_v2beta.CreateAdministratorResponse { + resp, err := i.Client.InternalPermissionv2Beta.CreateAdministrator(ctx, &internal_permission_v2beta.CreateAdministratorRequest{ + Resource: &internal_permission_v2beta.ResourceType{ + Resource: &internal_permission_v2beta.ResourceType_ProjectId{ProjectId: projectID}, + }, + UserId: userID, + Roles: []string{domain.RoleProjectOwner}, + }) + require.NoError(t, err) + return resp +} + +func (i *Instance) DeleteProjectMembership(t *testing.T, ctx context.Context, projectID, userID string) { + _, err := i.Client.InternalPermissionv2Beta.DeleteAdministrator(ctx, &internal_permission_v2beta.DeleteAdministratorRequest{ + Resource: &internal_permission_v2beta.ResourceType{Resource: &internal_permission_v2beta.ResourceType_ProjectId{ProjectId: projectID}}, + UserId: userID, + }) + require.NoError(t, err) +} + +func (i *Instance) CreateProjectGrantMembership(t *testing.T, ctx context.Context, projectID, grantID, userID string) *internal_permission_v2beta.CreateAdministratorResponse { + resp, err := i.Client.InternalPermissionv2Beta.CreateAdministrator(ctx, &internal_permission_v2beta.CreateAdministratorRequest{ + Resource: &internal_permission_v2beta.ResourceType{ + Resource: &internal_permission_v2beta.ResourceType_ProjectGrant_{ProjectGrant: &internal_permission_v2beta.ResourceType_ProjectGrant{ + ProjectId: projectID, + ProjectGrantId: grantID, + }}, + }, + UserId: userID, + Roles: []string{domain.RoleProjectGrantOwner}, + }) + require.NoError(t, err) + return resp +} + +func (i *Instance) DeleteProjectGrantMembership(t *testing.T, ctx context.Context, projectID, grantID, userID string) { + _, err := i.Client.InternalPermissionv2Beta.DeleteAdministrator(ctx, &internal_permission_v2beta.DeleteAdministratorRequest{ + Resource: &internal_permission_v2beta.ResourceType{ + Resource: &internal_permission_v2beta.ResourceType_ProjectGrant_{ProjectGrant: &internal_permission_v2beta.ResourceType_ProjectGrant{ + ProjectId: projectID, + ProjectGrantId: grantID, + }}, + }, + UserId: userID, }) require.NoError(t, err) } @@ -779,27 +1039,27 @@ func (i *Instance) CreateTarget(ctx context.Context, t *testing.T, name, endpoin RestAsync: &action.RESTAsync{}, } } - target, err := i.Client.ActionV2beta.CreateTarget(ctx, req) + target, err := i.Client.ActionV2.CreateTarget(ctx, req) require.NoError(t, err) return target } func (i *Instance) DeleteTarget(ctx context.Context, t *testing.T, id string) { - _, err := i.Client.ActionV2beta.DeleteTarget(ctx, &action.DeleteTargetRequest{ + _, err := i.Client.ActionV2.DeleteTarget(ctx, &action.DeleteTargetRequest{ Id: id, }) require.NoError(t, err) } func (i *Instance) DeleteExecution(ctx context.Context, t *testing.T, cond *action.Condition) { - _, err := i.Client.ActionV2beta.SetExecution(ctx, &action.SetExecutionRequest{ + _, err := i.Client.ActionV2.SetExecution(ctx, &action.SetExecutionRequest{ Condition: cond, }) require.NoError(t, err) } func (i *Instance) SetExecution(ctx context.Context, t *testing.T, cond *action.Condition, targets []string) *action.SetExecutionResponse { - target, err := i.Client.ActionV2beta.SetExecution(ctx, &action.SetExecutionRequest{ + target, err := i.Client.ActionV2.SetExecution(ctx, &action.SetExecutionRequest{ Condition: cond, Targets: targets, }) diff --git a/internal/integration/config/zitadel.yaml b/internal/integration/config/zitadel.yaml index bb8d86376d..fed746d823 100644 --- a/internal/integration/config/zitadel.yaml +++ b/internal/integration/config/zitadel.yaml @@ -101,3 +101,10 @@ SystemDefaults: KeyConfig: PrivateKeyLifetime: 7200h PublicKeyLifetime: 14400h + +OIDC: + DefaultLoginURLV2: "/login?authRequest=" # ZITADEL_OIDC_DEFAULTLOGINURLV2 + DefaultLogoutURLV2: "/logout?post_logout_redirect=" # ZITADEL_OIDC_DEFAULTLOGOUTURLV2 + +SAML: + DefaultLoginURLV2: "/login?authRequest=" # ZITADEL_SAML_DEFAULTLOGINURLV2 diff --git a/internal/integration/feature.go b/internal/integration/feature.go new file mode 100644 index 0000000000..07942fcdcd --- /dev/null +++ b/internal/integration/feature.go @@ -0,0 +1,30 @@ +package integration + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/pkg/grpc/feature/v2" +) + +func EnsureInstanceFeature(t *testing.T, ctx context.Context, instance *Instance, features *feature.SetInstanceFeaturesRequest, assertFeatures func(t *assert.CollectT, got *feature.GetInstanceFeaturesResponse)) { + ctx = instance.WithAuthorizationToken(ctx, UserTypeIAMOwner) + _, err := instance.Client.FeatureV2.SetInstanceFeatures(ctx, features) + require.NoError(t, err) + retryDuration, tick := WaitForAndTickWithMaxDuration(ctx, 5*time.Minute) + require.EventuallyWithT(t, + func(tt *assert.CollectT) { + got, err := instance.Client.FeatureV2.GetInstanceFeatures(ctx, &feature.GetInstanceFeaturesRequest{ + Inheritance: true, + }) + require.NoError(tt, err) + assertFeatures(tt, got) + }, + retryDuration, + tick, + "timed out waiting for ensuring instance feature") +} diff --git a/internal/integration/instance.go b/internal/integration/instance.go index 66e2cf18ec..9e262cb3d9 100644 --- a/internal/integration/instance.go +++ b/internal/integration/instance.go @@ -294,11 +294,14 @@ func (i *Instance) createWebAuthNClient() { i.WebAuthN = webauthn.NewClient(i.Config.WebAuthNName, i.Domain, http_util.BuildOrigin(i.Host(), i.Config.Secure)) } +// Deprecated: WithAuthorization is misleading, as we have Zitadel resources called authorization now. +// It is aliased to WithAuthorizationToken, which sets the Authorization header with a Bearer token. +// Use WithAuthorizationToken directly instead. func (i *Instance) WithAuthorization(ctx context.Context, u UserType) context.Context { - return i.WithInstanceAuthorization(ctx, u) + return i.WithAuthorizationToken(ctx, u) } -func (i *Instance) WithInstanceAuthorization(ctx context.Context, u UserType) context.Context { +func (i *Instance) WithAuthorizationToken(ctx context.Context, u UserType) context.Context { return WithAuthorizationToken(ctx, i.Users.Get(u).Token) } diff --git a/internal/integration/oidc.go b/internal/integration/oidc.go index 159fcb0119..3323be3f97 100644 --- a/internal/integration/oidc.go +++ b/internal/integration/oidc.go @@ -7,6 +7,7 @@ import ( "net/http" "net/url" "strings" + "testing" "time" "github.com/brianvoe/gofakeit/v6" @@ -133,13 +134,9 @@ func (i *Instance) CreateOIDCInactivateProjectClient(ctx context.Context, redire return client, err } -func (i *Instance) CreateOIDCImplicitFlowClient(ctx context.Context, redirectURI string, loginVersion *app.LoginVersion) (*management.AddOIDCAppResponse, error) { - project, err := i.Client.Mgmt.AddProject(ctx, &management.AddProjectRequest{ - Name: fmt.Sprintf("project-%d", time.Now().UnixNano()), - }) - if err != nil { - return nil, err - } +func (i *Instance) CreateOIDCImplicitFlowClient(ctx context.Context, t *testing.T, redirectURI string, loginVersion *app.LoginVersion) (*management.AddOIDCAppResponse, error) { + project := i.CreateProject(ctx, t, "", gofakeit.AppName(), false, false) + resp, err := i.Client.Mgmt.AddOIDCApp(ctx, &management.AddOIDCAppRequest{ ProjectId: project.GetId(), Name: fmt.Sprintf("app-%d", time.Now().UnixNano()), @@ -178,30 +175,11 @@ func (i *Instance) CreateOIDCImplicitFlowClient(ctx context.Context, redirectURI }) } -func (i *Instance) CreateOIDCTokenExchangeClient(ctx context.Context) (client *management.AddOIDCAppResponse, keyData []byte, err error) { - project, err := i.Client.Mgmt.AddProject(ctx, &management.AddProjectRequest{ - Name: fmt.Sprintf("project-%d", time.Now().UnixNano()), - }) - if err != nil { - return nil, nil, err - } +func (i *Instance) CreateOIDCTokenExchangeClient(ctx context.Context, t *testing.T) (client *management.AddOIDCAppResponse, keyData []byte, err error) { + project := i.CreateProject(ctx, t, "", gofakeit.AppName(), false, false) return i.CreateOIDCWebClientJWT(ctx, "", "", project.GetId(), app.OIDCGrantType_OIDC_GRANT_TYPE_TOKEN_EXCHANGE, app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE, app.OIDCGrantType_OIDC_GRANT_TYPE_REFRESH_TOKEN) } -func (i *Instance) CreateProject(ctx context.Context) (*management.AddProjectResponse, error) { - return i.Client.Mgmt.AddProject(ctx, &management.AddProjectRequest{ - Name: fmt.Sprintf("project-%d", time.Now().UnixNano()), - }) -} - -func (i *Instance) CreateProjectWithPermissionCheck(ctx context.Context, projectRoleCheck, hasProjectCheck bool) (*management.AddProjectResponse, error) { - return i.Client.Mgmt.AddProject(ctx, &management.AddProjectRequest{ - Name: fmt.Sprintf("project-%d", time.Now().UnixNano()), - HasProjectCheck: hasProjectCheck, - ProjectRoleCheck: projectRoleCheck, - }) -} - func (i *Instance) CreateAPIClientJWT(ctx context.Context, projectID string) (*management.AddAPIAppResponse, error) { return i.Client.Mgmt.AddAPIApp(ctx, &management.AddAPIAppRequest{ ProjectId: projectID, diff --git a/internal/migration/count_trigger.sql b/internal/migration/count_trigger.sql new file mode 100644 index 0000000000..4b521094ab --- /dev/null +++ b/internal/migration/count_trigger.sql @@ -0,0 +1,43 @@ +{{ define "count_trigger" -}} +CREATE OR REPLACE TRIGGER count_{{ .Resource }} + AFTER INSERT OR DELETE + ON {{ .Table }} + FOR EACH ROW + EXECUTE FUNCTION projections.count_resource( + '{{ .ParentType }}', + '{{ .InstanceIDColumn }}', + '{{ .ParentIDColumn }}', + '{{ .Resource }}' + ); + +CREATE OR REPLACE TRIGGER truncate_{{ .Resource }}_counts + AFTER TRUNCATE + ON {{ .Table }} + FOR EACH STATEMENT + EXECUTE FUNCTION projections.delete_table_counts(); + +-- Prevent inserts and deletes while we populate the counts. +LOCK TABLE {{ .Table }} IN SHARE MODE; + +-- Populate the resource counts for the existing data in the table. +INSERT INTO projections.resource_counts( + instance_id, + table_name, + parent_type, + parent_id, + resource_name, + amount +) +SELECT + {{ .InstanceIDColumn }}, + '{{ .Table }}', + '{{ .ParentType }}', + {{ .ParentIDColumn }}, + '{{ .Resource }}', + COUNT(*) AS amount +FROM {{ .Table }} +GROUP BY ({{ .InstanceIDColumn }}, {{ .ParentIDColumn }}) +ON CONFLICT (instance_id, table_name, parent_type, parent_id) DO +UPDATE SET updated_at = now(), amount = EXCLUDED.amount; + +{{- end -}} diff --git a/internal/migration/delete_parent_counts_trigger.sql b/internal/migration/delete_parent_counts_trigger.sql new file mode 100644 index 0000000000..a2e9df6626 --- /dev/null +++ b/internal/migration/delete_parent_counts_trigger.sql @@ -0,0 +1,13 @@ +{{ define "delete_parent_counts_trigger" -}} + +CREATE OR REPLACE TRIGGER delete_parent_counts_trigger + AFTER DELETE + ON {{ .Table }} + FOR EACH ROW + EXECUTE FUNCTION projections.delete_parent_counts( + '{{ .ParentType }}', + '{{ .InstanceIDColumn }}', + '{{ .ParentIDColumn }}' + ); + +{{- end -}} diff --git a/internal/migration/migration.go b/internal/migration/migration.go index a2224340a7..3aeb2f0612 100644 --- a/internal/migration/migration.go +++ b/internal/migration/migration.go @@ -36,7 +36,10 @@ type errCheckerMigration interface { type RepeatableMigration interface { Migration - Check(lastRun map[string]interface{}) bool + + // Check if the migration should be executed again. + // True will repeat the migration, false will not. + Check(lastRun map[string]any) bool } func Migrate(ctx context.Context, es *eventstore.Eventstore, migration Migration) (err error) { diff --git a/internal/migration/trigger.go b/internal/migration/trigger.go new file mode 100644 index 0000000000..bd06afd5c5 --- /dev/null +++ b/internal/migration/trigger.go @@ -0,0 +1,127 @@ +package migration + +import ( + "context" + "embed" + "fmt" + "strings" + "text/template" + + "github.com/mitchellh/mapstructure" + + "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore" +) + +const ( + countTriggerTmpl = "count_trigger" + deleteParentCountsTmpl = "delete_parent_counts_trigger" +) + +var ( + //go:embed *.sql + templateFS embed.FS + templates = template.Must(template.ParseFS(templateFS, "*.sql")) +) + +// CountTrigger registers the existing projections.count_trigger function. +// The trigger than takes care of keeping count of existing +// rows in the source table. +// It also pre-populates the projections.resource_counts table with +// the counts for the given table. +// +// During the population of the resource_counts table, +// the source table is share-locked to prevent concurrent modifications. +// Projection handlers will be halted until the lock is released. +// SELECT statements are not blocked by the lock. +// +// This migration repeats when any of the arguments are changed, +// such as renaming of a projection table. +func CountTrigger( + db *database.DB, + table string, + parentType domain.CountParentType, + instanceIDColumn string, + parentIDColumn string, + resource string, +) RepeatableMigration { + return &triggerMigration{ + triggerConfig: triggerConfig{ + Table: table, + ParentType: parentType.String(), + InstanceIDColumn: instanceIDColumn, + ParentIDColumn: parentIDColumn, + Resource: resource, + }, + db: db, + templateName: countTriggerTmpl, + } +} + +// DeleteParentCountsTrigger +// +// This migration repeats when any of the arguments are changed, +// such as renaming of a projection table. +func DeleteParentCountsTrigger( + db *database.DB, + table string, + parentType domain.CountParentType, + instanceIDColumn string, + parentIDColumn string, + resource string, +) RepeatableMigration { + return &triggerMigration{ + triggerConfig: triggerConfig{ + Table: table, + ParentType: parentType.String(), + InstanceIDColumn: instanceIDColumn, + ParentIDColumn: parentIDColumn, + Resource: resource, + }, + db: db, + templateName: deleteParentCountsTmpl, + } +} + +type triggerMigration struct { + triggerConfig + db *database.DB + templateName string +} + +// String implements [Migration] and [fmt.Stringer]. +func (m *triggerMigration) String() string { + return fmt.Sprintf("repeatable_%s_%s", m.Resource, m.templateName) +} + +// Execute implements [Migration] +func (m *triggerMigration) Execute(ctx context.Context, _ eventstore.Event) error { + var query strings.Builder + err := templates.ExecuteTemplate(&query, m.templateName, m.triggerConfig) + if err != nil { + return fmt.Errorf("%s: execute trigger template: %w", m, err) + } + _, err = m.db.ExecContext(ctx, query.String()) + if err != nil { + return fmt.Errorf("%s: exec trigger query: %w", m, err) + } + return nil +} + +type triggerConfig struct { + Table string `json:"table,omitempty" mapstructure:"table"` + ParentType string `json:"parent_type,omitempty" mapstructure:"parent_type"` + InstanceIDColumn string `json:"instance_id_column,omitempty" mapstructure:"instance_id_column"` + ParentIDColumn string `json:"parent_id_column,omitempty" mapstructure:"parent_id_column"` + Resource string `json:"resource,omitempty" mapstructure:"resource"` +} + +// Check implements [RepeatableMigration]. +func (c *triggerConfig) Check(lastRun map[string]any) bool { + var dst triggerConfig + if err := mapstructure.Decode(lastRun, &dst); err != nil { + panic(err) + } + return dst != *c +} diff --git a/internal/migration/trigger_test.go b/internal/migration/trigger_test.go new file mode 100644 index 0000000000..5799526428 --- /dev/null +++ b/internal/migration/trigger_test.go @@ -0,0 +1,253 @@ +package migration + +import ( + "context" + "regexp" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/internal/database" +) + +const ( + expCountTriggerQuery = `CREATE OR REPLACE TRIGGER count_resource + AFTER INSERT OR DELETE + ON table + FOR EACH ROW + EXECUTE FUNCTION projections.count_resource( + 'instance', + 'instance_id', + 'parent_id', + 'resource' + ); + +CREATE OR REPLACE TRIGGER truncate_resource_counts + AFTER TRUNCATE + ON table + FOR EACH STATEMENT + EXECUTE FUNCTION projections.delete_table_counts(); + +-- Prevent inserts and deletes while we populate the counts. +LOCK TABLE table IN SHARE MODE; + +-- Populate the resource counts for the existing data in the table. +INSERT INTO projections.resource_counts( + instance_id, + table_name, + parent_type, + parent_id, + resource_name, + amount +) +SELECT + instance_id, + 'table', + 'instance', + parent_id, + 'resource', + COUNT(*) AS amount +FROM table +GROUP BY (instance_id, parent_id) +ON CONFLICT (instance_id, table_name, parent_type, parent_id) DO +UPDATE SET updated_at = now(), amount = EXCLUDED.amount;` + + expDeleteParentCountsQuery = `CREATE OR REPLACE TRIGGER delete_parent_counts_trigger + AFTER DELETE + ON table + FOR EACH ROW + EXECUTE FUNCTION projections.delete_parent_counts( + 'instance', + 'instance_id', + 'parent_id' + );` +) + +func Test_triggerMigration_Execute(t *testing.T) { + type fields struct { + triggerConfig triggerConfig + templateName string + } + tests := []struct { + name string + fields fields + expects func(sqlmock.Sqlmock) + wantErr bool + }{ + { + name: "template error", + fields: fields{ + triggerConfig: triggerConfig{ + Table: "table", + ParentType: "instance", + InstanceIDColumn: "instance_id", + ParentIDColumn: "parent_id", + Resource: "resource", + }, + templateName: "foo", + }, + expects: func(_ sqlmock.Sqlmock) {}, + wantErr: true, + }, + { + name: "db error", + fields: fields{ + triggerConfig: triggerConfig{ + Table: "table", + ParentType: "instance", + InstanceIDColumn: "instance_id", + ParentIDColumn: "parent_id", + Resource: "resource", + }, + templateName: countTriggerTmpl, + }, + expects: func(mock sqlmock.Sqlmock) { + mock.ExpectExec(regexp.QuoteMeta(expCountTriggerQuery)). + WillReturnError(assert.AnError) + }, + wantErr: true, + }, + { + name: "count trigger", + fields: fields{ + triggerConfig: triggerConfig{ + Table: "table", + ParentType: "instance", + InstanceIDColumn: "instance_id", + ParentIDColumn: "parent_id", + Resource: "resource", + }, + templateName: countTriggerTmpl, + }, + expects: func(mock sqlmock.Sqlmock) { + mock.ExpectExec(regexp.QuoteMeta(expCountTriggerQuery)). + WithoutArgs(). + WillReturnResult( + sqlmock.NewResult(1, 1), + ) + }, + }, + { + name: "count trigger", + fields: fields{ + triggerConfig: triggerConfig{ + Table: "table", + ParentType: "instance", + InstanceIDColumn: "instance_id", + ParentIDColumn: "parent_id", + Resource: "resource", + }, + templateName: deleteParentCountsTmpl, + }, + expects: func(mock sqlmock.Sqlmock) { + mock.ExpectExec(regexp.QuoteMeta(expDeleteParentCountsQuery)). + WithoutArgs(). + WillReturnResult( + sqlmock.NewResult(1, 1), + ) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + db, mock, err := sqlmock.New() + require.NoError(t, err) + defer func() { + err := mock.ExpectationsWereMet() + require.NoError(t, err) + }() + defer db.Close() + tt.expects(mock) + mock.ExpectClose() + + m := &triggerMigration{ + db: &database.DB{ + DB: db, + }, + triggerConfig: tt.fields.triggerConfig, + templateName: tt.fields.templateName, + } + err = m.Execute(context.Background(), nil) + if tt.wantErr { + assert.Error(t, err) + return + } + require.NoError(t, err) + }) + } +} + +func Test_triggerConfig_Check(t *testing.T) { + type fields struct { + Table string + ParentType string + InstanceIDColumn string + ParentIDColumn string + Resource string + } + type args struct { + lastRun map[string]any + } + tests := []struct { + name string + fields fields + args args + want bool + }{ + { + name: "should", + fields: fields{ + Table: "users2", + ParentType: "instance", + InstanceIDColumn: "instance_id", + ParentIDColumn: "parent_id", + Resource: "user", + }, + args: args{ + lastRun: map[string]any{ + "table": "users1", + "parent_type": "instance", + "instance_id_column": "instance_id", + "parent_id_column": "parent_id", + "resource": "user", + }, + }, + want: true, + }, + { + name: "should not", + fields: fields{ + Table: "users1", + ParentType: "instance", + InstanceIDColumn: "instance_id", + ParentIDColumn: "parent_id", + Resource: "user", + }, + args: args{ + lastRun: map[string]any{ + "table": "users1", + "parent_type": "instance", + "instance_id_column": "instance_id", + "parent_id_column": "parent_id", + "resource": "user", + }, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &triggerConfig{ + Table: tt.fields.Table, + ParentType: tt.fields.ParentType, + InstanceIDColumn: tt.fields.InstanceIDColumn, + ParentIDColumn: tt.fields.ParentIDColumn, + Resource: tt.fields.Resource, + } + got := c.Check(tt.args.lastRun) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/internal/notification/handlers/back_channel_logout.go b/internal/notification/handlers/back_channel_logout.go index f1a99146ca..983915ac28 100644 --- a/internal/notification/handlers/back_channel_logout.go +++ b/internal/notification/handlers/back_channel_logout.go @@ -7,10 +7,8 @@ import ( "sync" "time" - "github.com/zitadel/logging" "github.com/zitadel/oidc/v3/pkg/crypto" "github.com/zitadel/oidc/v3/pkg/oidc" - "github.com/zitadel/oidc/v3/pkg/op" "github.com/zitadel/zitadel/internal/api/authz" http_utils "github.com/zitadel/zitadel/internal/api/http" @@ -149,7 +147,7 @@ func (u *backChannelLogoutNotifier) terminateSession(ctx context.Context, id str return err } - getSigner := zoidc.GetSignerOnce(u.queries.GetActiveSigningWebKey, u.signingKey) + getSigner := zoidc.GetSignerOnce(u.queries.GetActiveSigningWebKey) var wg sync.WaitGroup wg.Add(len(sessions.sessions)) @@ -172,20 +170,6 @@ func (u *backChannelLogoutNotifier) terminateSession(ctx context.Context, id str return errors.Join(errs...) } -func (u *backChannelLogoutNotifier) signingKey(ctx context.Context) (op.SigningKey, error) { - keys, err := u.queries.ActivePrivateSigningKey(ctx, time.Now()) - if err != nil { - return nil, err - } - if len(keys.Keys) == 0 { - logging.WithFields("instanceID", authz.GetInstance(ctx).InstanceID()). - Info("There's no active signing key and automatic rotation is not supported for back channel logout." + - "Please enable the webkey management feature on your instance") - return nil, zerrors.ThrowPreconditionFailed(nil, "HANDL-DF3nf", "no active signing key") - } - return zoidc.PrivateKeyToSigningKey(zoidc.SelectSigningKey(keys.Keys), u.keyEncryptionAlg) -} - func (u *backChannelLogoutNotifier) sendLogoutToken(ctx context.Context, oidcSession *backChannelLogoutOIDCSessions, e eventstore.Event, getSigner zoidc.SignerFunc) error { token, err := u.logoutToken(ctx, oidcSession, getSigner) if err != nil { diff --git a/internal/notification/handlers/mock/commands.mock.go b/internal/notification/handlers/mock/commands.mock.go index 7d41c30f30..ec327de8e8 100644 --- a/internal/notification/handlers/mock/commands.mock.go +++ b/internal/notification/handlers/mock/commands.mock.go @@ -23,6 +23,7 @@ import ( type MockCommands struct { ctrl *gomock.Controller recorder *MockCommandsMockRecorder + isgomock struct{} } // MockCommandsMockRecorder is the mock recorder for MockCommands. @@ -43,197 +44,197 @@ func (m *MockCommands) EXPECT() *MockCommandsMockRecorder { } // HumanEmailVerificationCodeSent mocks base method. -func (m *MockCommands) HumanEmailVerificationCodeSent(arg0 context.Context, arg1, arg2 string) error { +func (m *MockCommands) HumanEmailVerificationCodeSent(ctx context.Context, orgID, userID string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "HumanEmailVerificationCodeSent", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "HumanEmailVerificationCodeSent", ctx, orgID, userID) ret0, _ := ret[0].(error) return ret0 } // HumanEmailVerificationCodeSent indicates an expected call of HumanEmailVerificationCodeSent. -func (mr *MockCommandsMockRecorder) HumanEmailVerificationCodeSent(arg0, arg1, arg2 any) *gomock.Call { +func (mr *MockCommandsMockRecorder) HumanEmailVerificationCodeSent(ctx, orgID, userID any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HumanEmailVerificationCodeSent", reflect.TypeOf((*MockCommands)(nil).HumanEmailVerificationCodeSent), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HumanEmailVerificationCodeSent", reflect.TypeOf((*MockCommands)(nil).HumanEmailVerificationCodeSent), ctx, orgID, userID) } // HumanInitCodeSent mocks base method. -func (m *MockCommands) HumanInitCodeSent(arg0 context.Context, arg1, arg2 string) error { +func (m *MockCommands) HumanInitCodeSent(ctx context.Context, orgID, userID string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "HumanInitCodeSent", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "HumanInitCodeSent", ctx, orgID, userID) ret0, _ := ret[0].(error) return ret0 } // HumanInitCodeSent indicates an expected call of HumanInitCodeSent. -func (mr *MockCommandsMockRecorder) HumanInitCodeSent(arg0, arg1, arg2 any) *gomock.Call { +func (mr *MockCommandsMockRecorder) HumanInitCodeSent(ctx, orgID, userID any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HumanInitCodeSent", reflect.TypeOf((*MockCommands)(nil).HumanInitCodeSent), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HumanInitCodeSent", reflect.TypeOf((*MockCommands)(nil).HumanInitCodeSent), ctx, orgID, userID) } // HumanOTPEmailCodeSent mocks base method. -func (m *MockCommands) HumanOTPEmailCodeSent(arg0 context.Context, arg1, arg2 string) error { +func (m *MockCommands) HumanOTPEmailCodeSent(ctx context.Context, userID, resourceOwner string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "HumanOTPEmailCodeSent", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "HumanOTPEmailCodeSent", ctx, userID, resourceOwner) ret0, _ := ret[0].(error) return ret0 } // HumanOTPEmailCodeSent indicates an expected call of HumanOTPEmailCodeSent. -func (mr *MockCommandsMockRecorder) HumanOTPEmailCodeSent(arg0, arg1, arg2 any) *gomock.Call { +func (mr *MockCommandsMockRecorder) HumanOTPEmailCodeSent(ctx, userID, resourceOwner any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HumanOTPEmailCodeSent", reflect.TypeOf((*MockCommands)(nil).HumanOTPEmailCodeSent), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HumanOTPEmailCodeSent", reflect.TypeOf((*MockCommands)(nil).HumanOTPEmailCodeSent), ctx, userID, resourceOwner) } // HumanOTPSMSCodeSent mocks base method. -func (m *MockCommands) HumanOTPSMSCodeSent(arg0 context.Context, arg1, arg2 string, arg3 *senders.CodeGeneratorInfo) error { +func (m *MockCommands) HumanOTPSMSCodeSent(ctx context.Context, userID, resourceOwner string, generatorInfo *senders.CodeGeneratorInfo) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "HumanOTPSMSCodeSent", arg0, arg1, arg2, arg3) + ret := m.ctrl.Call(m, "HumanOTPSMSCodeSent", ctx, userID, resourceOwner, generatorInfo) ret0, _ := ret[0].(error) return ret0 } // HumanOTPSMSCodeSent indicates an expected call of HumanOTPSMSCodeSent. -func (mr *MockCommandsMockRecorder) HumanOTPSMSCodeSent(arg0, arg1, arg2, arg3 any) *gomock.Call { +func (mr *MockCommandsMockRecorder) HumanOTPSMSCodeSent(ctx, userID, resourceOwner, generatorInfo any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HumanOTPSMSCodeSent", reflect.TypeOf((*MockCommands)(nil).HumanOTPSMSCodeSent), arg0, arg1, arg2, arg3) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HumanOTPSMSCodeSent", reflect.TypeOf((*MockCommands)(nil).HumanOTPSMSCodeSent), ctx, userID, resourceOwner, generatorInfo) } // HumanPasswordlessInitCodeSent mocks base method. -func (m *MockCommands) HumanPasswordlessInitCodeSent(arg0 context.Context, arg1, arg2, arg3 string) error { +func (m *MockCommands) HumanPasswordlessInitCodeSent(ctx context.Context, userID, resourceOwner, codeID string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "HumanPasswordlessInitCodeSent", arg0, arg1, arg2, arg3) + ret := m.ctrl.Call(m, "HumanPasswordlessInitCodeSent", ctx, userID, resourceOwner, codeID) ret0, _ := ret[0].(error) return ret0 } // HumanPasswordlessInitCodeSent indicates an expected call of HumanPasswordlessInitCodeSent. -func (mr *MockCommandsMockRecorder) HumanPasswordlessInitCodeSent(arg0, arg1, arg2, arg3 any) *gomock.Call { +func (mr *MockCommandsMockRecorder) HumanPasswordlessInitCodeSent(ctx, userID, resourceOwner, codeID any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HumanPasswordlessInitCodeSent", reflect.TypeOf((*MockCommands)(nil).HumanPasswordlessInitCodeSent), arg0, arg1, arg2, arg3) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HumanPasswordlessInitCodeSent", reflect.TypeOf((*MockCommands)(nil).HumanPasswordlessInitCodeSent), ctx, userID, resourceOwner, codeID) } // HumanPhoneVerificationCodeSent mocks base method. -func (m *MockCommands) HumanPhoneVerificationCodeSent(arg0 context.Context, arg1, arg2 string, arg3 *senders.CodeGeneratorInfo) error { +func (m *MockCommands) HumanPhoneVerificationCodeSent(ctx context.Context, orgID, userID string, generatorInfo *senders.CodeGeneratorInfo) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "HumanPhoneVerificationCodeSent", arg0, arg1, arg2, arg3) + ret := m.ctrl.Call(m, "HumanPhoneVerificationCodeSent", ctx, orgID, userID, generatorInfo) ret0, _ := ret[0].(error) return ret0 } // HumanPhoneVerificationCodeSent indicates an expected call of HumanPhoneVerificationCodeSent. -func (mr *MockCommandsMockRecorder) HumanPhoneVerificationCodeSent(arg0, arg1, arg2, arg3 any) *gomock.Call { +func (mr *MockCommandsMockRecorder) HumanPhoneVerificationCodeSent(ctx, orgID, userID, generatorInfo any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HumanPhoneVerificationCodeSent", reflect.TypeOf((*MockCommands)(nil).HumanPhoneVerificationCodeSent), arg0, arg1, arg2, arg3) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HumanPhoneVerificationCodeSent", reflect.TypeOf((*MockCommands)(nil).HumanPhoneVerificationCodeSent), ctx, orgID, userID, generatorInfo) } // InviteCodeSent mocks base method. -func (m *MockCommands) InviteCodeSent(arg0 context.Context, arg1, arg2 string) error { +func (m *MockCommands) InviteCodeSent(ctx context.Context, orgID, userID string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "InviteCodeSent", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "InviteCodeSent", ctx, orgID, userID) ret0, _ := ret[0].(error) return ret0 } // InviteCodeSent indicates an expected call of InviteCodeSent. -func (mr *MockCommandsMockRecorder) InviteCodeSent(arg0, arg1, arg2 any) *gomock.Call { +func (mr *MockCommandsMockRecorder) InviteCodeSent(ctx, orgID, userID any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InviteCodeSent", reflect.TypeOf((*MockCommands)(nil).InviteCodeSent), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InviteCodeSent", reflect.TypeOf((*MockCommands)(nil).InviteCodeSent), ctx, orgID, userID) } // MilestonePushed mocks base method. -func (m *MockCommands) MilestonePushed(arg0 context.Context, arg1 string, arg2 milestone.Type, arg3 []string) error { +func (m *MockCommands) MilestonePushed(ctx context.Context, instanceID string, msType milestone.Type, endpoints []string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "MilestonePushed", arg0, arg1, arg2, arg3) + ret := m.ctrl.Call(m, "MilestonePushed", ctx, instanceID, msType, endpoints) ret0, _ := ret[0].(error) return ret0 } // MilestonePushed indicates an expected call of MilestonePushed. -func (mr *MockCommandsMockRecorder) MilestonePushed(arg0, arg1, arg2, arg3 any) *gomock.Call { +func (mr *MockCommandsMockRecorder) MilestonePushed(ctx, instanceID, msType, endpoints any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MilestonePushed", reflect.TypeOf((*MockCommands)(nil).MilestonePushed), arg0, arg1, arg2, arg3) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MilestonePushed", reflect.TypeOf((*MockCommands)(nil).MilestonePushed), ctx, instanceID, msType, endpoints) } // OTPEmailSent mocks base method. -func (m *MockCommands) OTPEmailSent(arg0 context.Context, arg1, arg2 string) error { +func (m *MockCommands) OTPEmailSent(ctx context.Context, sessionID, resourceOwner string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "OTPEmailSent", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "OTPEmailSent", ctx, sessionID, resourceOwner) ret0, _ := ret[0].(error) return ret0 } // OTPEmailSent indicates an expected call of OTPEmailSent. -func (mr *MockCommandsMockRecorder) OTPEmailSent(arg0, arg1, arg2 any) *gomock.Call { +func (mr *MockCommandsMockRecorder) OTPEmailSent(ctx, sessionID, resourceOwner any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OTPEmailSent", reflect.TypeOf((*MockCommands)(nil).OTPEmailSent), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OTPEmailSent", reflect.TypeOf((*MockCommands)(nil).OTPEmailSent), ctx, sessionID, resourceOwner) } // OTPSMSSent mocks base method. -func (m *MockCommands) OTPSMSSent(arg0 context.Context, arg1, arg2 string, arg3 *senders.CodeGeneratorInfo) error { +func (m *MockCommands) OTPSMSSent(ctx context.Context, sessionID, resourceOwner string, generatorInfo *senders.CodeGeneratorInfo) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "OTPSMSSent", arg0, arg1, arg2, arg3) + ret := m.ctrl.Call(m, "OTPSMSSent", ctx, sessionID, resourceOwner, generatorInfo) ret0, _ := ret[0].(error) return ret0 } // OTPSMSSent indicates an expected call of OTPSMSSent. -func (mr *MockCommandsMockRecorder) OTPSMSSent(arg0, arg1, arg2, arg3 any) *gomock.Call { +func (mr *MockCommandsMockRecorder) OTPSMSSent(ctx, sessionID, resourceOwner, generatorInfo any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OTPSMSSent", reflect.TypeOf((*MockCommands)(nil).OTPSMSSent), arg0, arg1, arg2, arg3) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OTPSMSSent", reflect.TypeOf((*MockCommands)(nil).OTPSMSSent), ctx, sessionID, resourceOwner, generatorInfo) } // PasswordChangeSent mocks base method. -func (m *MockCommands) PasswordChangeSent(arg0 context.Context, arg1, arg2 string) error { +func (m *MockCommands) PasswordChangeSent(ctx context.Context, orgID, userID string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "PasswordChangeSent", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "PasswordChangeSent", ctx, orgID, userID) ret0, _ := ret[0].(error) return ret0 } // PasswordChangeSent indicates an expected call of PasswordChangeSent. -func (mr *MockCommandsMockRecorder) PasswordChangeSent(arg0, arg1, arg2 any) *gomock.Call { +func (mr *MockCommandsMockRecorder) PasswordChangeSent(ctx, orgID, userID any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PasswordChangeSent", reflect.TypeOf((*MockCommands)(nil).PasswordChangeSent), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PasswordChangeSent", reflect.TypeOf((*MockCommands)(nil).PasswordChangeSent), ctx, orgID, userID) } // PasswordCodeSent mocks base method. -func (m *MockCommands) PasswordCodeSent(arg0 context.Context, arg1, arg2 string, arg3 *senders.CodeGeneratorInfo) error { +func (m *MockCommands) PasswordCodeSent(ctx context.Context, orgID, userID string, generatorInfo *senders.CodeGeneratorInfo) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "PasswordCodeSent", arg0, arg1, arg2, arg3) + ret := m.ctrl.Call(m, "PasswordCodeSent", ctx, orgID, userID, generatorInfo) ret0, _ := ret[0].(error) return ret0 } // PasswordCodeSent indicates an expected call of PasswordCodeSent. -func (mr *MockCommandsMockRecorder) PasswordCodeSent(arg0, arg1, arg2, arg3 any) *gomock.Call { +func (mr *MockCommandsMockRecorder) PasswordCodeSent(ctx, orgID, userID, generatorInfo any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PasswordCodeSent", reflect.TypeOf((*MockCommands)(nil).PasswordCodeSent), arg0, arg1, arg2, arg3) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PasswordCodeSent", reflect.TypeOf((*MockCommands)(nil).PasswordCodeSent), ctx, orgID, userID, generatorInfo) } // UsageNotificationSent mocks base method. -func (m *MockCommands) UsageNotificationSent(arg0 context.Context, arg1 *quota.NotificationDueEvent) error { +func (m *MockCommands) UsageNotificationSent(ctx context.Context, dueEvent *quota.NotificationDueEvent) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UsageNotificationSent", arg0, arg1) + ret := m.ctrl.Call(m, "UsageNotificationSent", ctx, dueEvent) ret0, _ := ret[0].(error) return ret0 } // UsageNotificationSent indicates an expected call of UsageNotificationSent. -func (mr *MockCommandsMockRecorder) UsageNotificationSent(arg0, arg1 any) *gomock.Call { +func (mr *MockCommandsMockRecorder) UsageNotificationSent(ctx, dueEvent any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UsageNotificationSent", reflect.TypeOf((*MockCommands)(nil).UsageNotificationSent), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UsageNotificationSent", reflect.TypeOf((*MockCommands)(nil).UsageNotificationSent), ctx, dueEvent) } // UserDomainClaimedSent mocks base method. -func (m *MockCommands) UserDomainClaimedSent(arg0 context.Context, arg1, arg2 string) error { +func (m *MockCommands) UserDomainClaimedSent(ctx context.Context, orgID, userID string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UserDomainClaimedSent", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "UserDomainClaimedSent", ctx, orgID, userID) ret0, _ := ret[0].(error) return ret0 } // UserDomainClaimedSent indicates an expected call of UserDomainClaimedSent. -func (mr *MockCommandsMockRecorder) UserDomainClaimedSent(arg0, arg1, arg2 any) *gomock.Call { +func (mr *MockCommandsMockRecorder) UserDomainClaimedSent(ctx, orgID, userID any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UserDomainClaimedSent", reflect.TypeOf((*MockCommands)(nil).UserDomainClaimedSent), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UserDomainClaimedSent", reflect.TypeOf((*MockCommands)(nil).UserDomainClaimedSent), ctx, orgID, userID) } diff --git a/internal/notification/handlers/mock/queries.mock.go b/internal/notification/handlers/mock/queries.mock.go index 670d3f3896..2cf53d1b2a 100644 --- a/internal/notification/handlers/mock/queries.mock.go +++ b/internal/notification/handlers/mock/queries.mock.go @@ -12,7 +12,6 @@ package mock import ( context "context" reflect "reflect" - time "time" jose "github.com/go-jose/go-jose/v4" authz "github.com/zitadel/zitadel/internal/api/authz" @@ -26,6 +25,7 @@ import ( type MockQueries struct { ctrl *gomock.Controller recorder *MockQueriesMockRecorder + isgomock struct{} } // MockQueriesMockRecorder is the mock recorder for MockQueries. @@ -60,240 +60,225 @@ func (mr *MockQueriesMockRecorder) ActiveInstances() *gomock.Call { } // ActiveLabelPolicyByOrg mocks base method. -func (m *MockQueries) ActiveLabelPolicyByOrg(arg0 context.Context, arg1 string, arg2 bool) (*query.LabelPolicy, error) { +func (m *MockQueries) ActiveLabelPolicyByOrg(ctx context.Context, orgID string, withOwnerRemoved bool) (*query.LabelPolicy, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ActiveLabelPolicyByOrg", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "ActiveLabelPolicyByOrg", ctx, orgID, withOwnerRemoved) ret0, _ := ret[0].(*query.LabelPolicy) ret1, _ := ret[1].(error) return ret0, ret1 } // ActiveLabelPolicyByOrg indicates an expected call of ActiveLabelPolicyByOrg. -func (mr *MockQueriesMockRecorder) ActiveLabelPolicyByOrg(arg0, arg1, arg2 any) *gomock.Call { +func (mr *MockQueriesMockRecorder) ActiveLabelPolicyByOrg(ctx, orgID, withOwnerRemoved any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ActiveLabelPolicyByOrg", reflect.TypeOf((*MockQueries)(nil).ActiveLabelPolicyByOrg), arg0, arg1, arg2) -} - -// ActivePrivateSigningKey mocks base method. -func (m *MockQueries) ActivePrivateSigningKey(arg0 context.Context, arg1 time.Time) (*query.PrivateKeys, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ActivePrivateSigningKey", arg0, arg1) - ret0, _ := ret[0].(*query.PrivateKeys) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ActivePrivateSigningKey indicates an expected call of ActivePrivateSigningKey. -func (mr *MockQueriesMockRecorder) ActivePrivateSigningKey(arg0, arg1 any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ActivePrivateSigningKey", reflect.TypeOf((*MockQueries)(nil).ActivePrivateSigningKey), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ActiveLabelPolicyByOrg", reflect.TypeOf((*MockQueries)(nil).ActiveLabelPolicyByOrg), ctx, orgID, withOwnerRemoved) } // CustomTextListByTemplate mocks base method. -func (m *MockQueries) CustomTextListByTemplate(arg0 context.Context, arg1, arg2 string, arg3 bool) (*query.CustomTexts, error) { +func (m *MockQueries) CustomTextListByTemplate(ctx context.Context, aggregateID, template string, withOwnerRemoved bool) (*query.CustomTexts, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CustomTextListByTemplate", arg0, arg1, arg2, arg3) + ret := m.ctrl.Call(m, "CustomTextListByTemplate", ctx, aggregateID, template, withOwnerRemoved) ret0, _ := ret[0].(*query.CustomTexts) ret1, _ := ret[1].(error) return ret0, ret1 } // CustomTextListByTemplate indicates an expected call of CustomTextListByTemplate. -func (mr *MockQueriesMockRecorder) CustomTextListByTemplate(arg0, arg1, arg2, arg3 any) *gomock.Call { +func (mr *MockQueriesMockRecorder) CustomTextListByTemplate(ctx, aggregateID, template, withOwnerRemoved any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CustomTextListByTemplate", reflect.TypeOf((*MockQueries)(nil).CustomTextListByTemplate), arg0, arg1, arg2, arg3) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CustomTextListByTemplate", reflect.TypeOf((*MockQueries)(nil).CustomTextListByTemplate), ctx, aggregateID, template, withOwnerRemoved) } // GetActiveSigningWebKey mocks base method. -func (m *MockQueries) GetActiveSigningWebKey(arg0 context.Context) (*jose.JSONWebKey, error) { +func (m *MockQueries) GetActiveSigningWebKey(ctx context.Context) (*jose.JSONWebKey, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetActiveSigningWebKey", arg0) + ret := m.ctrl.Call(m, "GetActiveSigningWebKey", ctx) ret0, _ := ret[0].(*jose.JSONWebKey) ret1, _ := ret[1].(error) return ret0, ret1 } // GetActiveSigningWebKey indicates an expected call of GetActiveSigningWebKey. -func (mr *MockQueriesMockRecorder) GetActiveSigningWebKey(arg0 any) *gomock.Call { +func (mr *MockQueriesMockRecorder) GetActiveSigningWebKey(ctx any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetActiveSigningWebKey", reflect.TypeOf((*MockQueries)(nil).GetActiveSigningWebKey), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetActiveSigningWebKey", reflect.TypeOf((*MockQueries)(nil).GetActiveSigningWebKey), ctx) } // GetDefaultLanguage mocks base method. -func (m *MockQueries) GetDefaultLanguage(arg0 context.Context) language.Tag { +func (m *MockQueries) GetDefaultLanguage(ctx context.Context) language.Tag { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetDefaultLanguage", arg0) + ret := m.ctrl.Call(m, "GetDefaultLanguage", ctx) ret0, _ := ret[0].(language.Tag) return ret0 } // GetDefaultLanguage indicates an expected call of GetDefaultLanguage. -func (mr *MockQueriesMockRecorder) GetDefaultLanguage(arg0 any) *gomock.Call { +func (mr *MockQueriesMockRecorder) GetDefaultLanguage(ctx any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDefaultLanguage", reflect.TypeOf((*MockQueries)(nil).GetDefaultLanguage), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDefaultLanguage", reflect.TypeOf((*MockQueries)(nil).GetDefaultLanguage), ctx) } // GetInstanceRestrictions mocks base method. -func (m *MockQueries) GetInstanceRestrictions(arg0 context.Context) (query.Restrictions, error) { +func (m *MockQueries) GetInstanceRestrictions(ctx context.Context) (query.Restrictions, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetInstanceRestrictions", arg0) + ret := m.ctrl.Call(m, "GetInstanceRestrictions", ctx) ret0, _ := ret[0].(query.Restrictions) ret1, _ := ret[1].(error) return ret0, ret1 } // GetInstanceRestrictions indicates an expected call of GetInstanceRestrictions. -func (mr *MockQueriesMockRecorder) GetInstanceRestrictions(arg0 any) *gomock.Call { +func (mr *MockQueriesMockRecorder) GetInstanceRestrictions(ctx any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInstanceRestrictions", reflect.TypeOf((*MockQueries)(nil).GetInstanceRestrictions), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInstanceRestrictions", reflect.TypeOf((*MockQueries)(nil).GetInstanceRestrictions), ctx) } // GetNotifyUserByID mocks base method. -func (m *MockQueries) GetNotifyUserByID(arg0 context.Context, arg1 bool, arg2 string) (*query.NotifyUser, error) { +func (m *MockQueries) GetNotifyUserByID(ctx context.Context, shouldTriggered bool, userID string) (*query.NotifyUser, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetNotifyUserByID", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "GetNotifyUserByID", ctx, shouldTriggered, userID) ret0, _ := ret[0].(*query.NotifyUser) ret1, _ := ret[1].(error) return ret0, ret1 } // GetNotifyUserByID indicates an expected call of GetNotifyUserByID. -func (mr *MockQueriesMockRecorder) GetNotifyUserByID(arg0, arg1, arg2 any) *gomock.Call { +func (mr *MockQueriesMockRecorder) GetNotifyUserByID(ctx, shouldTriggered, userID any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNotifyUserByID", reflect.TypeOf((*MockQueries)(nil).GetNotifyUserByID), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNotifyUserByID", reflect.TypeOf((*MockQueries)(nil).GetNotifyUserByID), ctx, shouldTriggered, userID) } // InstanceByID mocks base method. -func (m *MockQueries) InstanceByID(arg0 context.Context, arg1 string) (authz.Instance, error) { +func (m *MockQueries) InstanceByID(ctx context.Context, id string) (authz.Instance, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "InstanceByID", arg0, arg1) + ret := m.ctrl.Call(m, "InstanceByID", ctx, id) ret0, _ := ret[0].(authz.Instance) ret1, _ := ret[1].(error) return ret0, ret1 } // InstanceByID indicates an expected call of InstanceByID. -func (mr *MockQueriesMockRecorder) InstanceByID(arg0, arg1 any) *gomock.Call { +func (mr *MockQueriesMockRecorder) InstanceByID(ctx, id any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InstanceByID", reflect.TypeOf((*MockQueries)(nil).InstanceByID), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InstanceByID", reflect.TypeOf((*MockQueries)(nil).InstanceByID), ctx, id) } // MailTemplateByOrg mocks base method. -func (m *MockQueries) MailTemplateByOrg(arg0 context.Context, arg1 string, arg2 bool) (*query.MailTemplate, error) { +func (m *MockQueries) MailTemplateByOrg(ctx context.Context, orgID string, withOwnerRemoved bool) (*query.MailTemplate, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "MailTemplateByOrg", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "MailTemplateByOrg", ctx, orgID, withOwnerRemoved) ret0, _ := ret[0].(*query.MailTemplate) ret1, _ := ret[1].(error) return ret0, ret1 } // MailTemplateByOrg indicates an expected call of MailTemplateByOrg. -func (mr *MockQueriesMockRecorder) MailTemplateByOrg(arg0, arg1, arg2 any) *gomock.Call { +func (mr *MockQueriesMockRecorder) MailTemplateByOrg(ctx, orgID, withOwnerRemoved any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MailTemplateByOrg", reflect.TypeOf((*MockQueries)(nil).MailTemplateByOrg), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MailTemplateByOrg", reflect.TypeOf((*MockQueries)(nil).MailTemplateByOrg), ctx, orgID, withOwnerRemoved) } // NotificationPolicyByOrg mocks base method. -func (m *MockQueries) NotificationPolicyByOrg(arg0 context.Context, arg1 bool, arg2 string, arg3 bool) (*query.NotificationPolicy, error) { +func (m *MockQueries) NotificationPolicyByOrg(ctx context.Context, shouldTriggerBulk bool, orgID string, withOwnerRemoved bool) (*query.NotificationPolicy, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "NotificationPolicyByOrg", arg0, arg1, arg2, arg3) + ret := m.ctrl.Call(m, "NotificationPolicyByOrg", ctx, shouldTriggerBulk, orgID, withOwnerRemoved) ret0, _ := ret[0].(*query.NotificationPolicy) ret1, _ := ret[1].(error) return ret0, ret1 } // NotificationPolicyByOrg indicates an expected call of NotificationPolicyByOrg. -func (mr *MockQueriesMockRecorder) NotificationPolicyByOrg(arg0, arg1, arg2, arg3 any) *gomock.Call { +func (mr *MockQueriesMockRecorder) NotificationPolicyByOrg(ctx, shouldTriggerBulk, orgID, withOwnerRemoved any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NotificationPolicyByOrg", reflect.TypeOf((*MockQueries)(nil).NotificationPolicyByOrg), arg0, arg1, arg2, arg3) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NotificationPolicyByOrg", reflect.TypeOf((*MockQueries)(nil).NotificationPolicyByOrg), ctx, shouldTriggerBulk, orgID, withOwnerRemoved) } // NotificationProviderByIDAndType mocks base method. -func (m *MockQueries) NotificationProviderByIDAndType(arg0 context.Context, arg1 string, arg2 domain.NotificationProviderType) (*query.DebugNotificationProvider, error) { +func (m *MockQueries) NotificationProviderByIDAndType(ctx context.Context, aggID string, providerType domain.NotificationProviderType) (*query.DebugNotificationProvider, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "NotificationProviderByIDAndType", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "NotificationProviderByIDAndType", ctx, aggID, providerType) ret0, _ := ret[0].(*query.DebugNotificationProvider) ret1, _ := ret[1].(error) return ret0, ret1 } // NotificationProviderByIDAndType indicates an expected call of NotificationProviderByIDAndType. -func (mr *MockQueriesMockRecorder) NotificationProviderByIDAndType(arg0, arg1, arg2 any) *gomock.Call { +func (mr *MockQueriesMockRecorder) NotificationProviderByIDAndType(ctx, aggID, providerType any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NotificationProviderByIDAndType", reflect.TypeOf((*MockQueries)(nil).NotificationProviderByIDAndType), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NotificationProviderByIDAndType", reflect.TypeOf((*MockQueries)(nil).NotificationProviderByIDAndType), ctx, aggID, providerType) } // SMSProviderConfigActive mocks base method. -func (m *MockQueries) SMSProviderConfigActive(arg0 context.Context, arg1 string) (*query.SMSConfig, error) { +func (m *MockQueries) SMSProviderConfigActive(ctx context.Context, resourceOwner string) (*query.SMSConfig, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SMSProviderConfigActive", arg0, arg1) + ret := m.ctrl.Call(m, "SMSProviderConfigActive", ctx, resourceOwner) ret0, _ := ret[0].(*query.SMSConfig) ret1, _ := ret[1].(error) return ret0, ret1 } // SMSProviderConfigActive indicates an expected call of SMSProviderConfigActive. -func (mr *MockQueriesMockRecorder) SMSProviderConfigActive(arg0, arg1 any) *gomock.Call { +func (mr *MockQueriesMockRecorder) SMSProviderConfigActive(ctx, resourceOwner any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SMSProviderConfigActive", reflect.TypeOf((*MockQueries)(nil).SMSProviderConfigActive), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SMSProviderConfigActive", reflect.TypeOf((*MockQueries)(nil).SMSProviderConfigActive), ctx, resourceOwner) } // SMTPConfigActive mocks base method. -func (m *MockQueries) SMTPConfigActive(arg0 context.Context, arg1 string) (*query.SMTPConfig, error) { +func (m *MockQueries) SMTPConfigActive(ctx context.Context, resourceOwner string) (*query.SMTPConfig, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SMTPConfigActive", arg0, arg1) + ret := m.ctrl.Call(m, "SMTPConfigActive", ctx, resourceOwner) ret0, _ := ret[0].(*query.SMTPConfig) ret1, _ := ret[1].(error) return ret0, ret1 } // SMTPConfigActive indicates an expected call of SMTPConfigActive. -func (mr *MockQueriesMockRecorder) SMTPConfigActive(arg0, arg1 any) *gomock.Call { +func (mr *MockQueriesMockRecorder) SMTPConfigActive(ctx, resourceOwner any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SMTPConfigActive", reflect.TypeOf((*MockQueries)(nil).SMTPConfigActive), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SMTPConfigActive", reflect.TypeOf((*MockQueries)(nil).SMTPConfigActive), ctx, resourceOwner) } // SearchInstanceDomains mocks base method. -func (m *MockQueries) SearchInstanceDomains(arg0 context.Context, arg1 *query.InstanceDomainSearchQueries) (*query.InstanceDomains, error) { +func (m *MockQueries) SearchInstanceDomains(ctx context.Context, queries *query.InstanceDomainSearchQueries) (*query.InstanceDomains, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SearchInstanceDomains", arg0, arg1) + ret := m.ctrl.Call(m, "SearchInstanceDomains", ctx, queries) ret0, _ := ret[0].(*query.InstanceDomains) ret1, _ := ret[1].(error) return ret0, ret1 } // SearchInstanceDomains indicates an expected call of SearchInstanceDomains. -func (mr *MockQueriesMockRecorder) SearchInstanceDomains(arg0, arg1 any) *gomock.Call { +func (mr *MockQueriesMockRecorder) SearchInstanceDomains(ctx, queries any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchInstanceDomains", reflect.TypeOf((*MockQueries)(nil).SearchInstanceDomains), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchInstanceDomains", reflect.TypeOf((*MockQueries)(nil).SearchInstanceDomains), ctx, queries) } // SearchMilestones mocks base method. -func (m *MockQueries) SearchMilestones(arg0 context.Context, arg1 []string, arg2 *query.MilestonesSearchQueries) (*query.Milestones, error) { +func (m *MockQueries) SearchMilestones(ctx context.Context, instanceIDs []string, queries *query.MilestonesSearchQueries) (*query.Milestones, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SearchMilestones", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "SearchMilestones", ctx, instanceIDs, queries) ret0, _ := ret[0].(*query.Milestones) ret1, _ := ret[1].(error) return ret0, ret1 } // SearchMilestones indicates an expected call of SearchMilestones. -func (mr *MockQueriesMockRecorder) SearchMilestones(arg0, arg1, arg2 any) *gomock.Call { +func (mr *MockQueriesMockRecorder) SearchMilestones(ctx, instanceIDs, queries any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchMilestones", reflect.TypeOf((*MockQueries)(nil).SearchMilestones), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchMilestones", reflect.TypeOf((*MockQueries)(nil).SearchMilestones), ctx, instanceIDs, queries) } // SessionByID mocks base method. -func (m *MockQueries) SessionByID(arg0 context.Context, arg1 bool, arg2, arg3 string, arg4 domain.PermissionCheck) (*query.Session, error) { +func (m *MockQueries) SessionByID(ctx context.Context, shouldTriggerBulk bool, id, sessionToken string, check domain.PermissionCheck) (*query.Session, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SessionByID", arg0, arg1, arg2, arg3, arg4) + ret := m.ctrl.Call(m, "SessionByID", ctx, shouldTriggerBulk, id, sessionToken, check) ret0, _ := ret[0].(*query.Session) ret1, _ := ret[1].(error) return ret0, ret1 } // SessionByID indicates an expected call of SessionByID. -func (mr *MockQueriesMockRecorder) SessionByID(arg0, arg1, arg2, arg3, arg4 any) *gomock.Call { +func (mr *MockQueriesMockRecorder) SessionByID(ctx, shouldTriggerBulk, id, sessionToken, check any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SessionByID", reflect.TypeOf((*MockQueries)(nil).SessionByID), arg0, arg1, arg2, arg3, arg4) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SessionByID", reflect.TypeOf((*MockQueries)(nil).SessionByID), ctx, shouldTriggerBulk, id, sessionToken, check) } diff --git a/internal/notification/handlers/mock/queue.mock.go b/internal/notification/handlers/mock/queue.mock.go index e1387595db..e9cf3efed1 100644 --- a/internal/notification/handlers/mock/queue.mock.go +++ b/internal/notification/handlers/mock/queue.mock.go @@ -22,6 +22,7 @@ import ( type MockQueue struct { ctrl *gomock.Controller recorder *MockQueueMockRecorder + isgomock struct{} } // MockQueueMockRecorder is the mock recorder for MockQueue. @@ -42,10 +43,10 @@ func (m *MockQueue) EXPECT() *MockQueueMockRecorder { } // Insert mocks base method. -func (m *MockQueue) Insert(arg0 context.Context, arg1 river.JobArgs, arg2 ...queue.InsertOpt) error { +func (m *MockQueue) Insert(ctx context.Context, args river.JobArgs, opts ...queue.InsertOpt) error { m.ctrl.T.Helper() - varargs := []any{arg0, arg1} - for _, a := range arg2 { + varargs := []any{ctx, args} + for _, a := range opts { varargs = append(varargs, a) } ret := m.ctrl.Call(m, "Insert", varargs...) @@ -54,8 +55,8 @@ func (m *MockQueue) Insert(arg0 context.Context, arg1 river.JobArgs, arg2 ...que } // Insert indicates an expected call of Insert. -func (mr *MockQueueMockRecorder) Insert(arg0, arg1 any, arg2 ...any) *gomock.Call { +func (mr *MockQueueMockRecorder) Insert(ctx, args any, opts ...any) *gomock.Call { mr.mock.ctrl.T.Helper() - varargs := append([]any{arg0, arg1}, arg2...) + varargs := append([]any{ctx, args}, opts...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Insert", reflect.TypeOf((*MockQueue)(nil).Insert), varargs...) } diff --git a/internal/notification/handlers/queries.go b/internal/notification/handlers/queries.go index a3d68e4797..d9ff1b4201 100644 --- a/internal/notification/handlers/queries.go +++ b/internal/notification/handlers/queries.go @@ -2,7 +2,6 @@ package handlers import ( "context" - "time" "github.com/go-jose/go-jose/v4" "golang.org/x/text/language" @@ -30,7 +29,6 @@ type Queries interface { GetInstanceRestrictions(ctx context.Context) (restrictions query.Restrictions, err error) InstanceByID(ctx context.Context, id string) (instance authz.Instance, err error) GetActiveSigningWebKey(ctx context.Context) (*jose.JSONWebKey, error) - ActivePrivateSigningKey(ctx context.Context, t time.Time) (keys *query.PrivateKeys, err error) ActiveInstances() []string } diff --git a/internal/notification/static/i18n/tr.yaml b/internal/notification/static/i18n/tr.yaml new file mode 100644 index 0000000000..3ba72817db --- /dev/null +++ b/internal/notification/static/i18n/tr.yaml @@ -0,0 +1,68 @@ +InitCode: + Title: Kullanıcıyı Başlat + PreHeader: Kullanıcıyı Başlat + Subject: Kullanıcıyı Başlat + Greeting: Merhaba {{.DisplayName}}, + Text: Bu kullanıcı oluşturuldu. Giriş yapmak için {{.PreferredLoginName}} kullanıcı adını kullanın. Başlatma işlemini tamamlamak için lütfen aşağıdaki düğmeye tıklayın. (Kod {{.Code}}) Bu e-postayı siz istemediyseniz, lütfen görmezden gelin. + ButtonText: Başlatmayı tamamla +PasswordReset: + Title: Şifre sıfırla + PreHeader: Şifre sıfırla + Subject: Şifre sıfırla + Greeting: Merhaba {{.DisplayName}}, + Text: Şifre sıfırlama isteği aldık. Şifrenizi sıfırlamak için lütfen aşağıdaki düğmeyi kullanın. (Kod {{.Code}}) Bu e-postayı siz istemediyseniz, lütfen görmezden gelin. + ButtonText: Şifreyi sıfırla +VerifyEmail: + Title: E-postayı doğrula + PreHeader: E-postayı doğrula + Subject: E-postayı doğrula + Greeting: Merhaba {{.DisplayName}}, + Text: Yeni bir e-posta adresi eklendi. E-posta adresinizi doğrulamak için lütfen aşağıdaki düğmeyi kullanın. (Kod {{.Code}}) Yeni bir e-posta eklemediyseniz, lütfen bu e-postayı görmezden gelin. + ButtonText: E-postayı doğrula +VerifyPhone: + Title: Telefonu doğrula + PreHeader: Telefonu doğrula + Subject: Telefonu doğrula + Greeting: Merhaba {{.DisplayName}}, + Text: Yeni bir telefon numarası eklendi. Doğrulamak için lütfen şu kodu kullanın {{.Code}} + ButtonText: Telefonu doğrula +VerifyEmailOTP: + Title: Tek Kullanımlık Şifreyi Doğrula + PreHeader: Tek Kullanımlık Şifreyi Doğrula + Subject: Tek Kullanımlık Şifreyi Doğrula + Greeting: Merhaba {{.DisplayName}}, + Text: Önümüzdeki beş dakika içinde kimlik doğrulaması yapmak için {{.OTP}} tek kullanımlık şifreyi kullanın veya "Kimlik Doğrula" düğmesine tıklayın. + ButtonText: Kimlik Doğrula +VerifySMSOTP: + Text: >- + {{.OTP}}, {{ .Domain }} için tek kullanımlık şifrenizdir. Önümüzdeki {{.Expiry}} içinde kullanın. + + @{{.Domain}} #{{.OTP}} +DomainClaimed: + Title: Domain talep edildi + PreHeader: E-posta / kullanıcı adını değiştir + Subject: Domain talep edildi + Greeting: Merhaba {{.DisplayName}}, + Text: "{{.Domain}} domaini bir organizasyon tarafından talep edildi. Mevcut kullanıcınız {{.Username}} bu organizasyonun parçası değil. Bu nedenle giriş yaparken e-postanızı değiştirmeniz gerekecek. Bu giriş için geçici bir kullanıcı adı ({{.TempUsername}}) oluşturduk." + ButtonText: Giriş Yap +PasswordlessRegistration: + Title: Şifresiz Giriş Ekle + PreHeader: Şifresiz Giriş Ekle + Subject: Şifresiz Giriş Ekle + Greeting: Merhaba {{.DisplayName}}, + Text: Şifresiz giriş için token ekleme isteği aldık. Şifresiz giriş için token'ınızı veya cihazınızı eklemek için lütfen aşağıdaki düğmeyi kullanın. + ButtonText: Şifresiz Giriş Ekle +PasswordChange: + Title: Kullanıcının şifresi değişti + PreHeader: Şifre değiştir + Subject: Kullanıcının şifresi değişti + Greeting: Merhaba {{.DisplayName}}, + Text: Kullanıcınızın şifresi değişti. Bu değişikliği siz yapmadıysanız, lütfen derhal şifrenizi sıfırlamanız önerilir. + ButtonText: Giriş Yap +InviteUser: + Title: "{{.ApplicationName}} için davet" + PreHeader: "{{.ApplicationName}} için davet" + Subject: "{{.ApplicationName}} için davet" + Greeting: Merhaba {{.DisplayName}}, + Text: "Kullanıcınız {{.ApplicationName}} uygulamasına davet edildi. Davet işlemini tamamlamak için lütfen aşağıdaki düğmeye tıklayın. Bu e-postayı siz istemediyseniz, lütfen görmezden gelin." + ButtonText: Daveti kabul et \ No newline at end of file diff --git a/internal/project/model/oidc_config.go b/internal/project/model/oidc_config.go index 50be6c318a..2c482a67a7 100644 --- a/internal/project/model/oidc_config.go +++ b/internal/project/model/oidc_config.go @@ -3,6 +3,8 @@ package model import ( "time" + "github.com/muhlemmer/gu" + "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models" @@ -98,7 +100,7 @@ func GetOIDCCompliance(version OIDCVersion, appType OIDCApplicationType, grantTy for i, grantType := range grantTypes { domainGrantTypes[i] = domain.OIDCGrantType(grantType) } - compliance := domain.GetOIDCV1Compliance(domain.OIDCApplicationType(appType), domainGrantTypes, domain.OIDCAuthMethodType(authMethod), redirectUris) + compliance := domain.GetOIDCV1Compliance(gu.Ptr(domain.OIDCApplicationType(appType)), domainGrantTypes, gu.Ptr(domain.OIDCAuthMethodType(authMethod)), redirectUris) return &Compliance{ NoneCompliant: compliance.NoneCompliant, Problems: compliance.Problems, diff --git a/internal/protoc/protoc-gen-zitadel/zitadel.pb.go.tmpl b/internal/protoc/protoc-gen-zitadel/zitadel.pb.go.tmpl index adb71c42ff..0fb1c3e102 100644 --- a/internal/protoc/protoc-gen-zitadel/zitadel.pb.go.tmpl +++ b/internal/protoc/protoc-gen-zitadel/zitadel.pb.go.tmpl @@ -5,6 +5,7 @@ package {{.GoPackageName}} import ( "github.com/zitadel/zitadel/internal/api/authz" {{if .AuthContext}}"github.com/zitadel/zitadel/internal/api/grpc/server/middleware"{{end}} + {{if .AuthContext}}"github.com/zitadel/zitadel/internal/api/grpc/server/connect_middleware"{{end}} ) var {{.ServiceName}}_AuthMethods = authz.MethodMapping { @@ -23,6 +24,13 @@ func (r *{{ $m.Name }}) OrganizationFromRequest() *middleware.Organization { Domain: r{{$m.OrgMethod}}.GetOrgDomain(), } } + +func (r *{{ $m.Name }}) OrganizationFromRequestConnect() *connect_middleware.Organization { + return &connect_middleware.Organization{ + ID: r{{$m.OrgMethod}}.GetOrgId(), + Domain: r{{$m.OrgMethod}}.GetOrgDomain(), + } +} {{ end }} {{ range $resp := .CustomHTTPResponses}} diff --git a/internal/query/administrators.go b/internal/query/administrators.go new file mode 100644 index 0000000000..20bb955bbd --- /dev/null +++ b/internal/query/administrators.go @@ -0,0 +1,348 @@ +package query + +import ( + "context" + "database/sql" + "slices" + "time" + + sq "github.com/Masterminds/squirrel" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/telemetry/tracing" + "github.com/zitadel/zitadel/internal/zerrors" +) + +type Administrators struct { + SearchResponse + Administrators []*Administrator +} + +type Administrator struct { + Roles database.TextArray[string] + CreationDate time.Time + ChangeDate time.Time + ResourceOwner string + + User *UserAdministrator + Org *OrgAdministrator + Instance *InstanceAdministrator + Project *ProjectAdministrator + ProjectGrant *ProjectGrantAdministrator +} + +type UserAdministrator struct { + UserID string + LoginName string + DisplayName string + ResourceOwner string +} +type OrgAdministrator struct { + OrgID string + Name string +} + +type InstanceAdministrator struct { + InstanceID string + Name string +} + +type ProjectAdministrator struct { + ProjectID string + Name string + ResourceOwner string +} + +type ProjectGrantAdministrator struct { + ProjectID string + ProjectName string + GrantID string + GrantedOrgID string + ResourceOwner string +} + +func NewAdministratorUserResourceOwnerSearchQuery(value string) (SearchQuery, error) { + return NewTextQuery(UserResourceOwnerCol, value, TextEquals) +} + +func NewAdministratorUserLoginNameSearchQuery(value string) (SearchQuery, error) { + return NewTextQuery(LoginNameNameCol, value, TextEquals) +} + +func NewAdministratorUserDisplayNameSearchQuery(value string) (SearchQuery, error) { + return NewTextQuery(HumanDisplayNameCol, value, TextEquals) +} + +func administratorInstancePermissionCheckV2(ctx context.Context, query sq.SelectBuilder, enabled bool) sq.SelectBuilder { + if !enabled { + return query + } + join, args := PermissionClause( + ctx, + InstanceMemberResourceOwner, + domain.PermissionInstanceMemberRead, + OwnedRowsPermissionOption(InstanceMemberUserID), + ) + return query.JoinClause(join, args...) +} + +func administratorOrgPermissionCheckV2(ctx context.Context, query sq.SelectBuilder, enabled bool) sq.SelectBuilder { + if !enabled { + return query + } + join, args := PermissionClause( + ctx, + OrgMemberResourceOwner, + domain.PermissionOrgMemberRead, + OwnedRowsPermissionOption(OrgMemberUserID), + ) + return query.JoinClause(join, args...) +} + +func administratorProjectPermissionCheckV2(ctx context.Context, query sq.SelectBuilder, enabled bool) sq.SelectBuilder { + if !enabled { + return query + } + join, args := PermissionClause( + ctx, + ProjectMemberResourceOwner, + domain.PermissionProjectMemberRead, + WithProjectsPermissionOption(ProjectMemberProjectID), + OwnedRowsPermissionOption(ProjectMemberUserID), + ) + return query.JoinClause(join, args...) +} + +func administratorProjectGrantPermissionCheckV2(ctx context.Context, query sq.SelectBuilder, enabled bool) sq.SelectBuilder { + if !enabled { + return query + } + join, args := PermissionClause( + ctx, + ProjectGrantMemberResourceOwner, + domain.PermissionProjectGrantMemberRead, + WithProjectsPermissionOption(ProjectMemberProjectID), + OwnedRowsPermissionOption(ProjectGrantMemberUserID), + ) + return query.JoinClause(join, args...) +} + +func administratorsCheckPermission(ctx context.Context, administrators *Administrators, permissionCheck domain.PermissionCheck) { + selfUserID := authz.GetCtxData(ctx).UserID + administrators.Administrators = slices.DeleteFunc(administrators.Administrators, + func(administrator *Administrator) bool { + if administrator.User != nil && administrator.User.UserID == selfUserID { + return false + } + if administrator.ProjectGrant != nil { + return administratorProjectGrantCheckPermission(ctx, administrator.ProjectGrant.ResourceOwner, administrator.ProjectGrant.ProjectID, administrator.ProjectGrant.GrantID, administrator.ProjectGrant.GrantedOrgID, permissionCheck) != nil + } + if administrator.Project != nil { + return permissionCheck(ctx, domain.PermissionProjectMemberRead, administrator.Project.ResourceOwner, administrator.Project.ProjectID) != nil + } + if administrator.Org != nil { + return permissionCheck(ctx, domain.PermissionOrgMemberRead, administrator.Org.OrgID, administrator.Org.OrgID) != nil + } + if administrator.Instance != nil { + return permissionCheck(ctx, domain.PermissionInstanceMemberRead, administrator.Instance.InstanceID, administrator.Instance.InstanceID) != nil + } + return true + }, + ) +} + +func administratorProjectGrantCheckPermission(ctx context.Context, resourceOwner, projectID, grantID, grantedOrgID string, permissionCheck domain.PermissionCheck) error { + if err := permissionCheck(ctx, domain.PermissionProjectGrantMemberRead, resourceOwner, grantID); err != nil { + if err := permissionCheck(ctx, domain.PermissionProjectGrantMemberRead, grantedOrgID, grantID); err != nil { + if err := permissionCheck(ctx, domain.PermissionProjectGrantMemberRead, resourceOwner, projectID); err != nil { + return err + } + } + } + return nil +} + +func (q *Queries) SearchAdministrators(ctx context.Context, queries *MembershipSearchQuery, permissionCheck domain.PermissionCheck) (*Administrators, error) { + // removed as permission v2 is not implemented yet for project grant level permissions + // permissionCheckV2 := PermissionV2(ctx, permissionCheck) + admins, err := q.searchAdministrators(ctx, queries, false) + if err != nil { + return nil, err + } + if permissionCheck != nil { // && !authz.GetFeatures(ctx).PermissionCheckV2 { + administratorsCheckPermission(ctx, admins, permissionCheck) + } + return admins, nil +} + +func (q *Queries) searchAdministrators(ctx context.Context, queries *MembershipSearchQuery, permissionCheckV2 bool) (administrators *Administrators, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + query, queryArgs, scan := prepareAdministratorsQuery(ctx, queries, permissionCheckV2) + eq := sq.Eq{membershipInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()} + stmt, args, err := queries.toQuery(query).Where(eq).ToSql() + if err != nil { + return nil, zerrors.ThrowInvalidArgument(err, "QUERY-TODO", "Errors.Query.InvalidRequest") + } + latestState, err := q.latestState(ctx, orgMemberTable, instanceMemberTable, projectMemberTable, projectGrantMemberTable) + if err != nil { + return nil, err + } + queryArgs = append(queryArgs, args...) + + err = q.client.QueryContext(ctx, func(rows *sql.Rows) error { + administrators, err = scan(rows) + return err + }, stmt, queryArgs...) + if err != nil { + return nil, err + } + administrators.State = latestState + return administrators, nil +} + +func prepareAdministratorsQuery(ctx context.Context, queries *MembershipSearchQuery, permissionV2 bool) (sq.SelectBuilder, []interface{}, func(*sql.Rows) (*Administrators, error)) { + query, args := getMembershipFromQuery(ctx, queries, permissionV2) + return sq.Select( + MembershipUserID.identifier(), + membershipRoles.identifier(), + MembershipCreationDate.identifier(), + MembershipChangeDate.identifier(), + membershipResourceOwner.identifier(), + membershipOrgID.identifier(), + membershipIAMID.identifier(), + membershipProjectID.identifier(), + membershipGrantID.identifier(), + ProjectGrantColumnGrantedOrgID.identifier(), + ProjectColumnResourceOwner.identifier(), + ProjectColumnName.identifier(), + OrgColumnName.identifier(), + InstanceColumnName.identifier(), + LoginNameNameCol.identifier(), + HumanDisplayNameCol.identifier(), + MachineNameCol.identifier(), + HumanAvatarURLCol.identifier(), + UserTypeCol.identifier(), + UserResourceOwnerCol.identifier(), + countColumn.identifier(), + ).From(query). + LeftJoin(join(ProjectColumnID, membershipProjectID)). + LeftJoin(join(ProjectGrantColumnGrantID, membershipGrantID)). + LeftJoin(join(OrgColumnID, membershipOrgID)). + LeftJoin(join(InstanceColumnID, membershipInstanceID)). + LeftJoin(join(HumanUserIDCol, OrgMemberUserID)). + LeftJoin(join(MachineUserIDCol, OrgMemberUserID)). + LeftJoin(join(UserIDCol, OrgMemberUserID)). + LeftJoin(join(LoginNameUserIDCol, OrgMemberUserID)). + Where( + sq.Eq{LoginNameIsPrimaryCol.identifier(): true}, + ).PlaceholderFormat(sq.Dollar), + args, + func(rows *sql.Rows) (*Administrators, error) { + administrators := make([]*Administrator, 0) + var count uint64 + for rows.Next() { + + var ( + administrator = new(Administrator) + userID = sql.NullString{} + orgID = sql.NullString{} + instanceID = sql.NullString{} + projectID = sql.NullString{} + grantID = sql.NullString{} + grantedOrgID = sql.NullString{} + projectName = sql.NullString{} + orgName = sql.NullString{} + instanceName = sql.NullString{} + projectResourceOwner = sql.NullString{} + loginName = sql.NullString{} + displayName = sql.NullString{} + machineName = sql.NullString{} + avatarURL = sql.NullString{} + userType = sql.NullInt32{} + userResourceOwner = sql.NullString{} + ) + + err := rows.Scan( + &userID, + &administrator.Roles, + &administrator.CreationDate, + &administrator.ChangeDate, + &administrator.ResourceOwner, + &orgID, + &instanceID, + &projectID, + &grantID, + &grantedOrgID, + &projectResourceOwner, + &projectName, + &orgName, + &instanceName, + &loginName, + &displayName, + &machineName, + &avatarURL, + &userType, + &userResourceOwner, + &count, + ) + + if err != nil { + return nil, err + } + + if userID.Valid { + administrator.User = &UserAdministrator{ + UserID: userID.String, + LoginName: loginName.String, + DisplayName: displayName.String, + ResourceOwner: userResourceOwner.String, + } + } + + if orgID.Valid { + administrator.Org = &OrgAdministrator{ + OrgID: orgID.String, + Name: orgName.String, + } + } + if instanceID.Valid { + administrator.Instance = &InstanceAdministrator{ + InstanceID: instanceID.String, + Name: instanceName.String, + } + } + if projectID.Valid && grantID.Valid && grantedOrgID.Valid { + administrator.ProjectGrant = &ProjectGrantAdministrator{ + ProjectID: projectID.String, + ProjectName: projectName.String, + GrantID: grantID.String, + GrantedOrgID: grantedOrgID.String, + ResourceOwner: projectResourceOwner.String, + } + } else if projectID.Valid { + administrator.Project = &ProjectAdministrator{ + ProjectID: projectID.String, + Name: projectName.String, + ResourceOwner: projectResourceOwner.String, + } + } + + administrators = append(administrators, administrator) + } + + if err := rows.Close(); err != nil { + return nil, zerrors.ThrowInternal(err, "QUERY-TODO", "Errors.Query.CloseRows") + } + + return &Administrators{ + Administrators: administrators, + SearchResponse: SearchResponse{ + Count: count, + }, + }, nil + } +} diff --git a/internal/query/app.go b/internal/query/app.go index 5fed1e3ced..777c295139 100644 --- a/internal/query/app.go +++ b/internal/query/app.go @@ -5,9 +5,11 @@ import ( "database/sql" _ "embed" "errors" + "slices" "time" sq "github.com/Masterminds/squirrel" + "github.com/muhlemmer/gu" "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" @@ -307,6 +309,19 @@ func (q *Queries) AppByProjectAndAppID(ctx context.Context, shouldTriggerBulk bo return app, err } +func (q *Queries) AppByIDWithPermission(ctx context.Context, appID string, activeOnly bool, permissionCheck domain.PermissionCheck) (*App, error) { + app, err := q.AppByID(ctx, appID, activeOnly) + if err != nil { + return nil, err + } + + if err := appCheckPermission(ctx, app.ResourceOwner, app.ProjectID, permissionCheck); err != nil { + return nil, err + } + + return app, nil +} + func (q *Queries) AppByID(ctx context.Context, appID string, activeOnly bool) (app *App, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -455,27 +470,6 @@ func (q *Queries) ProjectIDFromClientID(ctx context.Context, appID string) (id s return id, err } -func (q *Queries) ProjectByOIDCClientID(ctx context.Context, id string) (project *Project, err error) { - ctx, span := tracing.NewSpan(ctx) - defer func() { span.EndWithError(err) }() - - stmt, scan := prepareProjectByOIDCAppQuery() - eq := sq.Eq{ - AppOIDCConfigColumnClientID.identifier(): id, - AppColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), - } - query, args, err := stmt.Where(eq).ToSql() - if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-XhJi4", "Errors.Query.SQLStatement") - } - - err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { - project, err = scan(row) - return err - }, query, args...) - return project, err -} - func (q *Queries) AppByOIDCClientID(ctx context.Context, clientID string) (app *App, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -526,11 +520,25 @@ func (q *Queries) AppByClientID(ctx context.Context, clientID string) (app *App, return app, err } -func (q *Queries) SearchApps(ctx context.Context, queries *AppSearchQueries, withOwnerRemoved bool) (apps *Apps, err error) { +func (q *Queries) SearchApps(ctx context.Context, queries *AppSearchQueries, permissionCheck domain.PermissionCheck) (*Apps, error) { + apps, err := q.searchApps(ctx, queries, PermissionV2(ctx, permissionCheck)) + if err != nil { + return nil, err + } + + if permissionCheck != nil && !authz.GetFeatures(ctx).PermissionCheckV2 { + apps.Apps = appsCheckPermission(ctx, apps.Apps, permissionCheck) + } + return apps, nil +} + +func (q *Queries) searchApps(ctx context.Context, queries *AppSearchQueries, isPermissionV2Enabled bool) (apps *Apps, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() query, scan := prepareAppsQuery() + query = appPermissionCheckV2(ctx, query, isPermissionV2Enabled, queries) + eq := sq.Eq{AppColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()} stmt, args, err := queries.toQuery(query).Where(eq).ToSql() if err != nil { @@ -548,6 +556,21 @@ func (q *Queries) SearchApps(ctx context.Context, queries *AppSearchQueries, wit return apps, err } +func appPermissionCheckV2(ctx context.Context, query sq.SelectBuilder, enabled bool, queries *AppSearchQueries) sq.SelectBuilder { + if !enabled { + return query + } + + join, args := PermissionClause( + ctx, + AppColumnResourceOwner, + domain.PermissionProjectAppRead, + SingleOrgPermissionOption(queries.Queries), + WithProjectsPermissionOption(AppColumnProjectID), + ) + return query.JoinClause(join, args...) +} + func (q *Queries) SearchClientIDs(ctx context.Context, queries *AppSearchQueries, shouldTriggerBulk bool) (ids []string, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -624,10 +647,25 @@ func (q *Queries) SAMLAppLoginVersion(ctx context.Context, appID string) (loginV return loginVersion, nil } +func appCheckPermission(ctx context.Context, resourceOwner string, projectID string, permissionCheck domain.PermissionCheck) error { + return permissionCheck(ctx, domain.PermissionProjectAppRead, resourceOwner, projectID) +} + +// appsCheckPermission returns only the apps that the user in context has permission to read +func appsCheckPermission(ctx context.Context, apps []*App, permissionCheck domain.PermissionCheck) []*App { + return slices.DeleteFunc(apps, func(app *App) bool { + return permissionCheck(ctx, domain.PermissionProjectAppRead, app.ResourceOwner, app.ProjectID) != nil + }) +} + func NewAppNameSearchQuery(method TextComparison, value string) (SearchQuery, error) { return NewTextQuery(AppColumnName, value, method) } +func NewAppStateSearchQuery(value domain.AppState) (SearchQuery, error) { + return NewNumberQuery(AppColumnState, int(value), NumberEquals) +} + func NewAppProjectIDSearchQuery(id string) (SearchQuery, error) { return NewTextQuery(AppColumnProjectID, id, TextEquals) } @@ -867,48 +905,6 @@ func prepareProjectIDByAppQuery() (sq.SelectBuilder, func(*sql.Row) (projectID s } } -func prepareProjectByOIDCAppQuery() (sq.SelectBuilder, func(*sql.Row) (*Project, error)) { - return sq.Select( - ProjectColumnID.identifier(), - ProjectColumnCreationDate.identifier(), - ProjectColumnChangeDate.identifier(), - ProjectColumnResourceOwner.identifier(), - ProjectColumnState.identifier(), - ProjectColumnSequence.identifier(), - ProjectColumnName.identifier(), - ProjectColumnProjectRoleAssertion.identifier(), - ProjectColumnProjectRoleCheck.identifier(), - ProjectColumnHasProjectCheck.identifier(), - ProjectColumnPrivateLabelingSetting.identifier(), - ).From(projectsTable.identifier()). - Join(join(AppColumnProjectID, ProjectColumnID)). - Join(join(AppOIDCConfigColumnAppID, AppColumnID)). - PlaceholderFormat(sq.Dollar), - func(row *sql.Row) (*Project, error) { - p := new(Project) - err := row.Scan( - &p.ID, - &p.CreationDate, - &p.ChangeDate, - &p.ResourceOwner, - &p.State, - &p.Sequence, - &p.Name, - &p.ProjectRoleAssertion, - &p.ProjectRoleCheck, - &p.HasProjectCheck, - &p.PrivateLabelingSetting, - ) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return nil, zerrors.ThrowNotFound(err, "QUERY-yxTMh", "Errors.Project.NotFound") - } - return nil, zerrors.ThrowInternal(err, "QUERY-dj2FF", "Errors.Internal") - } - return p, nil - } -} - func prepareProjectByAppQuery() (sq.SelectBuilder, func(*sql.Row) (*Project, error)) { return sq.Select( ProjectColumnID.identifier(), @@ -1181,7 +1177,7 @@ func (c sqlOIDCConfig) set(app *App) { if c.loginBaseURI.Valid { app.OIDCConfig.LoginBaseURI = &c.loginBaseURI.String } - compliance := domain.GetOIDCCompliance(app.OIDCConfig.Version, app.OIDCConfig.AppType, app.OIDCConfig.GrantTypes, app.OIDCConfig.ResponseTypes, app.OIDCConfig.AuthMethodType, app.OIDCConfig.RedirectURIs) + compliance := domain.GetOIDCCompliance(gu.Ptr(app.OIDCConfig.Version), gu.Ptr(app.OIDCConfig.AppType), app.OIDCConfig.GrantTypes, app.OIDCConfig.ResponseTypes, gu.Ptr(app.OIDCConfig.AuthMethodType), app.OIDCConfig.RedirectURIs) app.OIDCConfig.ComplianceProblems = compliance.Problems var err error diff --git a/internal/query/authn_key.go b/internal/query/authn_key.go index 8075422e63..abda5f011e 100644 --- a/internal/query/authn_key.go +++ b/internal/query/authn_key.go @@ -5,6 +5,7 @@ import ( "database/sql" _ "embed" "errors" + "slices" "time" sq "github.com/Masterminds/squirrel" @@ -18,6 +19,14 @@ import ( "github.com/zitadel/zitadel/internal/zerrors" ) +func keysCheckPermission(ctx context.Context, keys *AuthNKeys, permissionCheck domain.PermissionCheck) { + keys.AuthNKeys = slices.DeleteFunc(keys.AuthNKeys, + func(key *AuthNKey) bool { + return userCheckPermission(ctx, key.ResourceOwner, key.AggregateID, permissionCheck) != nil + }, + ) +} + var ( authNKeyTable = table{ name: projection.AuthNKeyTable, @@ -84,10 +93,12 @@ type AuthNKeys struct { type AuthNKey struct { ID string + AggregateID string CreationDate time.Time ChangeDate time.Time ResourceOwner string Sequence uint64 + ApplicationID string Expiration time.Time Type domain.AuthNKeyType @@ -124,12 +135,47 @@ func (q *AuthNKeySearchQueries) toQuery(query sq.SelectBuilder) sq.SelectBuilder return query } -func (q *Queries) SearchAuthNKeys(ctx context.Context, queries *AuthNKeySearchQueries, withOwnerRemoved bool) (authNKeys *AuthNKeys, err error) { +type JoinFilter int + +const ( + JoinFilterUnspecified JoinFilter = iota + JoinFilterApp + JoinFilterUserMachine +) + +// SearchAuthNKeys returns machine or app keys, depending on the join filter. +// If permissionCheck is nil, the keys are not filtered. +// If permissionCheck is not nil and the PermissionCheckV2 feature flag is false, the returned keys are filtered in-memory by the given permission check. +// If permissionCheck is not nil and the PermissionCheckV2 feature flag is true, the returned keys are filtered in the database. +func (q *Queries) SearchAuthNKeys(ctx context.Context, queries *AuthNKeySearchQueries, filter JoinFilter, permissionCheck domain.PermissionCheck) (authNKeys *AuthNKeys, err error) { + permissionCheckV2 := PermissionV2(ctx, permissionCheck) + keys, err := q.searchAuthNKeys(ctx, queries, filter, permissionCheckV2) + if err != nil { + return nil, err + } + if permissionCheck != nil && !authz.GetFeatures(ctx).PermissionCheckV2 { + keysCheckPermission(ctx, keys, permissionCheck) + } + return keys, nil +} + +func (q *Queries) searchAuthNKeys(ctx context.Context, queries *AuthNKeySearchQueries, joinFilter JoinFilter, permissionCheckV2 bool) (authNKeys *AuthNKeys, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() query, scan := prepareAuthNKeysQuery() query = queries.toQuery(query) + switch joinFilter { + case JoinFilterUnspecified: + // Select all authN keys + case JoinFilterApp: + joinCol := ProjectColumnID + query = query.Join(joinCol.table.identifier() + " ON " + AuthNKeyColumnIdentifier.identifier() + " = " + joinCol.identifier()) + case JoinFilterUserMachine: + joinCol := MachineUserIDCol + query = query.Join(joinCol.table.identifier() + " ON " + AuthNKeyColumnIdentifier.identifier() + " = " + joinCol.identifier()) + query = userPermissionCheckV2WithCustomColumns(ctx, query, permissionCheckV2, queries.Queries, AuthNKeyColumnResourceOwner, AuthNKeyColumnIdentifier) + } eq := sq.Eq{ AuthNKeyColumnEnabled.identifier(): true, AuthNKeyColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), @@ -177,6 +223,19 @@ func (q *Queries) SearchAuthNKeysData(ctx context.Context, queries *AuthNKeySear return authNKeys, err } +func (q *Queries) GetAuthNKeyByIDWithPermission(ctx context.Context, shouldTriggerBulk bool, id string, permissionCheck domain.PermissionCheck, queries ...SearchQuery) (*AuthNKey, error) { + key, err := q.GetAuthNKeyByID(ctx, shouldTriggerBulk, id, queries...) + if err != nil { + return nil, err + } + + if err := appCheckPermission(ctx, key.ResourceOwner, key.AggregateID, permissionCheck); err != nil { + return nil, err + } + + return key, nil +} + func (q *Queries) GetAuthNKeyByID(ctx context.Context, shouldTriggerBulk bool, id string, queries ...SearchQuery) (key *AuthNKey, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -209,34 +268,6 @@ func (q *Queries) GetAuthNKeyByID(ctx context.Context, shouldTriggerBulk bool, i return key, err } -func (q *Queries) GetAuthNKeyPublicKeyByIDAndIdentifier(ctx context.Context, id string, identifier string) (key []byte, err error) { - ctx, span := tracing.NewSpan(ctx) - defer func() { span.EndWithError(err) }() - - stmt, scan := prepareAuthNKeyPublicKeyQuery() - eq := sq.And{ - sq.Eq{ - AuthNKeyColumnID.identifier(): id, - AuthNKeyColumnIdentifier.identifier(): identifier, - AuthNKeyColumnEnabled.identifier(): true, - AuthNKeyColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), - }, - sq.Gt{ - AuthNKeyColumnExpiration.identifier(): time.Now(), - }, - } - query, args, err := stmt.Where(eq).ToSql() - if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-DAb32", "Errors.Query.SQLStatement") - } - - err = q.client.QueryRowContext(ctx, func(row *sql.Row) error { - key, err = scan(row) - return err - }, query, args...) - return key, err -} - func NewAuthNKeyResourceOwnerQuery(id string) (SearchQuery, error) { return NewTextQuery(AuthNKeyColumnResourceOwner, id, TextEquals) } @@ -249,6 +280,22 @@ func NewAuthNKeyObjectIDQuery(id string) (SearchQuery, error) { return NewTextQuery(AuthNKeyColumnObjectID, id, TextEquals) } +func NewAuthNKeyIDQuery(id string) (SearchQuery, error) { + return NewTextQuery(AuthNKeyColumnID, id, TextEquals) +} + +func NewAuthNKeyIdentifyerQuery(id string) (SearchQuery, error) { + return NewTextQuery(AuthNKeyColumnIdentifier, id, TextEquals) +} + +func NewAuthNKeyCreationDateQuery(ts time.Time, compare TimestampComparison) (SearchQuery, error) { + return NewTimestampQuery(AuthNKeyColumnCreationDate, ts, compare) +} + +func NewAuthNKeyExpirationDateDateQuery(ts time.Time, compare TimestampComparison) (SearchQuery, error) { + return NewTimestampQuery(AuthNKeyColumnExpiration, ts, compare) +} + //go:embed authn_key_user.sql var authNKeyUserQuery string @@ -288,49 +335,54 @@ func (q *Queries) GetAuthNKeyUser(ctx context.Context, keyID, userID string) (_ } func prepareAuthNKeysQuery() (sq.SelectBuilder, func(rows *sql.Rows) (*AuthNKeys, error)) { - return sq.Select( - AuthNKeyColumnID.identifier(), - AuthNKeyColumnCreationDate.identifier(), - AuthNKeyColumnChangeDate.identifier(), - AuthNKeyColumnResourceOwner.identifier(), - AuthNKeyColumnSequence.identifier(), - AuthNKeyColumnExpiration.identifier(), - AuthNKeyColumnType.identifier(), - countColumn.identifier(), - ).From(authNKeyTable.identifier()). - PlaceholderFormat(sq.Dollar), - func(rows *sql.Rows) (*AuthNKeys, error) { - authNKeys := make([]*AuthNKey, 0) - var count uint64 - for rows.Next() { - authNKey := new(AuthNKey) - err := rows.Scan( - &authNKey.ID, - &authNKey.CreationDate, - &authNKey.ChangeDate, - &authNKey.ResourceOwner, - &authNKey.Sequence, - &authNKey.Expiration, - &authNKey.Type, - &count, - ) - if err != nil { - return nil, err - } - authNKeys = append(authNKeys, authNKey) - } + query := sq.Select( + AuthNKeyColumnID.identifier(), + AuthNKeyColumnAggregateID.identifier(), + AuthNKeyColumnCreationDate.identifier(), + AuthNKeyColumnChangeDate.identifier(), + AuthNKeyColumnResourceOwner.identifier(), + AuthNKeyColumnSequence.identifier(), + AuthNKeyColumnExpiration.identifier(), + AuthNKeyColumnType.identifier(), + AuthNKeyColumnObjectID.identifier(), + countColumn.identifier(), + ).From(authNKeyTable.identifier()). + PlaceholderFormat(sq.Dollar) - if err := rows.Close(); err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-Dgfn3", "Errors.Query.CloseRows") + return query, func(rows *sql.Rows) (*AuthNKeys, error) { + authNKeys := make([]*AuthNKey, 0) + var count uint64 + for rows.Next() { + authNKey := new(AuthNKey) + err := rows.Scan( + &authNKey.ID, + &authNKey.AggregateID, + &authNKey.CreationDate, + &authNKey.ChangeDate, + &authNKey.ResourceOwner, + &authNKey.Sequence, + &authNKey.Expiration, + &authNKey.Type, + &authNKey.ApplicationID, + &count, + ) + if err != nil { + return nil, err } - - return &AuthNKeys{ - AuthNKeys: authNKeys, - SearchResponse: SearchResponse{ - Count: count, - }, - }, nil + authNKeys = append(authNKeys, authNKey) } + + if err := rows.Close(); err != nil { + return nil, zerrors.ThrowInternal(err, "QUERY-Dgfn3", "Errors.Query.CloseRows") + } + + return &AuthNKeys{ + AuthNKeys: authNKeys, + SearchResponse: SearchResponse{ + Count: count, + }, + }, nil + } } func prepareAuthNKeyQuery() (sq.SelectBuilder, func(row *sql.Row) (*AuthNKey, error)) { @@ -365,26 +417,6 @@ func prepareAuthNKeyQuery() (sq.SelectBuilder, func(row *sql.Row) (*AuthNKey, er } } -func prepareAuthNKeyPublicKeyQuery() (sq.SelectBuilder, func(row *sql.Row) ([]byte, error)) { - return sq.Select( - AuthNKeyColumnPublicKey.identifier(), - ).From(authNKeyTable.identifier()). - PlaceholderFormat(sq.Dollar), - func(row *sql.Row) ([]byte, error) { - var publicKey []byte - err := row.Scan( - &publicKey, - ) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - return nil, zerrors.ThrowNotFound(err, "QUERY-SDf32", "Errors.AuthNKey.NotFound") - } - return nil, zerrors.ThrowInternal(err, "QUERY-Bfs2a", "Errors.Internal") - } - return publicKey, nil - } -} - func prepareAuthNKeysDataQuery() (sq.SelectBuilder, func(rows *sql.Rows) (*AuthNKeysData, error)) { return sq.Select( AuthNKeyColumnID.identifier(), diff --git a/internal/query/authn_key_test.go b/internal/query/authn_key_test.go index c7441f8dae..619ffaac8c 100644 --- a/internal/query/authn_key_test.go +++ b/internal/query/authn_key_test.go @@ -19,22 +19,26 @@ import ( var ( prepareAuthNKeysStmt = `SELECT projections.authn_keys2.id,` + + ` projections.authn_keys2.aggregate_id,` + ` projections.authn_keys2.creation_date,` + ` projections.authn_keys2.change_date,` + ` projections.authn_keys2.resource_owner,` + ` projections.authn_keys2.sequence,` + ` projections.authn_keys2.expiration,` + ` projections.authn_keys2.type,` + + ` projections.authn_keys2.object_id,` + ` COUNT(*) OVER ()` + ` FROM projections.authn_keys2` prepareAuthNKeysCols = []string{ "id", + "aggregate_id", "creation_date", "change_date", "resource_owner", "sequence", "expiration", "type", + "object_id", "count", } @@ -120,12 +124,14 @@ func Test_AuthNKeyPrepares(t *testing.T) { [][]driver.Value{ { "id", + "aggId", testNow, testNow, "ro", uint64(20211109), testNow, 1, + "app1", }, }, ), @@ -137,12 +143,14 @@ func Test_AuthNKeyPrepares(t *testing.T) { AuthNKeys: []*AuthNKey{ { ID: "id", + AggregateID: "aggId", CreationDate: testNow, ChangeDate: testNow, ResourceOwner: "ro", Sequence: 20211109, Expiration: testNow, Type: domain.AuthNKeyTypeJSON, + ApplicationID: "app1", }, }, }, @@ -157,21 +165,25 @@ func Test_AuthNKeyPrepares(t *testing.T) { [][]driver.Value{ { "id-1", + "aggId-1", testNow, testNow, "ro", uint64(20211109), testNow, 1, + "app1", }, { "id-2", + "aggId-2", testNow, testNow, "ro", uint64(20211109), testNow, 1, + "app1", }, }, ), @@ -183,21 +195,25 @@ func Test_AuthNKeyPrepares(t *testing.T) { AuthNKeys: []*AuthNKey{ { ID: "id-1", + AggregateID: "aggId-1", CreationDate: testNow, ChangeDate: testNow, ResourceOwner: "ro", Sequence: 20211109, Expiration: testNow, Type: domain.AuthNKeyTypeJSON, + ApplicationID: "app1", }, { ID: "id-2", + AggregateID: "aggId-2", CreationDate: testNow, ChangeDate: testNow, ResourceOwner: "ro", Sequence: 20211109, Expiration: testNow, Type: domain.AuthNKeyTypeJSON, + ApplicationID: "app1", }, }, }, @@ -415,55 +431,6 @@ func Test_AuthNKeyPrepares(t *testing.T) { }, object: (*AuthNKey)(nil), }, - { - name: "prepareAuthNKeyPublicKeyQuery no result", - prepare: prepareAuthNKeyPublicKeyQuery, - want: want{ - sqlExpectations: mockQueriesScanErr( - regexp.QuoteMeta(prepareAuthNKeyPublicKeyStmt), - nil, - nil, - ), - err: func(err error) (error, bool) { - if !zerrors.IsNotFound(err) { - return fmt.Errorf("err should be zitadel.NotFoundError got: %w", err), false - } - return nil, true - }, - }, - object: ([]byte)(nil), - }, - { - name: "prepareAuthNKeyPublicKeyQuery found", - prepare: prepareAuthNKeyPublicKeyQuery, - want: want{ - sqlExpectations: mockQuery( - regexp.QuoteMeta(prepareAuthNKeyPublicKeyStmt), - prepareAuthNKeyPublicKeyCols, - []driver.Value{ - []byte("publicKey"), - }, - ), - }, - object: []byte("publicKey"), - }, - { - name: "prepareAuthNKeyPublicKeyQuery sql err", - prepare: prepareAuthNKeyPublicKeyQuery, - want: want{ - sqlExpectations: mockQueryErr( - regexp.QuoteMeta(prepareAuthNKeyPublicKeyStmt), - sql.ErrConnDone, - ), - err: func(err error) (error, bool) { - if !errors.Is(err, sql.ErrConnDone) { - return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false - } - return nil, true - }, - }, - object: ([]byte)(nil), - }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/internal/query/hosted_login_translation.go b/internal/query/hosted_login_translation.go new file mode 100644 index 0000000000..82193d2069 --- /dev/null +++ b/internal/query/hosted_login_translation.go @@ -0,0 +1,256 @@ +package query + +import ( + "context" + "crypto/md5" + "database/sql" + _ "embed" + "encoding/hex" + "encoding/json" + "fmt" + "time" + + "dario.cat/mergo" + sq "github.com/Masterminds/squirrel" + "github.com/zitadel/logging" + "golang.org/x/text/language" + "google.golang.org/protobuf/types/known/structpb" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/query/projection" + "github.com/zitadel/zitadel/internal/repository/instance" + "github.com/zitadel/zitadel/internal/telemetry/tracing" + "github.com/zitadel/zitadel/internal/v2/org" + "github.com/zitadel/zitadel/internal/zerrors" + "github.com/zitadel/zitadel/pkg/grpc/settings/v2" +) + +var ( + //go:embed v2-default.json + defaultLoginTranslations []byte + + defaultSystemTranslations map[language.Tag]map[string]any + + hostedLoginTranslationTable = table{ + name: projection.HostedLoginTranslationTable, + instanceIDCol: projection.HostedLoginTranslationInstanceIDCol, + } + + hostedLoginTranslationColInstanceID = Column{ + name: projection.HostedLoginTranslationInstanceIDCol, + table: hostedLoginTranslationTable, + } + hostedLoginTranslationColResourceOwner = Column{ + name: projection.HostedLoginTranslationAggregateIDCol, + table: hostedLoginTranslationTable, + } + hostedLoginTranslationColResourceOwnerType = Column{ + name: projection.HostedLoginTranslationAggregateTypeCol, + table: hostedLoginTranslationTable, + } + hostedLoginTranslationColLocale = Column{ + name: projection.HostedLoginTranslationLocaleCol, + table: hostedLoginTranslationTable, + } + hostedLoginTranslationColFile = Column{ + name: projection.HostedLoginTranslationFileCol, + table: hostedLoginTranslationTable, + } + hostedLoginTranslationColEtag = Column{ + name: projection.HostedLoginTranslationEtagCol, + table: hostedLoginTranslationTable, + } +) + +func init() { + err := json.Unmarshal(defaultLoginTranslations, &defaultSystemTranslations) + if err != nil { + panic(err) + } +} + +type HostedLoginTranslations struct { + SearchResponse + HostedLoginTranslations []*HostedLoginTranslation +} + +type HostedLoginTranslation struct { + AggregateID string + Sequence uint64 + CreationDate time.Time + ChangeDate time.Time + + Locale string + File map[string]any + LevelType string + LevelID string + Etag string +} + +func (q *Queries) GetHostedLoginTranslation(ctx context.Context, req *settings.GetHostedLoginTranslationRequest) (res *settings.GetHostedLoginTranslationResponse, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + inst := authz.GetInstance(ctx) + defaultInstLang := inst.DefaultLanguage() + + lang, err := language.BCP47.Parse(req.GetLocale()) + if err != nil || lang.IsRoot() { + return nil, zerrors.ThrowInvalidArgument(nil, "QUERY-rZLAGi", "Errors.Arguments.Locale.Invalid") + } + parentLang := lang.Parent() + if parentLang.IsRoot() { + parentLang = lang + } + + sysTranslation, systemEtag, err := getSystemTranslation(parentLang, defaultInstLang) + if err != nil { + return nil, err + } + + var levelID, resourceOwner string + switch t := req.GetLevel().(type) { + case *settings.GetHostedLoginTranslationRequest_System: + return getTranslationOutputMessage(sysTranslation, systemEtag) + case *settings.GetHostedLoginTranslationRequest_Instance: + levelID = authz.GetInstance(ctx).InstanceID() + resourceOwner = instance.AggregateType + case *settings.GetHostedLoginTranslationRequest_OrganizationId: + levelID = t.OrganizationId + resourceOwner = org.AggregateType + default: + return nil, zerrors.ThrowInvalidArgument(nil, "QUERY-YB6Sri", "Errors.Arguments.Level.Invalid") + } + + stmt, scan := prepareHostedLoginTranslationQuery() + + langORBaseLang := sq.Or{ + sq.Eq{hostedLoginTranslationColLocale.identifier(): lang.String()}, + sq.Eq{hostedLoginTranslationColLocale.identifier(): parentLang.String()}, + } + eq := sq.Eq{ + hostedLoginTranslationColInstanceID.identifier(): inst.InstanceID(), + hostedLoginTranslationColResourceOwner.identifier(): levelID, + hostedLoginTranslationColResourceOwnerType.identifier(): resourceOwner, + } + + query, args, err := stmt.Where(eq).Where(langORBaseLang).ToSql() + if err != nil { + logging.WithError(err).Error("unable to generate sql statement") + return nil, zerrors.ThrowInternal(err, "QUERY-ZgCMux", "Errors.Query.SQLStatement") + } + + var trs []*HostedLoginTranslation + err = q.client.QueryContext(ctx, func(rows *sql.Rows) error { + trs, err = scan(rows) + return err + }, query, args...) + if err != nil { + logging.WithError(err).Error("failed to query translations") + return nil, zerrors.ThrowInternal(err, "QUERY-6k1zjx", "Errors.Internal") + } + + requestedTranslation, parentTranslation := &HostedLoginTranslation{}, &HostedLoginTranslation{} + for _, tr := range trs { + if tr == nil { + continue + } + + if tr.LevelType == resourceOwner { + requestedTranslation = tr + } else { + parentTranslation = tr + } + } + + if !req.GetIgnoreInheritance() { + + // There is no record for the requested level, set the upper level etag + if requestedTranslation.Etag == "" { + requestedTranslation.Etag = parentTranslation.Etag + } + + // Case where Level == ORGANIZATION -> Check if we have an instance level translation + // If so, merge it with the translations we have + if parentTranslation != nil && parentTranslation.LevelType == instance.AggregateType { + if err := mergo.Merge(&requestedTranslation.File, parentTranslation.File); err != nil { + return nil, zerrors.ThrowInternal(err, "QUERY-pdgEJd", "Errors.Query.MergeTranslations") + } + } + + // The DB query returned no results, we have to set the system translation etag + if requestedTranslation.Etag == "" { + requestedTranslation.Etag = systemEtag + } + + // Merge the system translations + if err := mergo.Merge(&requestedTranslation.File, sysTranslation); err != nil { + return nil, zerrors.ThrowInternal(err, "QUERY-HdprNF", "Errors.Query.MergeTranslations") + } + } + + return getTranslationOutputMessage(requestedTranslation.File, requestedTranslation.Etag) +} + +func getSystemTranslation(lang, instanceDefaultLang language.Tag) (map[string]any, string, error) { + translation, ok := defaultSystemTranslations[lang] + if !ok { + translation, ok = defaultSystemTranslations[instanceDefaultLang] + if !ok { + return nil, "", zerrors.ThrowNotFoundf(nil, "QUERY-6gb5QR", "Errors.Query.HostedLoginTranslationNotFound-%s", lang) + } + } + + hash := md5.Sum(fmt.Append(nil, translation)) + + return translation, hex.EncodeToString(hash[:]), nil +} + +func prepareHostedLoginTranslationQuery() (sq.SelectBuilder, func(*sql.Rows) ([]*HostedLoginTranslation, error)) { + return sq.Select( + hostedLoginTranslationColFile.identifier(), + hostedLoginTranslationColResourceOwnerType.identifier(), + hostedLoginTranslationColEtag.identifier(), + ).From(hostedLoginTranslationTable.identifier()). + Limit(2). + PlaceholderFormat(sq.Dollar), + func(r *sql.Rows) ([]*HostedLoginTranslation, error) { + translations := make([]*HostedLoginTranslation, 0, 2) + for r.Next() { + var rawTranslation json.RawMessage + translation := &HostedLoginTranslation{} + err := r.Scan( + &rawTranslation, + &translation.LevelType, + &translation.Etag, + ) + if err != nil { + return nil, err + } + + if err := json.Unmarshal(rawTranslation, &translation.File); err != nil { + return nil, err + } + + translations = append(translations, translation) + } + + if err := r.Close(); err != nil { + return nil, zerrors.ThrowInternal(err, "QUERY-oc7r7i", "Errors.Query.CloseRows") + } + + return translations, nil + } +} + +func getTranslationOutputMessage(translation map[string]any, etag string) (*settings.GetHostedLoginTranslationResponse, error) { + protoTranslation, err := structpb.NewStruct(translation) + if err != nil { + return nil, zerrors.ThrowInternal(err, "QUERY-70ppPp", "Errors.Protobuf.ConvertToStruct") + } + + return &settings.GetHostedLoginTranslationResponse{ + Translations: protoTranslation, + Etag: etag, + }, nil +} diff --git a/internal/query/hosted_login_translation_test.go b/internal/query/hosted_login_translation_test.go new file mode 100644 index 0000000000..0e9f511002 --- /dev/null +++ b/internal/query/hosted_login_translation_test.go @@ -0,0 +1,337 @@ +package query + +import ( + "crypto/md5" + "database/sql" + "database/sql/driver" + "encoding/hex" + "encoding/json" + "fmt" + "maps" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/text/language" + "google.golang.org/protobuf/runtime/protoimpl" + "google.golang.org/protobuf/types/known/structpb" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/database/mock" + "github.com/zitadel/zitadel/internal/zerrors" + "github.com/zitadel/zitadel/pkg/grpc/settings/v2" +) + +func TestGetSystemTranslation(t *testing.T) { + okTranslation := defaultLoginTranslations + + parsedOKTranslation := map[string]map[string]any{} + require.Nil(t, json.Unmarshal(okTranslation, &parsedOKTranslation)) + + hashOK := md5.Sum(fmt.Append(nil, parsedOKTranslation["de"])) + + tt := []struct { + testName string + + inputLanguage language.Tag + inputInstanceLanguage language.Tag + systemTranslationToSet []byte + + expectedLanguage map[string]any + expectedEtag string + expectedError error + }{ + { + testName: "when neither input language nor system default language have translation should return not found error", + systemTranslationToSet: okTranslation, + inputLanguage: language.MustParse("ro"), + inputInstanceLanguage: language.MustParse("fr"), + + expectedError: zerrors.ThrowNotFoundf(nil, "QUERY-6gb5QR", "Errors.Query.HostedLoginTranslationNotFound-%s", "ro"), + }, + { + testName: "when input language has no translation should fallback onto instance default", + systemTranslationToSet: okTranslation, + inputLanguage: language.MustParse("ro"), + inputInstanceLanguage: language.MustParse("de"), + + expectedLanguage: parsedOKTranslation["de"], + expectedEtag: hex.EncodeToString(hashOK[:]), + }, + { + testName: "when input language has translation should return it", + systemTranslationToSet: okTranslation, + inputLanguage: language.MustParse("de"), + inputInstanceLanguage: language.MustParse("en"), + + expectedLanguage: parsedOKTranslation["de"], + expectedEtag: hex.EncodeToString(hashOK[:]), + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + // Given + defaultLoginTranslations = tc.systemTranslationToSet + + // When + translation, etag, err := getSystemTranslation(tc.inputLanguage, tc.inputInstanceLanguage) + + // Verify + require.Equal(t, tc.expectedError, err) + assert.Equal(t, tc.expectedLanguage, translation) + assert.Equal(t, tc.expectedEtag, etag) + }) + } +} + +func TestGetTranslationOutput(t *testing.T) { + t.Parallel() + + validMap := map[string]any{"loginHeader": "A login header"} + protoMap, err := structpb.NewStruct(validMap) + require.NoError(t, err) + + hash := md5.Sum(fmt.Append(nil, validMap)) + encodedHash := hex.EncodeToString(hash[:]) + + tt := []struct { + testName string + inputTranslation map[string]any + expectedError error + expectedResponse *settings.GetHostedLoginTranslationResponse + }{ + { + testName: "when unparsable map should return internal error", + inputTranslation: map[string]any{"\xc5z": "something"}, + expectedError: zerrors.ThrowInternal(protoimpl.X.NewError("invalid UTF-8 in string: %q", "\xc5z"), "QUERY-70ppPp", "Errors.Protobuf.ConvertToStruct"), + }, + { + testName: "when input translation is valid should return expected response message", + inputTranslation: validMap, + expectedResponse: &settings.GetHostedLoginTranslationResponse{ + Translations: protoMap, + Etag: hex.EncodeToString(hash[:]), + }, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + + // When + res, err := getTranslationOutputMessage(tc.inputTranslation, encodedHash) + + // Verify + require.Equal(t, tc.expectedError, err) + assert.Equal(t, tc.expectedResponse, res) + }) + } +} + +func TestGetHostedLoginTranslation(t *testing.T) { + query := `SELECT projections.hosted_login_translations.file, projections.hosted_login_translations.aggregate_type, projections.hosted_login_translations.etag + FROM projections.hosted_login_translations + WHERE projections.hosted_login_translations.aggregate_id = $1 + AND projections.hosted_login_translations.aggregate_type = $2 + AND projections.hosted_login_translations.instance_id = $3 + AND (projections.hosted_login_translations.locale = $4 OR projections.hosted_login_translations.locale = $5) + LIMIT 2` + okTranslation := defaultLoginTranslations + + parsedOKTranslation := map[string]map[string]any{} + require.NoError(t, json.Unmarshal(okTranslation, &parsedOKTranslation)) + + protoDefaultTranslation, err := structpb.NewStruct(parsedOKTranslation["en"]) + require.Nil(t, err) + + defaultWithDBTranslations := maps.Clone(parsedOKTranslation["en"]) + defaultWithDBTranslations["test"] = "translation" + defaultWithDBTranslations["test2"] = "translation2" + protoDefaultWithDBTranslation, err := structpb.NewStruct(defaultWithDBTranslations) + require.NoError(t, err) + + nilProtoDefaultMap, err := structpb.NewStruct(nil) + require.NoError(t, err) + + hashDefaultTranslations := md5.Sum(fmt.Append(nil, parsedOKTranslation["en"])) + + tt := []struct { + testName string + + defaultInstanceLanguage language.Tag + sqlExpectations []mock.Expectation + + inputRequest *settings.GetHostedLoginTranslationRequest + + expectedError error + expectedResult *settings.GetHostedLoginTranslationResponse + }{ + { + testName: "when input language is invalid should return invalid argument error", + + inputRequest: &settings.GetHostedLoginTranslationRequest{}, + + expectedError: zerrors.ThrowInvalidArgument(nil, "QUERY-rZLAGi", "Errors.Arguments.Locale.Invalid"), + }, + { + testName: "when input language is root should return invalid argument error", + + defaultInstanceLanguage: language.English, + inputRequest: &settings.GetHostedLoginTranslationRequest{ + Locale: "root", + }, + + expectedError: zerrors.ThrowInvalidArgument(nil, "QUERY-rZLAGi", "Errors.Arguments.Locale.Invalid"), + }, + { + testName: "when no system translation is available should return not found error", + + defaultInstanceLanguage: language.Romanian, + inputRequest: &settings.GetHostedLoginTranslationRequest{ + Locale: "ro-RO", + }, + + expectedError: zerrors.ThrowNotFoundf(nil, "QUERY-6gb5QR", "Errors.Query.HostedLoginTranslationNotFound-%s", "ro"), + }, + { + testName: "when requesting system translation should return it", + + defaultInstanceLanguage: language.English, + inputRequest: &settings.GetHostedLoginTranslationRequest{ + Locale: "en-US", + Level: &settings.GetHostedLoginTranslationRequest_System{}, + }, + + expectedResult: &settings.GetHostedLoginTranslationResponse{ + Translations: protoDefaultTranslation, + Etag: hex.EncodeToString(hashDefaultTranslations[:]), + }, + }, + { + testName: "when querying DB fails should return internal error", + + defaultInstanceLanguage: language.English, + sqlExpectations: []mock.Expectation{ + mock.ExpectQuery( + query, + mock.WithQueryArgs("123", "org", "instance-id", "en-US", "en"), + mock.WithQueryErr(sql.ErrConnDone), + ), + }, + inputRequest: &settings.GetHostedLoginTranslationRequest{ + Locale: "en-US", + Level: &settings.GetHostedLoginTranslationRequest_OrganizationId{ + OrganizationId: "123", + }, + }, + + expectedError: zerrors.ThrowInternal(sql.ErrConnDone, "QUERY-6k1zjx", "Errors.Internal"), + }, + { + testName: "when querying DB returns no result should return system translations", + + defaultInstanceLanguage: language.English, + sqlExpectations: []mock.Expectation{ + mock.ExpectQuery( + query, + mock.WithQueryArgs("123", "org", "instance-id", "en-US", "en"), + mock.WithQueryResult( + []string{"file", "aggregate_type", "etag"}, + [][]driver.Value{}, + ), + ), + }, + inputRequest: &settings.GetHostedLoginTranslationRequest{ + Locale: "en-US", + Level: &settings.GetHostedLoginTranslationRequest_OrganizationId{ + OrganizationId: "123", + }, + }, + + expectedResult: &settings.GetHostedLoginTranslationResponse{ + Translations: protoDefaultTranslation, + Etag: hex.EncodeToString(hashDefaultTranslations[:]), + }, + }, + { + testName: "when querying DB returns no result and inheritance disabled should return empty result", + + defaultInstanceLanguage: language.English, + sqlExpectations: []mock.Expectation{ + mock.ExpectQuery( + query, + mock.WithQueryArgs("123", "org", "instance-id", "en-US", "en"), + mock.WithQueryResult( + []string{"file", "aggregate_type", "etag"}, + [][]driver.Value{}, + ), + ), + }, + inputRequest: &settings.GetHostedLoginTranslationRequest{ + Locale: "en-US", + Level: &settings.GetHostedLoginTranslationRequest_OrganizationId{ + OrganizationId: "123", + }, + IgnoreInheritance: true, + }, + + expectedResult: &settings.GetHostedLoginTranslationResponse{ + Etag: "", + Translations: nilProtoDefaultMap, + }, + }, + { + testName: "when querying DB returns records should return merged result", + + defaultInstanceLanguage: language.English, + sqlExpectations: []mock.Expectation{ + mock.ExpectQuery( + query, + mock.WithQueryArgs("123", "org", "instance-id", "en-US", "en"), + mock.WithQueryResult( + []string{"file", "aggregate_type", "etag"}, + [][]driver.Value{ + {[]byte(`{"test": "translation"}`), "org", "etag-org"}, + {[]byte(`{"test2": "translation2"}`), "instance", "etag-instance"}, + }, + ), + ), + }, + inputRequest: &settings.GetHostedLoginTranslationRequest{ + Locale: "en-US", + Level: &settings.GetHostedLoginTranslationRequest_OrganizationId{ + OrganizationId: "123", + }, + }, + + expectedResult: &settings.GetHostedLoginTranslationResponse{ + Etag: "etag-org", + Translations: protoDefaultWithDBTranslation, + }, + }, + } + + for _, tc := range tt { + t.Run(tc.testName, func(t *testing.T) { + // Given + db := &database.DB{DB: mock.NewSQLMock(t, tc.sqlExpectations...).DB} + querier := Queries{client: db} + + ctx := authz.NewMockContext("instance-id", "org-id", "user-id", authz.WithMockDefaultLanguage(tc.defaultInstanceLanguage)) + + // When + res, err := querier.GetHostedLoginTranslation(ctx, tc.inputRequest) + + // Verify + require.Equal(t, tc.expectedError, err) + + if tc.expectedError == nil { + assert.Equal(t, tc.expectedResult.GetEtag(), res.GetEtag()) + assert.Equal(t, tc.expectedResult.GetTranslations().GetFields(), res.GetTranslations().GetFields()) + } + }) + } +} diff --git a/internal/query/idp_user_link.go b/internal/query/idp_user_link.go index 23305dfd6e..7f162f235e 100644 --- a/internal/query/idp_user_link.go +++ b/internal/query/idp_user_link.go @@ -106,12 +106,27 @@ func idpLinksCheckPermission(ctx context.Context, links *IDPUserLinks, permissio ) } +func idpLinksPermissionCheckV2(ctx context.Context, query sq.SelectBuilder, enabled bool, queries *IDPUserLinksSearchQuery) sq.SelectBuilder { + if !enabled { + return query + } + join, args := PermissionClause( + ctx, + IDPUserLinkResourceOwnerCol, + domain.PermissionUserRead, + SingleOrgPermissionOption(queries.Queries), + OwnedRowsPermissionOption(IDPUserLinkUserIDCol), + ) + return query.JoinClause(join, args...) +} + func (q *Queries) IDPUserLinks(ctx context.Context, queries *IDPUserLinksSearchQuery, permissionCheck domain.PermissionCheck) (idps *IDPUserLinks, err error) { - links, err := q.idpUserLinks(ctx, queries, false) + permissionCheckV2 := PermissionV2(ctx, permissionCheck) + links, err := q.idpUserLinks(ctx, queries, permissionCheckV2) if err != nil { return nil, err } - if permissionCheck != nil && len(links.Links) > 0 { + if permissionCheck != nil && len(links.Links) > 0 && !permissionCheckV2 { // when userID for query is provided, only one check has to be done if queries.hasUserID() { if err := userCheckPermission(ctx, links.Links[0].ResourceOwner, links.Links[0].UserID, permissionCheck); err != nil { @@ -124,14 +139,15 @@ func (q *Queries) IDPUserLinks(ctx context.Context, queries *IDPUserLinksSearchQ return links, nil } -func (q *Queries) idpUserLinks(ctx context.Context, queries *IDPUserLinksSearchQuery, withOwnerRemoved bool) (idps *IDPUserLinks, err error) { +func (q *Queries) idpUserLinks(ctx context.Context, queries *IDPUserLinksSearchQuery, permissionCheckV2 bool) (idps *IDPUserLinks, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() query, scan := prepareIDPUserLinksQuery() - eq := sq.Eq{IDPUserLinkInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID()} - if !withOwnerRemoved { - eq[IDPUserLinkOwnerRemovedCol.identifier()] = false + query = idpLinksPermissionCheckV2(ctx, query, permissionCheckV2, queries) + eq := sq.Eq{ + IDPUserLinkInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(), + IDPUserLinkOwnerRemovedCol.identifier(): false, } stmt, args, err := queries.toQuery(query).Where(eq).ToSql() if err != nil { diff --git a/internal/query/instance.go b/internal/query/instance.go index 1b3bb055cb..bb311cbb85 100644 --- a/internal/query/instance.go +++ b/internal/query/instance.go @@ -150,7 +150,7 @@ func (q *Queries) SearchInstances(ctx context.Context, queries *InstanceSearchQu ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - filter, query, scan := prepareInstancesQuery() + filter, query, scan := prepareInstancesQuery(queries.SortingColumn, queries.Asc) stmt, args, err := query(queries.toQuery(filter)).ToSql() if err != nil { return nil, zerrors.ThrowInvalidArgument(err, "QUERY-M9fow", "Errors.Query.SQLStatement") @@ -260,17 +260,20 @@ func (q *Queries) GetDefaultLanguage(ctx context.Context) language.Tag { return instance.DefaultLang } -func prepareInstancesQuery() (sq.SelectBuilder, func(sq.SelectBuilder) sq.SelectBuilder, func(*sql.Rows) (*Instances, error)) { +func prepareInstancesQuery(sortBy Column, isAscedingSort bool) (sq.SelectBuilder, func(sq.SelectBuilder) sq.SelectBuilder, func(*sql.Rows) (*Instances, error)) { instanceFilterTable := instanceTable.setAlias(InstancesFilterTableAlias) instanceFilterIDColumn := InstanceColumnID.setTable(instanceFilterTable) instanceFilterCountColumn := InstancesFilterTableAlias + ".count" - return sq.Select( - InstanceColumnID.identifier(), - countColumn.identifier(), - ).Distinct().From(instanceTable.identifier()). + + selector := sq.Select(InstanceColumnID.identifier(), countColumn.identifier()) + if !sortBy.isZero() { + selector = sq.Select(InstanceColumnID.identifier(), countColumn.identifier(), sortBy.identifier()) + } + + return selector.Distinct().From(instanceTable.identifier()). LeftJoin(join(InstanceDomainInstanceIDCol, InstanceColumnID)), func(builder sq.SelectBuilder) sq.SelectBuilder { - return sq.Select( + outerQuery := sq.Select( instanceFilterCountColumn, instanceFilterIDColumn.identifier(), InstanceColumnCreationDate.identifier(), @@ -292,6 +295,16 @@ func prepareInstancesQuery() (sq.SelectBuilder, func(sq.SelectBuilder) sq.Select LeftJoin(join(InstanceColumnID, instanceFilterIDColumn)). LeftJoin(join(InstanceDomainInstanceIDCol, instanceFilterIDColumn)). PlaceholderFormat(sq.Dollar) + + if !sortBy.isZero() { + sorting := sortBy.identifier() + if !isAscedingSort { + sorting += " DESC" + } + return outerQuery.OrderBy(sorting) + } + + return outerQuery }, func(rows *sql.Rows) (*Instances, error) { instances := make([]*Instance, 0) diff --git a/internal/query/instance_features.go b/internal/query/instance_features.go index 4ec40dc9d5..73f51bfdf7 100644 --- a/internal/query/instance_features.go +++ b/internal/query/instance_features.go @@ -8,21 +8,18 @@ import ( ) type InstanceFeatures struct { - Details *domain.ObjectDetails - LoginDefaultOrg FeatureSource[bool] - TriggerIntrospectionProjections FeatureSource[bool] - LegacyIntrospection FeatureSource[bool] - UserSchema FeatureSource[bool] - TokenExchange FeatureSource[bool] - ImprovedPerformance FeatureSource[[]feature.ImprovedPerformanceType] - WebKey FeatureSource[bool] - DebugOIDCParentError FeatureSource[bool] - OIDCSingleV1SessionTermination FeatureSource[bool] - DisableUserTokenEvent FeatureSource[bool] - EnableBackChannelLogout FeatureSource[bool] - LoginV2 FeatureSource[*feature.LoginV2] - PermissionCheckV2 FeatureSource[bool] - ConsoleUseV2UserApi FeatureSource[bool] + Details *domain.ObjectDetails + LoginDefaultOrg FeatureSource[bool] + UserSchema FeatureSource[bool] + TokenExchange FeatureSource[bool] + ImprovedPerformance FeatureSource[[]feature.ImprovedPerformanceType] + DebugOIDCParentError FeatureSource[bool] + OIDCSingleV1SessionTermination FeatureSource[bool] + DisableUserTokenEvent FeatureSource[bool] + EnableBackChannelLogout FeatureSource[bool] + LoginV2 FeatureSource[*feature.LoginV2] + PermissionCheckV2 FeatureSource[bool] + ConsoleUseV2UserApi FeatureSource[bool] } func (q *Queries) GetInstanceFeatures(ctx context.Context, cascade bool) (_ *InstanceFeatures, err error) { diff --git a/internal/query/instance_features_model.go b/internal/query/instance_features_model.go index 6a0abbb58c..fa0f638bed 100644 --- a/internal/query/instance_features_model.go +++ b/internal/query/instance_features_model.go @@ -63,12 +63,9 @@ func (m *InstanceFeaturesReadModel) Query() *eventstore.SearchQueryBuilder { feature_v1.DefaultLoginInstanceEventType, feature_v2.InstanceResetEventType, feature_v2.InstanceLoginDefaultOrgEventType, - feature_v2.InstanceTriggerIntrospectionProjectionsEventType, - feature_v2.InstanceLegacyIntrospectionEventType, feature_v2.InstanceUserSchemaEventType, feature_v2.InstanceTokenExchangeEventType, feature_v2.InstanceImprovedPerformanceEventType, - feature_v2.InstanceWebKeyEventType, feature_v2.InstanceDebugOIDCParentErrorEventType, feature_v2.InstanceOIDCSingleV1SessionTerminationEventType, feature_v2.InstanceDisableUserTokenEvent, @@ -93,8 +90,6 @@ func (m *InstanceFeaturesReadModel) populateFromSystem() bool { return false } m.instance.LoginDefaultOrg = m.system.LoginDefaultOrg - m.instance.TriggerIntrospectionProjections = m.system.TriggerIntrospectionProjections - m.instance.LegacyIntrospection = m.system.LegacyIntrospection m.instance.UserSchema = m.system.UserSchema m.instance.TokenExchange = m.system.TokenExchange m.instance.ImprovedPerformance = m.system.ImprovedPerformance @@ -111,23 +106,16 @@ func reduceInstanceFeatureSet[T any](features *InstanceFeatures, event *feature_ return err } switch key { - case feature.KeyUnspecified, - feature.KeyActionsDeprecated: + case feature.KeyUnspecified: return nil case feature.KeyLoginDefaultOrg: features.LoginDefaultOrg.set(level, event.Value) - case feature.KeyTriggerIntrospectionProjections: - features.TriggerIntrospectionProjections.set(level, event.Value) - case feature.KeyLegacyIntrospection: - features.LegacyIntrospection.set(level, event.Value) case feature.KeyUserSchema: features.UserSchema.set(level, event.Value) case feature.KeyTokenExchange: features.TokenExchange.set(level, event.Value) case feature.KeyImprovedPerformance: features.ImprovedPerformance.set(level, event.Value) - case feature.KeyWebKey: - features.WebKey.set(level, event.Value) case feature.KeyDebugOIDCParentError: features.DebugOIDCParentError.set(level, event.Value) case feature.KeyOIDCSingleV1SessionTermination: diff --git a/internal/query/instance_features_test.go b/internal/query/instance_features_test.go index d80a3b05fc..5c5f8ecc64 100644 --- a/internal/query/instance_features_test.go +++ b/internal/query/instance_features_test.go @@ -71,14 +71,6 @@ func TestQueries_GetInstanceFeatures(t *testing.T) { Level: feature.LevelUnspecified, Value: false, }, - TriggerIntrospectionProjections: FeatureSource[bool]{ - Level: feature.LevelUnspecified, - Value: false, - }, - LegacyIntrospection: FeatureSource[bool]{ - Level: feature.LevelUnspecified, - Value: false, - }, }, }, { @@ -93,14 +85,6 @@ func TestQueries_GetInstanceFeatures(t *testing.T) { ctx, aggregate, feature_v2.InstanceLoginDefaultOrgEventType, false, )), - eventFromEventPusher(feature_v2.NewSetEvent( - ctx, aggregate, - feature_v2.InstanceTriggerIntrospectionProjectionsEventType, true, - )), - eventFromEventPusher(feature_v2.NewSetEvent( - ctx, aggregate, - feature_v2.InstanceLegacyIntrospectionEventType, false, - )), eventFromEventPusher(feature_v2.NewSetEvent( ctx, aggregate, feature_v2.InstanceUserSchemaEventType, false, @@ -116,14 +100,6 @@ func TestQueries_GetInstanceFeatures(t *testing.T) { Level: feature.LevelInstance, Value: false, }, - TriggerIntrospectionProjections: FeatureSource[bool]{ - Level: feature.LevelInstance, - Value: true, - }, - LegacyIntrospection: FeatureSource[bool]{ - Level: feature.LevelInstance, - Value: false, - }, UserSchema: FeatureSource[bool]{ Level: feature.LevelInstance, Value: false, @@ -142,14 +118,6 @@ func TestQueries_GetInstanceFeatures(t *testing.T) { ctx, aggregate, feature_v2.InstanceLoginDefaultOrgEventType, false, )), - eventFromEventPusher(feature_v2.NewSetEvent( - ctx, aggregate, - feature_v2.InstanceTriggerIntrospectionProjectionsEventType, true, - )), - eventFromEventPusher(feature_v2.NewSetEvent( - ctx, aggregate, - feature_v2.InstanceLegacyIntrospectionEventType, false, - )), eventFromEventPusher(feature_v2.NewSetEvent( ctx, aggregate, feature_v2.InstanceUserSchemaEventType, false, @@ -158,10 +126,6 @@ func TestQueries_GetInstanceFeatures(t *testing.T) { ctx, aggregate, feature_v2.InstanceResetEventType, )), - eventFromEventPusher(feature_v2.NewSetEvent( - ctx, aggregate, - feature_v2.InstanceTriggerIntrospectionProjectionsEventType, true, - )), ), ), args: args{true}, @@ -173,14 +137,6 @@ func TestQueries_GetInstanceFeatures(t *testing.T) { Level: feature.LevelSystem, Value: true, }, - TriggerIntrospectionProjections: FeatureSource[bool]{ - Level: feature.LevelInstance, - Value: true, - }, - LegacyIntrospection: FeatureSource[bool]{ - Level: feature.LevelUnspecified, - Value: false, - }, UserSchema: FeatureSource[bool]{ Level: feature.LevelUnspecified, Value: false, @@ -195,14 +151,6 @@ func TestQueries_GetInstanceFeatures(t *testing.T) { ctx, aggregate, feature_v2.InstanceLoginDefaultOrgEventType, false, )), - eventFromEventPusher(feature_v2.NewSetEvent( - ctx, aggregate, - feature_v2.InstanceTriggerIntrospectionProjectionsEventType, true, - )), - eventFromEventPusher(feature_v2.NewSetEvent( - ctx, aggregate, - feature_v2.InstanceLegacyIntrospectionEventType, false, - )), eventFromEventPusher(feature_v2.NewSetEvent( ctx, aggregate, feature_v2.InstanceUserSchemaEventType, false, @@ -211,10 +159,6 @@ func TestQueries_GetInstanceFeatures(t *testing.T) { ctx, aggregate, feature_v2.InstanceResetEventType, )), - eventFromEventPusher(feature_v2.NewSetEvent( - ctx, aggregate, - feature_v2.InstanceTriggerIntrospectionProjectionsEventType, true, - )), ), ), args: args{false}, @@ -226,14 +170,6 @@ func TestQueries_GetInstanceFeatures(t *testing.T) { Level: feature.LevelUnspecified, Value: false, }, - TriggerIntrospectionProjections: FeatureSource[bool]{ - Level: feature.LevelInstance, - Value: true, - }, - LegacyIntrospection: FeatureSource[bool]{ - Level: feature.LevelUnspecified, - Value: false, - }, UserSchema: FeatureSource[bool]{ Level: feature.LevelUnspecified, Value: false, diff --git a/internal/query/instance_test.go b/internal/query/instance_test.go index 55b1c8314b..37adfc8605 100644 --- a/internal/query/instance_test.go +++ b/internal/query/instance_test.go @@ -70,7 +70,7 @@ func Test_InstancePrepares(t *testing.T) { { name: "prepareInstancesQuery no result", prepare: func() (sq.SelectBuilder, func(*sql.Rows) (*Instances, error)) { - filter, query, scan := prepareInstancesQuery() + filter, query, scan := prepareInstancesQuery(Column{}, true) return query(filter), scan }, want: want{ @@ -85,7 +85,7 @@ func Test_InstancePrepares(t *testing.T) { { name: "prepareInstancesQuery one result", prepare: func() (sq.SelectBuilder, func(*sql.Rows) (*Instances, error)) { - filter, query, scan := prepareInstancesQuery() + filter, query, scan := prepareInstancesQuery(Column{}, true) return query(filter), scan }, want: want{ @@ -149,7 +149,7 @@ func Test_InstancePrepares(t *testing.T) { { name: "prepareInstancesQuery multiple results", prepare: func() (sq.SelectBuilder, func(*sql.Rows) (*Instances, error)) { - filter, query, scan := prepareInstancesQuery() + filter, query, scan := prepareInstancesQuery(Column{}, true) return query(filter), scan }, want: want{ @@ -253,7 +253,8 @@ func Test_InstancePrepares(t *testing.T) { IsPrimary: true, }, }, - }, { + }, + { ID: "id2", CreationDate: testNow, ChangeDate: testNow, @@ -282,7 +283,7 @@ func Test_InstancePrepares(t *testing.T) { { name: "prepareInstancesQuery sql err", prepare: func() (sq.SelectBuilder, func(*sql.Rows) (*Instances, error)) { - filter, query, scan := prepareInstancesQuery() + filter, query, scan := prepareInstancesQuery(Column{}, true) return query(filter), scan }, want: want{ diff --git a/internal/query/introspection.go b/internal/query/introspection.go index ee96bf576b..a3ef125466 100644 --- a/internal/query/introspection.go +++ b/internal/query/introspection.go @@ -25,12 +25,6 @@ var introspectionTriggerHandlers = sync.OnceValue(func() []*handler.Handler { ) }) -// TriggerIntrospectionProjections triggers all projections -// relevant to introspection queries concurrently. -func TriggerIntrospectionProjections(ctx context.Context) { - triggerBatch(ctx, introspectionTriggerHandlers()...) -} - type AppType string const ( diff --git a/internal/query/key.go b/internal/query/key.go index 4831d88654..e7b81bb951 100644 --- a/internal/query/key.go +++ b/internal/query/key.go @@ -1,20 +1,10 @@ package query import ( - "context" - "crypto/rsa" - "database/sql" "time" - sq "github.com/Masterminds/squirrel" - - "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/crypto" - "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/query/projection" - "github.com/zitadel/zitadel/internal/repository/keypair" - "github.com/zitadel/zitadel/internal/telemetry/tracing" - "github.com/zitadel/zitadel/internal/zerrors" ) type Key interface { @@ -36,11 +26,6 @@ type PublicKey interface { Key() interface{} } -type PrivateKeys struct { - SearchResponse - Keys []PrivateKey -} - type PublicKeys struct { SearchResponse Keys []PublicKey @@ -72,34 +57,6 @@ func (k *key) Sequence() uint64 { return k.sequence } -type privateKey struct { - key - expiry time.Time - privateKey *crypto.CryptoValue -} - -func (k *privateKey) Expiry() time.Time { - return k.expiry -} - -func (k *privateKey) Key() *crypto.CryptoValue { - return k.privateKey -} - -type rsaPublicKey struct { - key - expiry time.Time - publicKey *rsa.PublicKey -} - -func (r *rsaPublicKey) Expiry() time.Time { - return r.expiry -} - -func (r *rsaPublicKey) Key() interface{} { - return r.publicKey -} - var ( keyTable = table{ name: projection.KeyProjectionTable, @@ -157,277 +114,3 @@ var ( table: keyPrivateTable, } ) - -var ( - keyPublicTable = table{ - name: projection.KeyPublicTable, - instanceIDCol: projection.KeyPrivateColumnInstanceID, - } - KeyPublicColID = Column{ - name: projection.KeyPublicColumnID, - table: keyPublicTable, - } - KeyPublicColExpiry = Column{ - name: projection.KeyPublicColumnExpiry, - table: keyPublicTable, - } - KeyPublicColKey = Column{ - name: projection.KeyPublicColumnKey, - table: keyPublicTable, - } -) - -func (q *Queries) ActivePublicKeys(ctx context.Context, t time.Time) (keys *PublicKeys, err error) { - ctx, span := tracing.NewSpan(ctx) - defer func() { span.EndWithError(err) }() - - query, scan := preparePublicKeysQuery() - if t.IsZero() { - t = time.Now() - } - stmt, args, err := query.Where( - sq.And{ - sq.Eq{KeyColInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()}, - sq.Gt{KeyPublicColExpiry.identifier(): t}, - }).ToSql() - if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-SDFfg", "Errors.Query.SQLStatement") - } - - err = q.client.QueryContext(ctx, func(rows *sql.Rows) error { - keys, err = scan(rows) - return err - }, stmt, args...) - if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-Sghn4", "Errors.Internal") - } - - keys.State, err = q.latestState(ctx, keyTable) - if !zerrors.IsNotFound(err) { - return keys, err - } - return keys, nil -} - -func (q *Queries) ActivePrivateSigningKey(ctx context.Context, t time.Time) (keys *PrivateKeys, err error) { - ctx, span := tracing.NewSpan(ctx) - defer func() { span.EndWithError(err) }() - - stmt, scan := preparePrivateKeysQuery() - if t.IsZero() { - t = time.Now() - } - query, args, err := stmt.Where( - sq.And{ - sq.Eq{ - KeyColUse.identifier(): crypto.KeyUsageSigning, - KeyColInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), - }, - sq.Gt{KeyPrivateColExpiry.identifier(): t}, - }).OrderBy(KeyPrivateColExpiry.identifier()).ToSql() - if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-SDff2", "Errors.Query.SQLStatement") - } - - err = q.client.QueryContext(ctx, func(rows *sql.Rows) error { - keys, err = scan(rows) - return err - }, query, args...) - if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-WRFG4", "Errors.Internal") - } - keys.State, err = q.latestState(ctx, keyTable) - if !zerrors.IsNotFound(err) { - return keys, err - } - return keys, nil -} - -func preparePublicKeysQuery() (sq.SelectBuilder, func(*sql.Rows) (*PublicKeys, error)) { - return sq.Select( - KeyColID.identifier(), - KeyColCreationDate.identifier(), - KeyColChangeDate.identifier(), - KeyColSequence.identifier(), - KeyColResourceOwner.identifier(), - KeyColAlgorithm.identifier(), - KeyColUse.identifier(), - KeyPublicColExpiry.identifier(), - KeyPublicColKey.identifier(), - countColumn.identifier(), - ).From(keyTable.identifier()). - LeftJoin(join(KeyPublicColID, KeyColID)). - PlaceholderFormat(sq.Dollar), - func(rows *sql.Rows) (*PublicKeys, error) { - keys := make([]PublicKey, 0) - var count uint64 - for rows.Next() { - k := new(rsaPublicKey) - var keyValue []byte - err := rows.Scan( - &k.id, - &k.creationDate, - &k.changeDate, - &k.sequence, - &k.resourceOwner, - &k.algorithm, - &k.use, - &k.expiry, - &keyValue, - &count, - ) - if err != nil { - return nil, err - } - k.publicKey, err = crypto.BytesToPublicKey(keyValue) - if err != nil { - return nil, err - } - keys = append(keys, k) - } - - if err := rows.Close(); err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-rKd6k", "Errors.Query.CloseRows") - } - - return &PublicKeys{ - Keys: keys, - SearchResponse: SearchResponse{ - Count: count, - }, - }, nil - } -} - -func preparePrivateKeysQuery() (sq.SelectBuilder, func(*sql.Rows) (*PrivateKeys, error)) { - return sq.Select( - KeyColID.identifier(), - KeyColCreationDate.identifier(), - KeyColChangeDate.identifier(), - KeyColSequence.identifier(), - KeyColResourceOwner.identifier(), - KeyColAlgorithm.identifier(), - KeyColUse.identifier(), - KeyPrivateColExpiry.identifier(), - KeyPrivateColKey.identifier(), - countColumn.identifier(), - ).From(keyTable.identifier()). - LeftJoin(join(KeyPrivateColID, KeyColID)). - PlaceholderFormat(sq.Dollar), - func(rows *sql.Rows) (*PrivateKeys, error) { - keys := make([]PrivateKey, 0) - var count uint64 - for rows.Next() { - k := new(privateKey) - err := rows.Scan( - &k.id, - &k.creationDate, - &k.changeDate, - &k.sequence, - &k.resourceOwner, - &k.algorithm, - &k.use, - &k.expiry, - &k.privateKey, - &count, - ) - if err != nil { - return nil, err - } - keys = append(keys, k) - } - - if err := rows.Close(); err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-rKd6k", "Errors.Query.CloseRows") - } - - return &PrivateKeys{ - Keys: keys, - SearchResponse: SearchResponse{ - Count: count, - }, - }, nil - } -} - -type PublicKeyReadModel struct { - eventstore.ReadModel - - Algorithm string - Key *crypto.CryptoValue - Expiry time.Time - Usage crypto.KeyUsage -} - -func NewPublicKeyReadModel(keyID, resourceOwner string) *PublicKeyReadModel { - return &PublicKeyReadModel{ - ReadModel: eventstore.ReadModel{ - AggregateID: keyID, - ResourceOwner: resourceOwner, - }, - } -} - -func (wm *PublicKeyReadModel) AppendEvents(events ...eventstore.Event) { - wm.ReadModel.AppendEvents(events...) -} - -func (wm *PublicKeyReadModel) Reduce() error { - for _, event := range wm.Events { - switch e := event.(type) { - case *keypair.AddedEvent: - wm.Algorithm = e.Algorithm - wm.Key = e.PublicKey.Key - wm.Expiry = e.PublicKey.Expiry - wm.Usage = e.Usage - default: - } - } - return wm.ReadModel.Reduce() -} - -func (wm *PublicKeyReadModel) Query() *eventstore.SearchQueryBuilder { - return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). - AwaitOpenTransactions(). - ResourceOwner(wm.ResourceOwner). - AddQuery(). - AggregateTypes(keypair.AggregateType). - AggregateIDs(wm.AggregateID). - EventTypes(keypair.AddedEventType). - Builder() -} - -func (q *Queries) GetPublicKeyByID(ctx context.Context, keyID string) (_ PublicKey, err error) { - ctx, span := tracing.NewSpan(ctx) - defer func() { span.EndWithError(err) }() - - model := NewPublicKeyReadModel(keyID, authz.GetInstance(ctx).InstanceID()) - if err := q.eventstore.FilterToQueryReducer(ctx, model); err != nil { - return nil, err - } - if model.Algorithm == "" || model.Key == nil { - return nil, zerrors.ThrowNotFound(err, "QUERY-Ahf7x", "Errors.Key.NotFound") - } - keyValue, err := crypto.Decrypt(model.Key, q.keyEncryptionAlgorithm) - if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-Ie4oh", "Errors.Internal") - } - publicKey, err := crypto.BytesToPublicKey(keyValue) - if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-Kai2Z", "Errors.Internal") - } - - return &rsaPublicKey{ - key: key{ - id: model.AggregateID, - creationDate: model.CreationDate, - changeDate: model.ChangeDate, - sequence: model.ProcessedSequence, - resourceOwner: model.ResourceOwner, - algorithm: model.Algorithm, - use: model.Usage, - }, - expiry: model.Expiry, - publicKey: publicKey, - }, nil -} diff --git a/internal/query/key_test.go b/internal/query/key_test.go deleted file mode 100644 index 7bc029fd7f..0000000000 --- a/internal/query/key_test.go +++ /dev/null @@ -1,453 +0,0 @@ -package query - -import ( - "context" - "crypto/rsa" - "database/sql" - "database/sql/driver" - "errors" - "fmt" - "io" - "math/big" - "regexp" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "go.uber.org/mock/gomock" - - "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/crypto" - "github.com/zitadel/zitadel/internal/eventstore" - key_repo "github.com/zitadel/zitadel/internal/repository/keypair" - "github.com/zitadel/zitadel/internal/zerrors" -) - -var ( - preparePublicKeysStmt = `SELECT projections.keys4.id,` + - ` projections.keys4.creation_date,` + - ` projections.keys4.change_date,` + - ` projections.keys4.sequence,` + - ` projections.keys4.resource_owner,` + - ` projections.keys4.algorithm,` + - ` projections.keys4.use,` + - ` projections.keys4_public.expiry,` + - ` projections.keys4_public.key,` + - ` COUNT(*) OVER ()` + - ` FROM projections.keys4` + - ` LEFT JOIN projections.keys4_public ON projections.keys4.id = projections.keys4_public.id AND projections.keys4.instance_id = projections.keys4_public.instance_id` - preparePublicKeysCols = []string{ - "id", - "creation_date", - "change_date", - "sequence", - "resource_owner", - "algorithm", - "use", - "expiry", - "key", - "count", - } - - preparePrivateKeysStmt = `SELECT projections.keys4.id,` + - ` projections.keys4.creation_date,` + - ` projections.keys4.change_date,` + - ` projections.keys4.sequence,` + - ` projections.keys4.resource_owner,` + - ` projections.keys4.algorithm,` + - ` projections.keys4.use,` + - ` projections.keys4_private.expiry,` + - ` projections.keys4_private.key,` + - ` COUNT(*) OVER ()` + - ` FROM projections.keys4` + - ` LEFT JOIN projections.keys4_private ON projections.keys4.id = projections.keys4_private.id AND projections.keys4.instance_id = projections.keys4_private.instance_id` -) - -func Test_KeyPrepares(t *testing.T) { - type want struct { - sqlExpectations sqlExpectation - err checkErr - } - tests := []struct { - name string - prepare interface{} - want want - object interface{} - }{ - { - name: "preparePublicKeysQuery no result", - prepare: preparePublicKeysQuery, - want: want{ - sqlExpectations: mockQueries( - regexp.QuoteMeta(preparePublicKeysStmt), - nil, - nil, - ), - err: func(err error) (error, bool) { - if !zerrors.IsNotFound(err) { - return fmt.Errorf("err should be zitadel.NotFoundError got: %w", err), false - } - return nil, true - }, - }, - object: &PublicKeys{Keys: []PublicKey{}}, - }, - { - name: "preparePublicKeysQuery found", - prepare: preparePublicKeysQuery, - want: want{ - sqlExpectations: mockQueries( - regexp.QuoteMeta(preparePublicKeysStmt), - preparePublicKeysCols, - [][]driver.Value{ - { - "key-id", - testNow, - testNow, - uint64(20211109), - "ro", - "RS256", - 0, - testNow, - []byte("-----BEGIN RSA PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsvX9P58JFxEs5C+L+H7W\nduFSWL5EPzber7C2m94klrSV6q0bAcrYQnGwFOlveThsY200hRbadKaKjHD7qIKH\nDEe0IY2PSRht33Jye52AwhkRw+M3xuQH/7R8LydnsNFk2KHpr5X2SBv42e37LjkE\nslKSaMRgJW+v0KZ30piY8QsdFRKKaVg5/Ajt1YToM1YVsdHXJ3vmXFMtypLdxwUD\ndIaLEX6pFUkU75KSuEQ/E2luT61Q3ta9kOWm9+0zvi7OMcbdekJT7mzcVnh93R1c\n13ZhQCLbh9A7si8jKFtaMWevjayrvqQABEcTN9N4Hoxcyg6l4neZtRDk75OMYcqm\nDQIDAQAB\n-----END RSA PUBLIC KEY-----\n"), - }, - }, - ), - }, - object: &PublicKeys{ - SearchResponse: SearchResponse{ - Count: 1, - }, - Keys: []PublicKey{ - &rsaPublicKey{ - key: key{ - id: "key-id", - creationDate: testNow, - changeDate: testNow, - sequence: 20211109, - resourceOwner: "ro", - algorithm: "RS256", - use: crypto.KeyUsageSigning, - }, - expiry: testNow, - publicKey: &rsa.PublicKey{ - E: 65537, - N: fromBase16("b2f5fd3f9f0917112ce42f8bf87ed676e15258be443f36deafb0b69bde2496b495eaad1b01cad84271b014e96f79386c636d348516da74a68a8c70fba882870c47b4218d8f49186ddf72727b9d80c21911c3e337c6e407ffb47c2f2767b0d164d8a1e9af95f6481bf8d9edfb2e3904b2529268c460256fafd0a677d29898f10b1d15128a695839fc08edd584e8335615b1d1d7277be65c532dca92ddc7050374868b117ea9154914ef9292b8443f13696e4fad50ded6bd90e5a6f7ed33be2ece31c6dd7a4253ee6cdc56787ddd1d5cd776614022db87d03bb22f23285b5a3167af8dacabbea40004471337d3781e8c5cca0ea5e27799b510e4ef938c61caa60d"), - }, - }, - }, - }, - }, - { - name: "preparePublicKeysQuery sql err", - prepare: preparePublicKeysQuery, - want: want{ - sqlExpectations: mockQueryErr( - regexp.QuoteMeta(preparePublicKeysStmt), - sql.ErrConnDone, - ), - err: func(err error) (error, bool) { - if !errors.Is(err, sql.ErrConnDone) { - return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false - } - return nil, true - }, - }, - object: (*PublicKeys)(nil), - }, - { - name: "preparePrivateKeysQuery no result", - prepare: preparePrivateKeysQuery, - want: want{ - sqlExpectations: mockQueries( - regexp.QuoteMeta(preparePrivateKeysStmt), - nil, - nil, - ), - err: func(err error) (error, bool) { - if !zerrors.IsNotFound(err) { - return fmt.Errorf("err should be zitadel.NotFoundError got: %w", err), false - } - return nil, true - }, - }, - object: &PrivateKeys{Keys: []PrivateKey{}}, - }, - { - name: "preparePrivateKeysQuery found", - prepare: preparePrivateKeysQuery, - want: want{ - sqlExpectations: mockQueries( - regexp.QuoteMeta(preparePrivateKeysStmt), - preparePublicKeysCols, - [][]driver.Value{ - { - "key-id", - testNow, - testNow, - uint64(20211109), - "ro", - "RS256", - 0, - testNow, - []byte(`{"Algorithm": "enc", "Crypted": "cHJpdmF0ZUtleQ==", "CryptoType": 0, "KeyID": "id"}`), - }, - }, - ), - }, - object: &PrivateKeys{ - SearchResponse: SearchResponse{ - Count: 1, - }, - Keys: []PrivateKey{ - &privateKey{ - key: key{ - id: "key-id", - creationDate: testNow, - changeDate: testNow, - sequence: 20211109, - resourceOwner: "ro", - algorithm: "RS256", - use: crypto.KeyUsageSigning, - }, - expiry: testNow, - privateKey: &crypto.CryptoValue{ - CryptoType: crypto.TypeEncryption, - Algorithm: "enc", - KeyID: "id", - Crypted: []byte("privateKey"), - }, - }, - }, - }, - }, - { - name: "preparePrivateKeysQuery sql err", - prepare: preparePrivateKeysQuery, - want: want{ - sqlExpectations: mockQueryErr( - regexp.QuoteMeta(preparePrivateKeysStmt), - sql.ErrConnDone, - ), - err: func(err error) (error, bool) { - if !errors.Is(err, sql.ErrConnDone) { - return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false - } - return nil, true - }, - }, - object: (*PrivateKeys)(nil), - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - assertPrepare(t, tt.prepare, tt.object, tt.want.sqlExpectations, tt.want.err) - }) - } -} - -func fromBase16(base16 string) *big.Int { - i, ok := new(big.Int).SetString(base16, 16) - if !ok { - panic("bad number: " + base16) - } - return i -} - -const pubKey = `-----BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs38btwb3c7r0tMaQpGvB -mY+mPwMU/LpfuPoC0k2t4RsKp0fv40SMl50CRrHgk395wch8PMPYbl3+8TtYAJuy -rFALIj3Ff1UcKIk0hOH5DDsfh7/q2wFuncTmS6bifYo8CfSq2vDGnM7nZnEvxY/M -fSydZdcmIqlkUpfQmtzExw9+tSe5Dxq6gn5JtlGgLgZGt69r5iMMrTEGhhVAXzNu -MZbmlCoBru+rC8ITlTX/0V1ZcsSbL8tYWhthyu9x6yjo1bH85wiVI4gs0MhU8f2a -+kjL/KGZbR14Ua2eo6tonBZLC5DHWM2TkYXgRCDPufjcgmzN0Lm91E4P8KvBcvly -6QIDAQAB ------END PUBLIC KEY----- -` - -func TestQueries_GetPublicKeyByID(t *testing.T) { - now := time.Now() - future := now.Add(time.Hour) - - tests := []struct { - name string - eventstore func(*testing.T) *eventstore.Eventstore - encryption func(*testing.T) *crypto.MockEncryptionAlgorithm - want *rsaPublicKey - wantErr error - }{ - { - name: "filter error", - eventstore: expectEventstore( - expectFilterError(io.ErrClosedPipe), - ), - wantErr: io.ErrClosedPipe, - }, - { - name: "not found error", - eventstore: expectEventstore( - expectFilter(), - ), - wantErr: zerrors.ThrowNotFound(nil, "QUERY-Ahf7x", "Errors.Key.NotFound"), - }, - { - name: "decrypt error", - eventstore: expectEventstore( - expectFilter( - eventFromEventPusher(key_repo.NewAddedEvent(context.Background(), - &eventstore.Aggregate{ - ID: "keyID", - Type: key_repo.AggregateType, - ResourceOwner: "instanceID", - InstanceID: "instanceID", - Version: key_repo.AggregateVersion, - }, - crypto.KeyUsageSigning, "alg", - &crypto.CryptoValue{ - CryptoType: crypto.TypeEncryption, - Algorithm: "alg", - KeyID: "keyID", - Crypted: []byte("private"), - }, - &crypto.CryptoValue{ - CryptoType: crypto.TypeEncryption, - Algorithm: "alg", - KeyID: "keyID", - Crypted: []byte("public"), - }, - future, - future, - )), - ), - ), - encryption: func(t *testing.T) *crypto.MockEncryptionAlgorithm { - encryption := crypto.NewMockEncryptionAlgorithm(gomock.NewController(t)) - expect := encryption.EXPECT() - expect.Algorithm().Return("alg") - expect.DecryptionKeyIDs().Return([]string{}) - return encryption - }, - wantErr: zerrors.ThrowInternal(nil, "QUERY-Ie4oh", "Errors.Internal"), - }, - { - name: "parse error", - eventstore: expectEventstore( - expectFilter( - eventFromEventPusher(key_repo.NewAddedEvent(context.Background(), - &eventstore.Aggregate{ - ID: "keyID", - Type: key_repo.AggregateType, - ResourceOwner: "instanceID", - InstanceID: "instanceID", - Version: key_repo.AggregateVersion, - }, - crypto.KeyUsageSigning, "alg", - &crypto.CryptoValue{ - CryptoType: crypto.TypeEncryption, - Algorithm: "alg", - KeyID: "keyID", - Crypted: []byte("private"), - }, - &crypto.CryptoValue{ - CryptoType: crypto.TypeEncryption, - Algorithm: "alg", - KeyID: "keyID", - Crypted: []byte("public"), - }, - future, - future, - )), - ), - ), - encryption: func(t *testing.T) *crypto.MockEncryptionAlgorithm { - encryption := crypto.NewMockEncryptionAlgorithm(gomock.NewController(t)) - expect := encryption.EXPECT() - expect.Algorithm().Return("alg") - expect.DecryptionKeyIDs().Return([]string{"keyID"}) - expect.Decrypt([]byte("public"), "keyID").Return([]byte("foo"), nil) - return encryption - }, - wantErr: zerrors.ThrowInternal(nil, "QUERY-Kai2Z", "Errors.Internal"), - }, - { - name: "success", - eventstore: expectEventstore( - expectFilter( - eventFromEventPusher(key_repo.NewAddedEvent(context.Background(), - &eventstore.Aggregate{ - ID: "keyID", - Type: key_repo.AggregateType, - ResourceOwner: "instanceID", - InstanceID: "instanceID", - Version: key_repo.AggregateVersion, - }, - crypto.KeyUsageSigning, "alg", - &crypto.CryptoValue{ - CryptoType: crypto.TypeEncryption, - Algorithm: "alg", - KeyID: "keyID", - Crypted: []byte("private"), - }, - &crypto.CryptoValue{ - CryptoType: crypto.TypeEncryption, - Algorithm: "alg", - KeyID: "keyID", - Crypted: []byte("public"), - }, - future, - future, - )), - ), - ), - encryption: func(t *testing.T) *crypto.MockEncryptionAlgorithm { - encryption := crypto.NewMockEncryptionAlgorithm(gomock.NewController(t)) - expect := encryption.EXPECT() - expect.Algorithm().Return("alg") - expect.DecryptionKeyIDs().Return([]string{"keyID"}) - expect.Decrypt([]byte("public"), "keyID").Return([]byte(pubKey), nil) - return encryption - }, - want: &rsaPublicKey{ - key: key{ - id: "keyID", - resourceOwner: "instanceID", - algorithm: "alg", - use: crypto.KeyUsageSigning, - }, - expiry: future, - publicKey: func() *rsa.PublicKey { - publicKey, err := crypto.BytesToPublicKey([]byte(pubKey)) - if err != nil { - panic(err) - } - return publicKey - }(), - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - q := &Queries{ - eventstore: tt.eventstore(t), - } - if tt.encryption != nil { - q.keyEncryptionAlgorithm = tt.encryption(t) - } - ctx := authz.NewMockContext("instanceID", "orgID", "loginClient") - key, err := q.GetPublicKeyByID(ctx, "keyID") - if tt.wantErr != nil { - require.ErrorIs(t, err, tt.wantErr) - return - } - require.NoError(t, err) - require.NotNil(t, key) - - got := key.(*rsaPublicKey) - assert.WithinDuration(t, tt.want.expiry, got.expiry, time.Second) - tt.want.expiry = time.Time{} - got.expiry = time.Time{} - assert.Equal(t, tt.want, got) - }) - } -} diff --git a/internal/query/member.go b/internal/query/member.go index 584ae15d1c..8a4ca3302d 100644 --- a/internal/query/member.go +++ b/internal/query/member.go @@ -35,8 +35,17 @@ func NewMemberLastNameSearchQuery(method TextComparison, value string) (SearchQu } func NewMemberUserIDSearchQuery(value string) (SearchQuery, error) { - return NewTextQuery(membershipUserID, value, TextEquals) + return NewTextQuery(MembershipUserID, value, TextEquals) } + +func NewMemberInUserIDsSearchQuery(ids []string) (SearchQuery, error) { + list := make([]interface{}, len(ids)) + for i, value := range ids { + list[i] = value + } + return NewListQuery(MembershipUserID, list, ListIn) +} + func NewMemberResourceOwnerSearchQuery(value string) (SearchQuery, error) { return NewTextQuery(membershipResourceOwner, value, TextEquals) } diff --git a/internal/query/org.go b/internal/query/org.go index 58b0dad4a5..e2d9e205da 100644 --- a/internal/query/org.go +++ b/internal/query/org.go @@ -96,6 +96,18 @@ func orgsCheckPermission(ctx context.Context, orgs *Orgs, permissionCheck domain ) } +func orgsPermissionCheckV2(ctx context.Context, query sq.SelectBuilder, enabled bool) sq.SelectBuilder { + if !enabled { + return query + } + join, args := PermissionClause( + ctx, + OrgColumnID, + domain_pkg.PermissionOrgRead, + ) + return query.JoinClause(join, args...) +} + type OrgSearchQueries struct { SearchRequest Queries []SearchQuery @@ -287,21 +299,23 @@ func (q *Queries) ExistsOrg(ctx context.Context, id, domain string) (verifiedID } func (q *Queries) SearchOrgs(ctx context.Context, queries *OrgSearchQueries, permissionCheck domain_pkg.PermissionCheck) (*Orgs, error) { - orgs, err := q.searchOrgs(ctx, queries) + permissionCheckV2 := PermissionV2(ctx, permissionCheck) + orgs, err := q.searchOrgs(ctx, queries, permissionCheckV2) if err != nil { return nil, err } - if permissionCheck != nil { + if permissionCheck != nil && !permissionCheckV2 { orgsCheckPermission(ctx, orgs, permissionCheck) } return orgs, nil } -func (q *Queries) searchOrgs(ctx context.Context, queries *OrgSearchQueries) (orgs *Orgs, err error) { +func (q *Queries) searchOrgs(ctx context.Context, queries *OrgSearchQueries, permissionCheckV2 bool) (orgs *Orgs, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() query, scan := prepareOrgsQuery() + query = orgsPermissionCheckV2(ctx, query, permissionCheckV2) stmt, args, err := queries.toQuery(query). Where(sq.And{ sq.Eq{ diff --git a/internal/query/org_metadata.go b/internal/query/org_metadata.go index 7edbdcbda3..e67c7222cd 100644 --- a/internal/query/org_metadata.go +++ b/internal/query/org_metadata.go @@ -194,7 +194,6 @@ func prepareOrgMetadataQuery() (sq.SelectBuilder, func(*sql.Row) (*OrgMetadata, &m.Key, &m.Value, ) - if err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, zerrors.ThrowNotFound(err, "QUERY-Rph32", "Errors.Metadata.NotFound") diff --git a/internal/query/permission.go b/internal/query/permission.go index c52b491144..19e3ed984e 100644 --- a/internal/query/permission.go +++ b/internal/query/permission.go @@ -2,74 +2,159 @@ package query import ( "context" - "encoding/json" - "fmt" sq "github.com/Masterminds/squirrel" "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/database" + domain_pkg "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/zerrors" ) const ( - // eventstore.permitted_orgs(instanceid text, userid text, system_user_perms JSONB, perm text filter_orgs text) - wherePermittedOrgsClause = "%s = ANY(eventstore.permitted_orgs(?, ?, ?, ?, ?))" - wherePermittedOrgsOrCurrentUserClause = "(" + wherePermittedOrgsClause + " OR %s = ?" + ")" + // eventstore.permitted_orgs(req_instance_id text, auth_user_id text, system_user_perms JSONB, perm text, filter_org text) + joinPermittedOrgsFunction = `INNER JOIN eventstore.permitted_orgs(?, ?, ?, ?, ?) permissions ON ` + + // eventstore.permitted_projects(req_instance_id text, auth_user_id text, system_user_perms JSONB, perm text, filter_org text) + joinPermittedProjectsFunction = `INNER JOIN eventstore.permitted_projects(?, ?, ?, ?, ?) permissions ON ` ) -// wherePermittedOrgs sets a `WHERE` clause to the query that filters the orgs -// for which the authenticated user has the requested permission for. -// The user ID is taken from the context. -// The `orgIDColumn` specifies the table column to which this filter must be applied, -// and is typically the `resource_owner` column in ZITADEL. -// We use full identifiers in the query builder so this function should be -// called with something like `UserResourceOwnerCol.identifier()` for example. -// func wherePermittedOrgs(ctx context.Context, query sq.SelectBuilder, filterOrgIds, orgIDColumn, permission string) (sq.SelectBuilder, error) { -// userID := authz.GetCtxData(ctx).UserID -// logging.WithFields("permission_check_v2_flag", authz.GetFeatures(ctx).PermissionCheckV2, "org_id_column", orgIDColumn, "permission", permission, "user_id", userID).Debug("permitted orgs check used") +// permissionClauseBuilder is used to build the SQL clause for permission checks. +// Don't use it directly, use the [PermissionClause] function with proper options instead. +type permissionClauseBuilder struct { + orgIDColumn Column + instanceID string + userID string + systemPermissions []authz.SystemUserPermissions + permission string -// systemUserPermissions := authz.GetSystemUserPermissions(ctx) -// var systemUserPermissionsJson []byte -// if systemUserPermissions != nil { -// var err error -// systemUserPermissionsJson, err = json.Marshal(systemUserPermissions) -// if err != nil { -// return query, err -// } -// } + // optional fields + orgID *string + projectIDColumn *Column + connections []sq.Eq +} -// return query.Where( -// fmt.Sprintf(wherePermittedOrgsClause, orgIDColumn), -// authz.GetInstance(ctx).InstanceID(), -// userID, -// systemUserPermissionsJson, -// permission, -// filterOrgIds, -// ), nil -// } +func (b *permissionClauseBuilder) appendConnection(column string, value any) { + b.connections = append(b.connections, sq.Eq{column: value}) +} -func wherePermittedOrgsOrCurrentUser(ctx context.Context, query sq.SelectBuilder, filterOrgIds, orgIDColumn, userIdColum, permission string) (sq.SelectBuilder, error) { - userID := authz.GetCtxData(ctx).UserID - logging.WithFields("permission_check_v2_flag", authz.GetFeatures(ctx).PermissionCheckV2, "org_id_column", orgIDColumn, "user_id_colum", userIdColum, "permission", permission, "user_id", userID).Debug("permitted orgs check used") +// joinFunction picks the correct SQL function and return the required arguments for that function. +func (b *permissionClauseBuilder) joinFunction() (sql string, args []any) { + sql = joinPermittedOrgsFunction + if b.projectIDColumn != nil { + sql = joinPermittedProjectsFunction + } + return sql, []any{ + b.instanceID, + b.userID, + database.NewJSONArray(b.systemPermissions), + b.permission, + b.orgID, + } +} - systemUserPermissions := authz.GetSystemUserPermissions(ctx) - var systemUserPermissionsJson []byte - if systemUserPermissions != nil { - var err error - systemUserPermissionsJson, err = json.Marshal(systemUserPermissions) - if err != nil { - return query, zerrors.ThrowInternal(err, "AUTHZ-HS4us", "Errors.Internal") +// joinConditions returns the conditions for the join, +// which are dynamic based on the provided options. +func (b *permissionClauseBuilder) joinConditions() sq.Or { + conditions := make(sq.Or, 2, len(b.connections)+3) + conditions[0] = sq.Expr("permissions.instance_permitted") + conditions[1] = sq.Expr(b.orgIDColumn.identifier() + " = ANY(permissions.org_ids)") + if b.projectIDColumn != nil { + conditions = append(conditions, + sq.Expr(b.projectIDColumn.identifier()+" = ANY(permissions.project_ids)"), + ) + } + for _, c := range b.connections { + conditions = append(conditions, c) + } + return conditions +} + +type PermissionOption func(b *permissionClauseBuilder) + +// OwnedRowsPermissionOption allows rows to be returned of which the current user is the owner. +// Even if the user does not have an explicit permission for the organization. +// For example an authenticated user can always see his own user account. +// This option may be provided multiple times to allow matching with multiple columns. +// See [ConnectionPermissionOption] for more details. +func OwnedRowsPermissionOption(userIDColumn Column) PermissionOption { + return func(b *permissionClauseBuilder) { + b.appendConnection(userIDColumn.identifier(), b.userID) + } +} + +// ConnectionPermissionOption allows returning of rows where the value is matched. +// Even if the user does not have an explicit permission for the resource. +// Multiple connections may be provided. +// Each connection is applied in a OR condition, so if previous permissions are not met, +// matching rows are still returned for a later match. +func ConnectionPermissionOption(column Column, value any) PermissionOption { + return func(b *permissionClauseBuilder) { + b.appendConnection(column.identifier(), value) + } +} + +// SingleOrgPermissionOption may be used to optimize the permitted orgs function by limiting the +// returned organizations, to the one used in the requested filters. +func SingleOrgPermissionOption(queries []SearchQuery) PermissionOption { + return func(b *permissionClauseBuilder) { + orgID, ok := findTextEqualsQuery(b.orgIDColumn, queries) + if ok { + b.orgID = &orgID } } - - return query.Where( - fmt.Sprintf(wherePermittedOrgsOrCurrentUserClause, orgIDColumn, userIdColum), - authz.GetInstance(ctx).InstanceID(), - userID, - systemUserPermissionsJson, - permission, - filterOrgIds, - userID, - ), nil +} + +// WithProjectsPermissionOption sets an additional filter against the project ID column, +// allowing for project specific permissions. +func WithProjectsPermissionOption(projectIDColumn Column) PermissionOption { + return func(b *permissionClauseBuilder) { + b.projectIDColumn = &projectIDColumn + } +} + +// PermissionClause builds a `INNER JOIN` clause which can be applied to a query builder. +// It filters returned rows the current authenticated user has the requested permission to. +// See permission_example_test.go for examples. +// +// Experimental: Work in progress. Currently only organization and project permissions are supported +// TODO: Add support for project grants. +func PermissionClause(ctx context.Context, orgIDCol Column, permission string, options ...PermissionOption) (string, []any) { + ctxData := authz.GetCtxData(ctx) + b := &permissionClauseBuilder{ + orgIDColumn: orgIDCol, + instanceID: authz.GetInstance(ctx).InstanceID(), + userID: ctxData.UserID, + systemPermissions: ctxData.SystemUserPermissions, + permission: permission, + } + for _, opt := range options { + opt(b) + } + logging.WithFields( + "org_id_column", b.orgIDColumn, + "instance_id", b.instanceID, + "user_id", b.userID, + "system_user_permissions", b.systemPermissions, + "permission", b.permission, + "org_id", b.orgID, + "project_id_column", b.projectIDColumn, + "connections", b.connections, + ).Debug("permitted orgs check used") + + sql, args := b.joinFunction() + conditions, conditionArgs, err := b.joinConditions().ToSql() + if err != nil { + // all cases are tested, no need to return an error. + // If an error does happen, it's a bug and not a user error. + panic(zerrors.ThrowInternal(err, "PERMISSION-OoS5o", "Errors.Internal")) + } + return sql + conditions, append(args, conditionArgs...) +} + +// PermissionV2 checks are enabled when the feature flag is set and the permission check function is not nil. +// When the permission check function is nil, it indicates a v1 API and no resource based permission check is needed. +func PermissionV2(ctx context.Context, cf domain_pkg.PermissionCheck) bool { + return authz.GetFeatures(ctx).PermissionCheckV2 && cf != nil } diff --git a/internal/query/permission_example_test.go b/internal/query/permission_example_test.go new file mode 100644 index 0000000000..6211ad0bb2 --- /dev/null +++ b/internal/query/permission_example_test.go @@ -0,0 +1,78 @@ +package query + +import ( + "context" + "fmt" + + sq "github.com/Masterminds/squirrel" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/domain" +) + +// ExamplePermissionClause_org shows how to use the PermissionClause function to filter +// permitted records based on the resource owner and the user's instance or organization membership. +func ExamplePermissionClause_org() { + // These variables are typically set in the middleware of Zitadel. + // They do not influence the generation of the clause, just what + // the function does in Postgres. + ctx := authz.WithInstanceID(context.Background(), "instanceID") + ctx = authz.SetCtxData(ctx, authz.CtxData{ + UserID: "userID", + }) + + join, args := PermissionClause( + ctx, + UserResourceOwnerCol, // match the resource owner column + domain.PermissionUserRead, + SingleOrgPermissionOption([]SearchQuery{ + mustSearchQuery(NewUserDisplayNameSearchQuery("zitadel", TextContains)), + mustSearchQuery(NewUserResourceOwnerSearchQuery("orgID", TextEquals)), + }), // If the request had an orgID filter, it can be used to optimize the SQL function. + OwnedRowsPermissionOption(UserIDCol), // allow user to find themselves. + ) + + sql, _, _ := sq.Select("*"). + From(userTable.identifier()). + JoinClause(join, args...). + Where(sq.Eq{ + UserInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(), + }).ToSql() + fmt.Println(sql) + // Output: + // SELECT * FROM projections.users14 INNER JOIN eventstore.permitted_orgs(?, ?, ?, ?, ?) permissions ON (permissions.instance_permitted OR projections.users14.resource_owner = ANY(permissions.org_ids) OR projections.users14.id = ?) WHERE projections.users14.instance_id = ? +} + +// ExamplePermissionClause_project shows how to use the PermissionClause function to filter +// permitted records based on the resource owner and the user's instance or organization membership. +// Additionally, it allows returning records based on the project ID and project membership. +func ExamplePermissionClause_project() { + // These variables are typically set in the middleware of Zitadel. + // They do not influence the generation of the clause, just what + // the function does in Postgres. + ctx := authz.WithInstanceID(context.Background(), "instanceID") + ctx = authz.SetCtxData(ctx, authz.CtxData{ + UserID: "userID", + }) + + join, args := PermissionClause( + ctx, + ProjectColumnResourceOwner, // match the resource owner column + "project.read", + WithProjectsPermissionOption(ProjectColumnID), + SingleOrgPermissionOption([]SearchQuery{ + mustSearchQuery(NewUserDisplayNameSearchQuery("zitadel", TextContains)), + mustSearchQuery(NewUserResourceOwnerSearchQuery("orgID", TextEquals)), + }), // If the request had an orgID filter, it can be used to optimize the SQL function. + ) + + sql, _, _ := sq.Select("*"). + From(projectsTable.identifier()). + JoinClause(join, args...). + Where(sq.Eq{ + ProjectColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), + }).ToSql() + fmt.Println(sql) + // Output: + // SELECT * FROM projections.projects4 INNER JOIN eventstore.permitted_projects(?, ?, ?, ?, ?) permissions ON (permissions.instance_permitted OR projections.projects4.resource_owner = ANY(permissions.org_ids) OR projections.projects4.id = ANY(permissions.project_ids)) WHERE projections.projects4.instance_id = ? +} diff --git a/internal/query/permission_test.go b/internal/query/permission_test.go new file mode 100644 index 0000000000..24692a9406 --- /dev/null +++ b/internal/query/permission_test.go @@ -0,0 +1,243 @@ +package query + +import ( + "context" + "testing" + + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/database" + domain_pkg "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/feature" +) + +func TestPermissionClause(t *testing.T) { + var permissions = []authz.SystemUserPermissions{ + { + MemberType: authz.MemberTypeOrganization, + AggregateID: "orgID", + Permissions: []string{"permission1", "permission2"}, + }, + { + MemberType: authz.MemberTypeIAM, + Permissions: []string{"permission2", "permission3"}, + }, + } + ctx := authz.WithInstanceID(context.Background(), "instanceID") + ctx = authz.SetCtxData(ctx, authz.CtxData{ + UserID: "userID", + SystemUserPermissions: permissions, + }) + + type args struct { + ctx context.Context + orgIDCol Column + permission string + options []PermissionOption + } + tests := []struct { + name string + args args + wantSql string + wantArgs []any + }{ + { + name: "org, no options", + args: args{ + ctx: ctx, + orgIDCol: UserResourceOwnerCol, + permission: "permission1", + }, + wantSql: "INNER JOIN eventstore.permitted_orgs(?, ?, ?, ?, ?) permissions ON (permissions.instance_permitted OR projections.users14.resource_owner = ANY(permissions.org_ids))", + wantArgs: []any{ + "instanceID", + "userID", + database.NewJSONArray(permissions), + "permission1", + (*string)(nil), + }, + }, + { + name: "org, owned rows option", + args: args{ + ctx: ctx, + orgIDCol: UserResourceOwnerCol, + permission: "permission1", + options: []PermissionOption{ + OwnedRowsPermissionOption(UserIDCol), + }, + }, + wantSql: "INNER JOIN eventstore.permitted_orgs(?, ?, ?, ?, ?) permissions ON (permissions.instance_permitted OR projections.users14.resource_owner = ANY(permissions.org_ids) OR projections.users14.id = ?)", + wantArgs: []any{ + "instanceID", + "userID", + database.NewJSONArray(permissions), + "permission1", + (*string)(nil), + "userID", + }, + }, + { + name: "org, connection rows option", + args: args{ + ctx: ctx, + orgIDCol: UserResourceOwnerCol, + permission: "permission1", + options: []PermissionOption{ + OwnedRowsPermissionOption(UserIDCol), + ConnectionPermissionOption(UserStateCol, "bar"), + }, + }, + wantSql: "INNER JOIN eventstore.permitted_orgs(?, ?, ?, ?, ?) permissions ON (permissions.instance_permitted OR projections.users14.resource_owner = ANY(permissions.org_ids) OR projections.users14.id = ? OR projections.users14.state = ?)", + wantArgs: []any{ + "instanceID", + "userID", + database.NewJSONArray(permissions), + "permission1", + (*string)(nil), + "userID", + "bar", + }, + }, + { + name: "org, with ID", + args: args{ + ctx: ctx, + orgIDCol: UserResourceOwnerCol, + permission: "permission1", + options: []PermissionOption{ + SingleOrgPermissionOption([]SearchQuery{ + mustSearchQuery(NewUserDisplayNameSearchQuery("zitadel", TextContains)), + mustSearchQuery(NewUserResourceOwnerSearchQuery("orgID", TextEquals)), + }), + }, + }, + wantSql: "INNER JOIN eventstore.permitted_orgs(?, ?, ?, ?, ?) permissions ON (permissions.instance_permitted OR projections.users14.resource_owner = ANY(permissions.org_ids))", + wantArgs: []any{ + "instanceID", + "userID", + database.NewJSONArray(permissions), + "permission1", + gu.Ptr("orgID"), + }, + }, + { + name: "project", + args: args{ + ctx: ctx, + orgIDCol: ProjectColumnResourceOwner, + permission: "permission1", + options: []PermissionOption{ + WithProjectsPermissionOption(ProjectColumnID), + }, + }, + wantSql: "INNER JOIN eventstore.permitted_projects(?, ?, ?, ?, ?) permissions ON (permissions.instance_permitted OR projections.projects4.resource_owner = ANY(permissions.org_ids) OR projections.projects4.id = ANY(permissions.project_ids))", + wantArgs: []any{ + "instanceID", + "userID", + database.NewJSONArray(permissions), + "permission1", + (*string)(nil), + }, + }, + { + name: "project, single org", + args: args{ + ctx: ctx, + orgIDCol: ProjectColumnResourceOwner, + permission: "permission1", + options: []PermissionOption{ + WithProjectsPermissionOption(ProjectColumnID), + SingleOrgPermissionOption([]SearchQuery{ + mustSearchQuery(NewProjectResourceOwnerSearchQuery("orgID")), + }), + }, + }, + wantSql: "INNER JOIN eventstore.permitted_projects(?, ?, ?, ?, ?) permissions ON (permissions.instance_permitted OR projections.projects4.resource_owner = ANY(permissions.org_ids) OR projections.projects4.id = ANY(permissions.project_ids))", + wantArgs: []any{ + "instanceID", + "userID", + database.NewJSONArray(permissions), + "permission1", + gu.Ptr("orgID"), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotSql, gotArgs := PermissionClause(tt.args.ctx, tt.args.orgIDCol, tt.args.permission, tt.args.options...) + assert.Equal(t, tt.wantSql, gotSql) + assert.Equal(t, tt.wantArgs, gotArgs) + }) + } +} + +func mustSearchQuery(q SearchQuery, err error) SearchQuery { + if err != nil { + panic(err) + } + return q +} + +func TestPermissionV2(t *testing.T) { + type args struct { + ctx context.Context + cf domain_pkg.PermissionCheck + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "feature disabled, no permission check", + args: args{ + ctx: context.Background(), + cf: nil, + }, + want: false, + }, + { + name: "feature enabled, no permission check", + args: args{ + ctx: authz.WithFeatures(context.Background(), feature.Features{ + PermissionCheckV2: true, + }), + cf: nil, + }, + want: false, + }, + { + name: "feature enabled, with permission check", + args: args{ + ctx: authz.WithFeatures(context.Background(), feature.Features{ + PermissionCheckV2: true, + }), + cf: func(context.Context, string, string, string) error { + return nil + }, + }, + want: true, + }, + { + name: "feature disabled, with permission check", + args: args{ + ctx: authz.WithFeatures(context.Background(), feature.Features{ + PermissionCheckV2: false, + }), + cf: func(context.Context, string, string, string) error { + return nil + }, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := PermissionV2(tt.args.ctx, tt.args.cf) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/internal/query/project.go b/internal/query/project.go index f3f900af83..23c70b9ab8 100644 --- a/internal/query/project.go +++ b/internal/query/project.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "errors" + "slices" "time" sq "github.com/Masterminds/squirrel" @@ -72,11 +73,94 @@ var ( } ) +var ( + grantedProjectsAlias = table{ + name: "granted_projects", + instanceIDCol: projection.ProjectColumnInstanceID, + } + GrantedProjectColumnID = Column{ + name: projection.ProjectColumnID, + table: grantedProjectsAlias, + } + GrantedProjectColumnCreationDate = Column{ + name: projection.ProjectColumnCreationDate, + table: grantedProjectsAlias, + } + GrantedProjectColumnChangeDate = Column{ + name: projection.ProjectColumnChangeDate, + table: grantedProjectsAlias, + } + grantedProjectColumnResourceOwner = Column{ + name: projection.ProjectColumnResourceOwner, + table: grantedProjectsAlias, + } + grantedProjectColumnInstanceID = Column{ + name: projection.ProjectGrantColumnInstanceID, + table: grantedProjectsAlias, + } + grantedProjectColumnState = Column{ + name: "project_state", + table: grantedProjectsAlias, + } + GrantedProjectColumnName = Column{ + name: "project_name", + table: grantedProjectsAlias, + } + grantedProjectColumnProjectRoleAssertion = Column{ + name: projection.ProjectColumnProjectRoleAssertion, + table: grantedProjectsAlias, + } + grantedProjectColumnProjectRoleCheck = Column{ + name: projection.ProjectColumnProjectRoleCheck, + table: grantedProjectsAlias, + } + grantedProjectColumnHasProjectCheck = Column{ + name: projection.ProjectColumnHasProjectCheck, + table: grantedProjectsAlias, + } + grantedProjectColumnPrivateLabelingSetting = Column{ + name: projection.ProjectColumnPrivateLabelingSetting, + table: grantedProjectsAlias, + } + grantedProjectColumnGrantResourceOwner = Column{ + name: "project_grant_resource_owner", + table: grantedProjectsAlias, + } + grantedProjectColumnGrantID = Column{ + name: projection.ProjectGrantColumnGrantID, + table: grantedProjectsAlias, + } + grantedProjectColumnGrantedOrganization = Column{ + name: projection.ProjectGrantColumnGrantedOrgID, + table: grantedProjectsAlias, + } + grantedProjectColumnGrantedOrganizationName = Column{ + name: "granted_org_name", + table: grantedProjectsAlias, + } + grantedProjectColumnGrantState = Column{ + name: "project_grant_state", + table: grantedProjectsAlias, + } +) + type Projects struct { SearchResponse Projects []*Project } +func projectsCheckPermission(ctx context.Context, projects *Projects, permissionCheck domain.PermissionCheck) { + projects.Projects = slices.DeleteFunc(projects.Projects, + func(project *Project) bool { + return projectCheckPermission(ctx, project.ResourceOwner, project.ID, permissionCheck) != nil + }, + ) +} + +func projectCheckPermission(ctx context.Context, resourceOwner string, projectID string, permissionCheck domain.PermissionCheck) error { + return permissionCheck(ctx, domain.PermissionProjectRead, resourceOwner, projectID) +} + type Project struct { ID string CreationDate time.Time @@ -94,7 +178,19 @@ type Project struct { type ProjectSearchQueries struct { SearchRequest - Queries []SearchQuery + Queries []SearchQuery + GrantQueries []SearchQuery +} + +func (q *Queries) GetProjectByIDWithPermission(ctx context.Context, shouldTriggerBulk bool, id string, permissionCheck domain.PermissionCheck) (*Project, error) { + project, err := q.ProjectByID(ctx, shouldTriggerBulk, id) + if err != nil { + return nil, err + } + if err := projectCheckPermission(ctx, project.ResourceOwner, project.ID, permissionCheck); err != nil { + return nil, err + } + return project, nil } func (q *Queries) ProjectByID(ctx context.Context, shouldTriggerBulk bool, id string) (project *Project, err error) { @@ -125,7 +221,18 @@ func (q *Queries) ProjectByID(ctx context.Context, shouldTriggerBulk bool, id st return project, err } -func (q *Queries) SearchProjects(ctx context.Context, queries *ProjectSearchQueries) (projects *Projects, err error) { +func (q *Queries) SearchProjects(ctx context.Context, queries *ProjectSearchQueries, permissionCheck domain.PermissionCheck) (*Projects, error) { + projects, err := q.searchProjects(ctx, queries) + if err != nil { + return nil, err + } + if permissionCheck != nil { + projectsCheckPermission(ctx, projects, permissionCheck) + } + return projects, nil +} + +func (q *Queries) searchProjects(ctx context.Context, queries *ProjectSearchQueries) (projects *Projects, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -147,6 +254,104 @@ func (q *Queries) SearchProjects(ctx context.Context, queries *ProjectSearchQuer return projects, err } +type ProjectAndGrantedProjectSearchQueries struct { + SearchRequest + Queries []SearchQuery +} + +func (q *ProjectAndGrantedProjectSearchQueries) toQuery(query sq.SelectBuilder) sq.SelectBuilder { + query = q.SearchRequest.toQuery(query) + for _, q := range q.Queries { + query = q.toQuery(query) + } + return query +} + +func projectPermissionCheckV2(ctx context.Context, query sq.SelectBuilder, enabled bool, queries *ProjectAndGrantedProjectSearchQueries) sq.SelectBuilder { + if !enabled { + return query + } + join, args := PermissionClause( + ctx, + grantedProjectColumnResourceOwner, + domain.PermissionProjectRead, + SingleOrgPermissionOption(queries.Queries), + WithProjectsPermissionOption(GrantedProjectColumnID), + ) + return query.JoinClause(join, args...) +} + +func (q *Queries) SearchGrantedProjects(ctx context.Context, queries *ProjectAndGrantedProjectSearchQueries, permissionCheck domain.PermissionCheck) (*GrantedProjects, error) { + // removed as permission v2 is not implemented yet for project grant level permissions + // permissionCheckV2 := PermissionV2(ctx, permissionCheck) + projects, err := q.searchGrantedProjects(ctx, queries, false) + if err != nil { + return nil, err + } + if permissionCheck != nil { // && !authz.GetFeatures(ctx).PermissionCheckV2 { + grantedProjectsCheckPermission(ctx, projects, permissionCheck) + } + return projects, nil +} + +func (q *Queries) searchGrantedProjects(ctx context.Context, queries *ProjectAndGrantedProjectSearchQueries, permissionCheckV2 bool) (grantedProjects *GrantedProjects, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + query, scan := prepareGrantedProjectsQuery() + query = projectPermissionCheckV2(ctx, query, permissionCheckV2, queries) + eq := sq.Eq{grantedProjectColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()} + stmt, args, err := queries.toQuery(query).Where(eq).ToSql() + if err != nil { + return nil, zerrors.ThrowInvalidArgument(err, "QUERY-T84X9", "Errors.Query.InvalidRequest") + } + + err = q.client.QueryContext(ctx, func(rows *sql.Rows) error { + grantedProjects, err = scan(rows) + return err + }, stmt, args...) + if err != nil { + return nil, err + } + return grantedProjects, nil +} + +func NewGrantedProjectNameSearchQuery(method TextComparison, value string) (SearchQuery, error) { + return NewTextQuery(GrantedProjectColumnName, value, method) +} + +func NewGrantedProjectResourceOwnerSearchQuery(value string) (SearchQuery, error) { + return NewTextQuery(grantedProjectColumnResourceOwner, value, TextEquals) +} + +func NewGrantedProjectIDSearchQuery(ids []string) (SearchQuery, error) { + list := make([]interface{}, len(ids)) + for i, value := range ids { + list[i] = value + } + return NewListQuery(GrantedProjectColumnID, list, ListIn) +} + +func NewGrantedProjectOrganizationIDSearchQuery(value string) (SearchQuery, error) { + project, err := NewGrantedProjectResourceOwnerSearchQuery(value) + if err != nil { + return nil, err + } + grant, err := NewGrantedProjectGrantedOrganizationIDSearchQuery(value) + if err != nil { + return nil, err + } + return NewOrQuery(project, grant) +} + +func NewGrantedProjectGrantResourceOwnerSearchQuery(value string) (SearchQuery, error) { + return NewTextQuery(grantedProjectColumnGrantResourceOwner, value, TextEquals) +} + +func NewGrantedProjectGrantedOrganizationIDSearchQuery(value string) (SearchQuery, error) { + return NewTextQuery(grantedProjectColumnGrantedOrganization, value, TextEquals) +} + func NewProjectNameSearchQuery(method TextComparison, value string) (SearchQuery, error) { return NewTextQuery(ProjectColumnName, value, method) } @@ -285,3 +490,183 @@ func prepareProjectsQuery() (sq.SelectBuilder, func(*sql.Rows) (*Projects, error }, nil } } + +type GrantedProjects struct { + SearchResponse + GrantedProjects []*GrantedProject +} + +func grantedProjectsCheckPermission(ctx context.Context, grantedProjects *GrantedProjects, permissionCheck domain.PermissionCheck) { + grantedProjects.GrantedProjects = slices.DeleteFunc(grantedProjects.GrantedProjects, + func(grantedProject *GrantedProject) bool { + if grantedProject.GrantedOrgID != "" { + return projectGrantCheckPermission(ctx, grantedProject.ResourceOwner, grantedProject.ProjectID, grantedProject.GrantID, grantedProject.GrantedOrgID, permissionCheck) != nil + } + return projectCheckPermission(ctx, grantedProject.ResourceOwner, grantedProject.ProjectID, permissionCheck) != nil + }, + ) +} + +type GrantedProject struct { + ProjectID string + CreationDate time.Time + ChangeDate time.Time + ResourceOwner string + InstanceID string + ProjectState domain.ProjectState + ProjectName string + + ProjectRoleAssertion bool + ProjectRoleCheck bool + HasProjectCheck bool + PrivateLabelingSetting domain.PrivateLabelingSetting + + GrantID string + GrantedOrgID string + OrgName string + ProjectGrantState domain.ProjectGrantState +} + +func prepareGrantedProjectsQuery() (sq.SelectBuilder, func(*sql.Rows) (*GrantedProjects, error)) { + return sq.Select( + GrantedProjectColumnID.identifier(), + GrantedProjectColumnCreationDate.identifier(), + GrantedProjectColumnChangeDate.identifier(), + grantedProjectColumnResourceOwner.identifier(), + grantedProjectColumnInstanceID.identifier(), + grantedProjectColumnState.identifier(), + GrantedProjectColumnName.identifier(), + grantedProjectColumnProjectRoleAssertion.identifier(), + grantedProjectColumnProjectRoleCheck.identifier(), + grantedProjectColumnHasProjectCheck.identifier(), + grantedProjectColumnPrivateLabelingSetting.identifier(), + grantedProjectColumnGrantID.identifier(), + grantedProjectColumnGrantedOrganization.identifier(), + grantedProjectColumnGrantedOrganizationName.identifier(), + grantedProjectColumnGrantState.identifier(), + countColumn.identifier(), + ).From(getProjectsAndGrantedProjectsFromQuery()). + PlaceholderFormat(sq.Dollar), + func(rows *sql.Rows) (*GrantedProjects, error) { + projects := make([]*GrantedProject, 0) + var ( + count uint64 + grantID = sql.NullString{} + orgID = sql.NullString{} + orgName = sql.NullString{} + projectGrantState = sql.NullInt16{} + ) + for rows.Next() { + grantedProject := new(GrantedProject) + err := rows.Scan( + &grantedProject.ProjectID, + &grantedProject.CreationDate, + &grantedProject.ChangeDate, + &grantedProject.ResourceOwner, + &grantedProject.InstanceID, + &grantedProject.ProjectState, + &grantedProject.ProjectName, + &grantedProject.ProjectRoleAssertion, + &grantedProject.ProjectRoleCheck, + &grantedProject.HasProjectCheck, + &grantedProject.PrivateLabelingSetting, + &grantID, + &orgID, + &orgName, + &projectGrantState, + &count, + ) + if err != nil { + return nil, err + } + if grantID.Valid { + grantedProject.GrantID = grantID.String + } + if orgID.Valid { + grantedProject.GrantedOrgID = orgID.String + } + if orgName.Valid { + grantedProject.OrgName = orgName.String + } + if projectGrantState.Valid { + grantedProject.ProjectGrantState = domain.ProjectGrantState(projectGrantState.Int16) + } + projects = append(projects, grantedProject) + } + + if err := rows.Close(); err != nil { + return nil, zerrors.ThrowInternal(err, "QUERY-K9gEE", "Errors.Query.CloseRows") + } + + return &GrantedProjects{ + GrantedProjects: projects, + SearchResponse: SearchResponse{ + Count: count, + }, + }, nil + } +} + +func getProjectsAndGrantedProjectsFromQuery() string { + return "(" + + prepareProjects() + + " UNION ALL " + + prepareGrantedProjects() + + ") AS " + grantedProjectsAlias.identifier() +} + +func prepareProjects() string { + builder := sq.Select( + ProjectColumnID.identifier()+" AS "+GrantedProjectColumnID.name, + ProjectColumnCreationDate.identifier()+" AS "+GrantedProjectColumnCreationDate.name, + ProjectColumnChangeDate.identifier()+" AS "+GrantedProjectColumnChangeDate.name, + ProjectColumnResourceOwner.identifier()+" AS "+grantedProjectColumnResourceOwner.name, + ProjectColumnInstanceID.identifier()+" AS "+grantedProjectColumnInstanceID.name, + ProjectColumnState.identifier()+" AS "+grantedProjectColumnState.name, + ProjectColumnName.identifier()+" AS "+GrantedProjectColumnName.name, + ProjectColumnProjectRoleAssertion.identifier()+" AS "+grantedProjectColumnProjectRoleAssertion.name, + ProjectColumnProjectRoleCheck.identifier()+" AS "+grantedProjectColumnProjectRoleCheck.name, + ProjectColumnHasProjectCheck.identifier()+" AS "+grantedProjectColumnHasProjectCheck.name, + ProjectColumnPrivateLabelingSetting.identifier()+" AS "+grantedProjectColumnPrivateLabelingSetting.name, + "NULL::TEXT AS "+grantedProjectColumnGrantResourceOwner.name, + "NULL::TEXT AS "+grantedProjectColumnGrantID.name, + "NULL::TEXT AS "+grantedProjectColumnGrantedOrganization.name, + "NULL::TEXT AS "+grantedProjectColumnGrantedOrganizationName.name, + "NULL::SMALLINT AS "+grantedProjectColumnGrantState.name, + countColumn.identifier()). + From(projectsTable.identifier()). + PlaceholderFormat(sq.Dollar) + + stmt, _ := builder.MustSql() + return stmt +} + +func prepareGrantedProjects() string { + grantedOrgTable := orgsTable.setAlias(ProjectGrantGrantedOrgTableAlias) + grantedOrgIDColumn := OrgColumnID.setTable(grantedOrgTable) + builder := sq.Select( + ProjectGrantColumnProjectID.identifier()+" AS "+GrantedProjectColumnID.name, + ProjectGrantColumnCreationDate.identifier()+" AS "+GrantedProjectColumnCreationDate.name, + ProjectGrantColumnChangeDate.identifier()+" AS "+GrantedProjectColumnChangeDate.name, + ProjectColumnResourceOwner.identifier()+" AS "+grantedProjectColumnResourceOwner.name, + ProjectGrantColumnInstanceID.identifier()+" AS "+grantedProjectColumnInstanceID.name, + ProjectColumnState.identifier()+" AS "+grantedProjectColumnState.name, + ProjectColumnName.identifier()+" AS "+GrantedProjectColumnName.name, + ProjectColumnProjectRoleAssertion.identifier()+" AS "+grantedProjectColumnProjectRoleAssertion.name, + ProjectColumnProjectRoleCheck.identifier()+" AS "+grantedProjectColumnProjectRoleCheck.name, + ProjectColumnHasProjectCheck.identifier()+" AS "+grantedProjectColumnHasProjectCheck.name, + ProjectColumnPrivateLabelingSetting.identifier()+" AS "+grantedProjectColumnPrivateLabelingSetting.name, + ProjectGrantColumnResourceOwner.identifier()+" AS "+grantedProjectColumnGrantResourceOwner.name, + ProjectGrantColumnGrantID.identifier()+" AS "+grantedProjectColumnGrantID.name, + ProjectGrantColumnGrantedOrgID.identifier()+" AS "+grantedProjectColumnGrantedOrganization.name, + ProjectGrantColumnGrantedOrgName.identifier()+" AS "+grantedProjectColumnGrantedOrganizationName.name, + ProjectGrantColumnState.identifier()+" AS "+grantedProjectColumnGrantState.name, + countColumn.identifier()). + From(projectGrantsTable.identifier()). + PlaceholderFormat(sq.Dollar). + LeftJoin(join(ProjectColumnID, ProjectGrantColumnProjectID)). + LeftJoin(join(grantedOrgIDColumn, ProjectGrantColumnGrantedOrgID)) + + stmt, _ := builder.MustSql() + return stmt +} diff --git a/internal/query/project_grant.go b/internal/query/project_grant.go index e4388c377a..17e40e01eb 100644 --- a/internal/query/project_grant.go +++ b/internal/query/project_grant.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "errors" + "slices" "time" sq "github.com/Masterminds/squirrel" @@ -104,6 +105,50 @@ type ProjectGrantSearchQueries struct { Queries []SearchQuery } +func projectGrantsCheckPermission(ctx context.Context, projectGrants *ProjectGrants, permissionCheck domain.PermissionCheck) { + projectGrants.ProjectGrants = slices.DeleteFunc(projectGrants.ProjectGrants, + func(projectGrant *ProjectGrant) bool { + return projectGrantCheckPermission(ctx, projectGrant.ResourceOwner, projectGrant.ProjectID, projectGrant.GrantID, projectGrant.GrantedOrgID, permissionCheck) != nil + }, + ) +} + +func projectGrantCheckPermission(ctx context.Context, resourceOwner, projectID, grantID, grantedOrgID string, permissionCheck domain.PermissionCheck) error { + if err := permissionCheck(ctx, domain.PermissionProjectGrantRead, resourceOwner, grantID); err != nil { + if err := permissionCheck(ctx, domain.PermissionProjectGrantRead, grantedOrgID, grantID); err != nil { + if err := permissionCheck(ctx, domain.PermissionProjectGrantRead, resourceOwner, projectID); err != nil { + return err + } + } + } + return nil +} + +// TODO: add permission check on project grant level +func projectGrantPermissionCheckV2(ctx context.Context, query sq.SelectBuilder, enabled bool, queries *ProjectGrantSearchQueries) sq.SelectBuilder { + if !enabled { + return query + } + join, args := PermissionClause( + ctx, + ProjectGrantColumnResourceOwner, + domain.PermissionProjectGrantRead, + SingleOrgPermissionOption(queries.Queries), + ) + return query.JoinClause(join, args...) +} + +func (q *Queries) GetProjectGrantByIDWithPermission(ctx context.Context, shouldTriggerBulk bool, id string, permissionCheck domain.PermissionCheck) (*ProjectGrant, error) { + projectGrant, err := q.ProjectGrantByID(ctx, shouldTriggerBulk, id) + if err != nil { + return nil, err + } + if err := projectCheckPermission(ctx, projectGrant.ResourceOwner, projectGrant.GrantID, permissionCheck); err != nil { + return nil, err + } + return projectGrant, nil +} + func (q *Queries) ProjectGrantByID(ctx context.Context, shouldTriggerBulk bool, id string) (grant *ProjectGrant, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -154,11 +199,25 @@ func (q *Queries) ProjectGrantByIDAndGrantedOrg(ctx context.Context, id, granted return grant, err } -func (q *Queries) SearchProjectGrants(ctx context.Context, queries *ProjectGrantSearchQueries) (grants *ProjectGrants, err error) { +func (q *Queries) SearchProjectGrants(ctx context.Context, queries *ProjectGrantSearchQueries, permissionCheck domain.PermissionCheck) (grants *ProjectGrants, err error) { + // removed as permission v2 is not implemented yet for project grant level permissions + // permissionCheckV2 := PermissionV2(ctx, permissionCheck) + projectsGrants, err := q.searchProjectGrants(ctx, queries, false) + if err != nil { + return nil, err + } + if permissionCheck != nil { // && !authz.GetFeatures(ctx).PermissionCheckV2 { + projectGrantsCheckPermission(ctx, projectsGrants, permissionCheck) + } + return projectsGrants, nil +} + +func (q *Queries) searchProjectGrants(ctx context.Context, queries *ProjectGrantSearchQueries, permissionCheckV2 bool) (grants *ProjectGrants, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() query, scan := prepareProjectGrantsQuery() + query = projectGrantPermissionCheckV2(ctx, query, permissionCheckV2, queries) eq := sq.Eq{ ProjectGrantColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), } @@ -179,6 +238,7 @@ func (q *Queries) SearchProjectGrants(ctx context.Context, queries *ProjectGrant return grants, err } +// SearchProjectGrantsByProjectIDAndRoleKey is used internally to remove the roles of a project grant, so no permission check necessary func (q *Queries) SearchProjectGrantsByProjectIDAndRoleKey(ctx context.Context, projectID, roleKey string) (projects *ProjectGrants, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -195,13 +255,33 @@ func (q *Queries) SearchProjectGrantsByProjectIDAndRoleKey(ctx context.Context, if err != nil { return nil, err } - return q.SearchProjectGrants(ctx, searchQuery) + return q.SearchProjectGrants(ctx, searchQuery, nil) } func NewProjectGrantProjectIDSearchQuery(value string) (SearchQuery, error) { return NewTextQuery(ProjectGrantColumnProjectID, value, TextEquals) } +func (q *ProjectGrantSearchQueries) AppendPermissionQueries(permissions []string) error { + if !authz.HasGlobalPermission(permissions) { + ids := authz.GetAllPermissionCtxIDs(permissions) + query, err := NewProjectGrantIDsSearchQuery(ids) + if err != nil { + return err + } + q.Queries = append(q.Queries, query) + } + return nil +} + +func NewProjectGrantProjectIDsSearchQuery(ids []string) (SearchQuery, error) { + list := make([]interface{}, len(ids)) + for i, value := range ids { + list[i] = value + } + return NewListQuery(ProjectGrantColumnProjectID, list, ListIn) +} + func NewProjectGrantIDsSearchQuery(values []string) (SearchQuery, error) { list := make([]interface{}, len(values)) for i, value := range values { @@ -243,18 +323,6 @@ func (q *ProjectGrantSearchQueries) AppendGrantedOrgQuery(orgID string) error { return nil } -func (q *ProjectGrantSearchQueries) AppendPermissionQueries(permissions []string) error { - if !authz.HasGlobalPermission(permissions) { - ids := authz.GetAllPermissionCtxIDs(permissions) - query, err := NewProjectGrantIDsSearchQuery(ids) - if err != nil { - return err - } - q.Queries = append(q.Queries, query) - } - return nil -} - func (q *ProjectGrantSearchQueries) toQuery(query sq.SelectBuilder) sq.SelectBuilder { query = q.SearchRequest.toQuery(query) for _, q := range q.Queries { diff --git a/internal/query/project_grant_member.go b/internal/query/project_grant_member.go index a9cc49c498..2f469dce80 100644 --- a/internal/query/project_grant_member.go +++ b/internal/query/project_grant_member.go @@ -128,7 +128,7 @@ func prepareProjectGrantMembersQuery() (sq.SelectBuilder, func(*sql.Rows) (*Memb LeftJoin(join(MachineUserIDCol, ProjectGrantMemberUserID)). LeftJoin(join(UserIDCol, ProjectGrantMemberUserID)). LeftJoin(join(LoginNameUserIDCol, ProjectGrantMemberUserID)). - LeftJoin(join(ProjectGrantColumnGrantID, ProjectGrantMemberGrantID)). + LeftJoin(join(ProjectGrantColumnGrantID, ProjectGrantMemberGrantID) + " AND " + ProjectGrantMemberProjectID.identifier() + " = " + ProjectGrantColumnProjectID.identifier()). Where( sq.Eq{LoginNameIsPrimaryCol.identifier(): true}, ).PlaceholderFormat(sq.Dollar), diff --git a/internal/query/project_grant_member_test.go b/internal/query/project_grant_member_test.go index 23d1258b7c..be0f5f572e 100644 --- a/internal/query/project_grant_member_test.go +++ b/internal/query/project_grant_member_test.go @@ -46,6 +46,7 @@ var ( "LEFT JOIN projections.project_grants4 " + "ON members.grant_id = projections.project_grants4.grant_id " + "AND members.instance_id = projections.project_grants4.instance_id " + + "AND members.project_id = projections.project_grants4.project_id " + "WHERE projections.login_names3.is_primary = $1") projectGrantMembersColumns = []string{ "creation_date", diff --git a/internal/query/project_role.go b/internal/query/project_role.go index ab4f40ca38..e70fcf277e 100644 --- a/internal/query/project_role.go +++ b/internal/query/project_role.go @@ -3,12 +3,14 @@ package query import ( "context" "database/sql" + "slices" "time" sq "github.com/Masterminds/squirrel" "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -80,7 +82,45 @@ type ProjectRoleSearchQueries struct { Queries []SearchQuery } -func (q *Queries) SearchProjectRoles(ctx context.Context, shouldTriggerBulk bool, queries *ProjectRoleSearchQueries) (roles *ProjectRoles, err error) { +func projectRolesCheckPermission(ctx context.Context, projectRoles *ProjectRoles, permissionCheck domain.PermissionCheck) { + projectRoles.ProjectRoles = slices.DeleteFunc(projectRoles.ProjectRoles, + func(projectRole *ProjectRole) bool { + return projectRoleCheckPermission(ctx, projectRole.ResourceOwner, projectRole.Key, permissionCheck) != nil + }, + ) +} + +func projectRoleCheckPermission(ctx context.Context, resourceOwner string, grantID string, permissionCheck domain.PermissionCheck) error { + return permissionCheck(ctx, domain.PermissionProjectGrantRead, resourceOwner, grantID) +} + +func projectRolePermissionCheckV2(ctx context.Context, query sq.SelectBuilder, enabled bool, queries *ProjectRoleSearchQueries) sq.SelectBuilder { + if !enabled { + return query + } + join, args := PermissionClause( + ctx, + ProjectRoleColumnResourceOwner, + domain.PermissionProjectRoleRead, + SingleOrgPermissionOption(queries.Queries), + WithProjectsPermissionOption(ProjectRoleColumnProjectID), + ) + return query.JoinClause(join, args...) +} + +func (q *Queries) SearchProjectRoles(ctx context.Context, shouldTriggerBulk bool, queries *ProjectRoleSearchQueries, permissionCheck domain.PermissionCheck) (roles *ProjectRoles, err error) { + permissionCheckV2 := PermissionV2(ctx, permissionCheck) + projectRoles, err := q.searchProjectRoles(ctx, shouldTriggerBulk, queries, permissionCheckV2) + if err != nil { + return nil, err + } + if permissionCheck != nil && !authz.GetFeatures(ctx).PermissionCheckV2 { + projectRolesCheckPermission(ctx, projectRoles, permissionCheck) + } + return projectRoles, nil +} + +func (q *Queries) searchProjectRoles(ctx context.Context, shouldTriggerBulk bool, queries *ProjectRoleSearchQueries, permissionCheckV2 bool) (roles *ProjectRoles, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -94,6 +134,7 @@ func (q *Queries) SearchProjectRoles(ctx context.Context, shouldTriggerBulk bool eq := sq.Eq{ProjectRoleColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()} query, scan := prepareProjectRolesQuery() + query = projectRolePermissionCheckV2(ctx, query, permissionCheckV2, queries) stmt, args, err := queries.toQuery(query).Where(eq).ToSql() if err != nil { return nil, zerrors.ThrowInvalidArgument(err, "QUERY-3N9ff", "Errors.Query.InvalidRequest") diff --git a/internal/query/projection/authn_key.go b/internal/query/projection/authn_key.go index e2229ad332..a287701cfb 100644 --- a/internal/query/projection/authn_key.go +++ b/internal/query/projection/authn_key.go @@ -62,6 +62,9 @@ func (*authNKeyProjection) Init() *old_handler.Check { handler.NewPrimaryKey(AuthNKeyInstanceIDCol, AuthNKeyIDCol), handler.WithIndex(handler.NewIndex("enabled", []string{AuthNKeyEnabledCol})), handler.WithIndex(handler.NewIndex("identifier", []string{AuthNKeyIdentifierCol})), + handler.WithIndex(handler.NewIndex("resource_owner", []string{AuthNKeyResourceOwnerCol})), + handler.WithIndex(handler.NewIndex("creation_date", []string{AuthNKeyCreationDateCol})), + handler.WithIndex(handler.NewIndex("expiration_date", []string{AuthNKeyExpirationCol})), ), ) } diff --git a/internal/query/projection/hosted_login_translation.go b/internal/query/projection/hosted_login_translation.go new file mode 100644 index 0000000000..865d3738b9 --- /dev/null +++ b/internal/query/projection/hosted_login_translation.go @@ -0,0 +1,144 @@ +package projection + +import ( + "context" + "crypto/md5" + "encoding/hex" + "fmt" + + "github.com/zitadel/zitadel/internal/eventstore" + old_handler "github.com/zitadel/zitadel/internal/eventstore/handler" + "github.com/zitadel/zitadel/internal/eventstore/handler/v2" + "github.com/zitadel/zitadel/internal/repository/instance" + "github.com/zitadel/zitadel/internal/repository/org" + "github.com/zitadel/zitadel/internal/zerrors" +) + +const ( + HostedLoginTranslationTable = "projections.hosted_login_translations" + + HostedLoginTranslationInstanceIDCol = "instance_id" + HostedLoginTranslationCreationDateCol = "creation_date" + HostedLoginTranslationChangeDateCol = "change_date" + HostedLoginTranslationAggregateIDCol = "aggregate_id" + HostedLoginTranslationAggregateTypeCol = "aggregate_type" + HostedLoginTranslationSequenceCol = "sequence" + HostedLoginTranslationLocaleCol = "locale" + HostedLoginTranslationFileCol = "file" + HostedLoginTranslationEtagCol = "etag" +) + +type hostedLoginTranslationProjection struct{} + +func newHostedLoginTranslationProjection(ctx context.Context, config handler.Config) *handler.Handler { + return handler.NewHandler(ctx, &config, new(hostedLoginTranslationProjection)) +} + +// Init implements [handler.initializer] +func (p *hostedLoginTranslationProjection) Init() *old_handler.Check { + return handler.NewTableCheck( + handler.NewTable([]*handler.InitColumn{ + handler.NewColumn(HostedLoginTranslationInstanceIDCol, handler.ColumnTypeText), + handler.NewColumn(HostedLoginTranslationCreationDateCol, handler.ColumnTypeTimestamp), + handler.NewColumn(HostedLoginTranslationChangeDateCol, handler.ColumnTypeTimestamp), + handler.NewColumn(HostedLoginTranslationAggregateIDCol, handler.ColumnTypeText), + handler.NewColumn(HostedLoginTranslationAggregateTypeCol, handler.ColumnTypeText), + handler.NewColumn(HostedLoginTranslationSequenceCol, handler.ColumnTypeInt64), + handler.NewColumn(HostedLoginTranslationLocaleCol, handler.ColumnTypeText), + handler.NewColumn(HostedLoginTranslationFileCol, handler.ColumnTypeJSONB), + handler.NewColumn(HostedLoginTranslationEtagCol, handler.ColumnTypeText), + }, + handler.NewPrimaryKey( + HostedLoginTranslationInstanceIDCol, + HostedLoginTranslationAggregateIDCol, + HostedLoginTranslationAggregateTypeCol, + HostedLoginTranslationLocaleCol, + ), + ), + ) +} + +func (hltp *hostedLoginTranslationProjection) Name() string { + return HostedLoginTranslationTable +} + +func (hltp *hostedLoginTranslationProjection) Reducers() []handler.AggregateReducer { + return []handler.AggregateReducer{ + { + Aggregate: org.AggregateType, + EventReducers: []handler.EventReducer{ + { + Event: org.HostedLoginTranslationSet, + Reduce: hltp.reduceSet, + }, + }, + }, + { + Aggregate: instance.AggregateType, + EventReducers: []handler.EventReducer{ + { + Event: instance.HostedLoginTranslationSet, + Reduce: hltp.reduceSet, + }, + }, + }, + } +} + +func (hltp *hostedLoginTranslationProjection) reduceSet(e eventstore.Event) (*handler.Statement, error) { + + switch e := e.(type) { + case *org.HostedLoginTranslationSetEvent: + orgEvent := *e + return handler.NewUpsertStatement( + &orgEvent, + []handler.Column{ + handler.NewCol(HostedLoginTranslationInstanceIDCol, nil), + handler.NewCol(HostedLoginTranslationAggregateIDCol, nil), + handler.NewCol(HostedLoginTranslationAggregateTypeCol, nil), + handler.NewCol(HostedLoginTranslationLocaleCol, nil), + }, + []handler.Column{ + handler.NewCol(HostedLoginTranslationInstanceIDCol, orgEvent.Aggregate().InstanceID), + handler.NewCol(HostedLoginTranslationAggregateIDCol, orgEvent.Aggregate().ID), + handler.NewCol(HostedLoginTranslationAggregateTypeCol, orgEvent.Aggregate().Type), + handler.NewCol(HostedLoginTranslationCreationDateCol, handler.OnlySetValueOnInsert(HostedLoginTranslationTable, orgEvent.CreationDate())), + handler.NewCol(HostedLoginTranslationChangeDateCol, orgEvent.CreationDate()), + handler.NewCol(HostedLoginTranslationSequenceCol, orgEvent.Sequence()), + handler.NewCol(HostedLoginTranslationLocaleCol, orgEvent.Language), + handler.NewCol(HostedLoginTranslationFileCol, orgEvent.Translation), + handler.NewCol(HostedLoginTranslationEtagCol, hltp.computeEtag(orgEvent.Translation)), + }, + ), nil + case *instance.HostedLoginTranslationSetEvent: + instanceEvent := *e + return handler.NewUpsertStatement( + &instanceEvent, + []handler.Column{ + handler.NewCol(HostedLoginTranslationInstanceIDCol, nil), + handler.NewCol(HostedLoginTranslationAggregateIDCol, nil), + handler.NewCol(HostedLoginTranslationAggregateTypeCol, nil), + handler.NewCol(HostedLoginTranslationLocaleCol, nil), + }, + []handler.Column{ + handler.NewCol(HostedLoginTranslationInstanceIDCol, instanceEvent.Aggregate().InstanceID), + handler.NewCol(HostedLoginTranslationAggregateIDCol, instanceEvent.Aggregate().ID), + handler.NewCol(HostedLoginTranslationAggregateTypeCol, instanceEvent.Aggregate().Type), + handler.NewCol(HostedLoginTranslationCreationDateCol, handler.OnlySetValueOnInsert(HostedLoginTranslationTable, instanceEvent.CreationDate())), + handler.NewCol(HostedLoginTranslationChangeDateCol, instanceEvent.CreationDate()), + handler.NewCol(HostedLoginTranslationSequenceCol, instanceEvent.Sequence()), + handler.NewCol(HostedLoginTranslationLocaleCol, instanceEvent.Language), + handler.NewCol(HostedLoginTranslationFileCol, instanceEvent.Translation), + handler.NewCol(HostedLoginTranslationEtagCol, hltp.computeEtag(instanceEvent.Translation)), + }, + ), nil + default: + return nil, zerrors.ThrowInvalidArgumentf(nil, "PROJE-AZshaa", "reduce.wrong.event.type %v", []eventstore.EventType{org.HostedLoginTranslationSet}) + } + +} + +func (hltp *hostedLoginTranslationProjection) computeEtag(translation map[string]any) string { + hash := md5.Sum(fmt.Append(nil, translation)) + return hex.EncodeToString(hash[:]) +} diff --git a/internal/query/projection/instance_features.go b/internal/query/projection/instance_features.go index 34100a0d66..32ec2cf111 100644 --- a/internal/query/projection/instance_features.go +++ b/internal/query/projection/instance_features.go @@ -64,14 +64,6 @@ func (*instanceFeatureProjection) Reducers() []handler.AggregateReducer { Event: feature_v2.InstanceLoginDefaultOrgEventType, Reduce: reduceInstanceSetFeature[bool], }, - { - Event: feature_v2.InstanceTriggerIntrospectionProjectionsEventType, - Reduce: reduceInstanceSetFeature[bool], - }, - { - Event: feature_v2.InstanceLegacyIntrospectionEventType, - Reduce: reduceInstanceSetFeature[bool], - }, { Event: feature_v2.InstanceUserSchemaEventType, Reduce: reduceInstanceSetFeature[bool], @@ -84,10 +76,6 @@ func (*instanceFeatureProjection) Reducers() []handler.AggregateReducer { Event: feature_v2.InstanceImprovedPerformanceEventType, Reduce: reduceInstanceSetFeature[[]feature.ImprovedPerformanceType], }, - { - Event: feature_v2.InstanceWebKeyEventType, - Reduce: reduceInstanceSetFeature[bool], - }, { Event: feature_v2.InstanceDebugOIDCParentErrorEventType, Reduce: reduceInstanceSetFeature[bool], diff --git a/internal/query/projection/instance_features_test.go b/internal/query/projection/instance_features_test.go index 4a4a46727f..703a0ce00a 100644 --- a/internal/query/projection/instance_features_test.go +++ b/internal/query/projection/instance_features_test.go @@ -26,7 +26,7 @@ func TestInstanceFeaturesProjection_reduces(t *testing.T) { args: args{ event: getEvent( testEvent( - feature_v2.InstanceLegacyIntrospectionEventType, + feature_v2.SystemUserSchemaEventType, feature_v2.AggregateType, []byte(`{"value": true}`), ), eventstore.GenericEventMapper[feature_v2.SetEvent[bool]]), @@ -41,7 +41,7 @@ func TestInstanceFeaturesProjection_reduces(t *testing.T) { expectedStmt: "INSERT INTO projections.instance_features2 (instance_id, key, creation_date, change_date, sequence, value) VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (instance_id, key) DO UPDATE SET (creation_date, change_date, sequence, value) = (projections.instance_features2.creation_date, EXCLUDED.change_date, EXCLUDED.sequence, EXCLUDED.value)", expectedArgs: []interface{}{ "agg-id", - "legacy_introspection", + "user_schema", anyArg{}, anyArg{}, uint64(15), diff --git a/internal/query/projection/projection.go b/internal/query/projection/projection.go index 77a28ac79a..5ad62380ea 100644 --- a/internal/query/projection/projection.go +++ b/internal/query/projection/projection.go @@ -86,6 +86,7 @@ var ( UserSchemaProjection *handler.Handler WebKeyProjection *handler.Handler DebugEventsProjection *handler.Handler + HostedLoginTranslationProjection *handler.Handler ProjectGrantFields *handler.FieldHandler OrgDomainVerifiedFields *handler.FieldHandler @@ -179,6 +180,7 @@ func Create(ctx context.Context, sqlClient *database.DB, es handler.EventStore, UserSchemaProjection = newUserSchemaProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["user_schemas"])) WebKeyProjection = newWebKeyProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["web_keys"])) DebugEventsProjection = newDebugEventsProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["debug_events"])) + HostedLoginTranslationProjection = newHostedLoginTranslationProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["hosted_login_translation"])) ProjectGrantFields = newFillProjectGrantFields(applyCustomConfig(projectionConfig, config.Customizations[fieldsProjectGrant])) OrgDomainVerifiedFields = newFillOrgDomainVerifiedFields(applyCustomConfig(projectionConfig, config.Customizations[fieldsOrgDomainVerified])) @@ -357,5 +359,6 @@ func newProjectionsList() { UserSchemaProjection, WebKeyProjection, DebugEventsProjection, + HostedLoginTranslationProjection, } } diff --git a/internal/query/projection/system_features.go b/internal/query/projection/system_features.go index de54054e78..32f49108e6 100644 --- a/internal/query/projection/system_features.go +++ b/internal/query/projection/system_features.go @@ -56,14 +56,6 @@ func (*systemFeatureProjection) Reducers() []handler.AggregateReducer { Event: feature_v2.SystemLoginDefaultOrgEventType, Reduce: reduceSystemSetFeature[bool], }, - { - Event: feature_v2.SystemTriggerIntrospectionProjectionsEventType, - Reduce: reduceSystemSetFeature[bool], - }, - { - Event: feature_v2.SystemLegacyIntrospectionEventType, - Reduce: reduceSystemSetFeature[bool], - }, { Event: feature_v2.SystemUserSchemaEventType, Reduce: reduceSystemSetFeature[bool], diff --git a/internal/query/projection/system_features_test.go b/internal/query/projection/system_features_test.go index 9bc19573cc..b64db7fb0a 100644 --- a/internal/query/projection/system_features_test.go +++ b/internal/query/projection/system_features_test.go @@ -24,7 +24,7 @@ func TestSystemFeaturesProjection_reduces(t *testing.T) { args: args{ event: getEvent( testEvent( - feature_v2.SystemLegacyIntrospectionEventType, + feature_v2.SystemUserSchemaEventType, feature_v2.AggregateType, []byte(`{"value": true}`), ), eventstore.GenericEventMapper[feature_v2.SetEvent[bool]]), @@ -38,7 +38,7 @@ func TestSystemFeaturesProjection_reduces(t *testing.T) { { expectedStmt: "INSERT INTO projections.system_features (key, creation_date, change_date, sequence, value) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (key) DO UPDATE SET (creation_date, change_date, sequence, value) = (projections.system_features.creation_date, EXCLUDED.change_date, EXCLUDED.sequence, EXCLUDED.value)", expectedArgs: []interface{}{ - "legacy_introspection", + "user_schema", anyArg{}, anyArg{}, uint64(15), diff --git a/internal/query/projection/user_personal_access_token.go b/internal/query/projection/user_personal_access_token.go index 0efb5d6412..610ca9c4e2 100644 --- a/internal/query/projection/user_personal_access_token.go +++ b/internal/query/projection/user_personal_access_token.go @@ -56,6 +56,8 @@ func (*personalAccessTokenProjection) Init() *old_handler.Check { handler.WithIndex(handler.NewIndex("user_id", []string{PersonalAccessTokenColumnUserID})), handler.WithIndex(handler.NewIndex("resource_owner", []string{PersonalAccessTokenColumnResourceOwner})), handler.WithIndex(handler.NewIndex("owner_removed", []string{PersonalAccessTokenColumnOwnerRemoved})), + handler.WithIndex(handler.NewIndex("creation_date", []string{PersonalAccessTokenColumnCreationDate})), + handler.WithIndex(handler.NewIndex("expiration_date", []string{PersonalAccessTokenColumnExpiration})), ), ) } diff --git a/internal/query/query.go b/internal/query/query.go index c0c051f7b7..e2e7f58ffc 100644 --- a/internal/query/query.go +++ b/internal/query/query.go @@ -148,3 +148,16 @@ func triggerBatch(ctx context.Context, handlers ...*handler.Handler) { wg.Wait() } + +func findTextEqualsQuery(column Column, queries []SearchQuery) (text string, ok bool) { + for _, query := range queries { + if query.Col() != column { + continue + } + tq, ok := query.(*textQuery) + if ok && tq.Compare == TextEquals { + return tq.Text, true + } + } + return "", false +} diff --git a/internal/query/resource_counts.go b/internal/query/resource_counts.go new file mode 100644 index 0000000000..9d486e0b90 --- /dev/null +++ b/internal/query/resource_counts.go @@ -0,0 +1,61 @@ +package query + +import ( + "context" + "database/sql" + _ "embed" + "time" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/telemetry/tracing" + "github.com/zitadel/zitadel/internal/zerrors" +) + +var ( + //go:embed resource_counts_list.sql + resourceCountsListQuery string +) + +type ResourceCount struct { + ID int // Primary key, used for pagination + InstanceID string + TableName string + ParentType domain.CountParentType + ParentID string + Resource string + UpdatedAt time.Time + Amount int +} + +// ListResourceCounts retrieves all resource counts. +// It supports pagination using lastID and limit parameters. +// +// TODO: Currently only a proof of concept, filters may be implemented later if required. +func (q *Queries) ListResourceCounts(ctx context.Context, lastID, limit int) (result []ResourceCount, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + err = q.client.QueryContext(ctx, func(rows *sql.Rows) error { + for rows.Next() { + var count ResourceCount + err := rows.Scan( + &count.ID, + &count.InstanceID, + &count.TableName, + &count.ParentType, + &count.ParentID, + &count.Resource, + &count.UpdatedAt, + &count.Amount) + if err != nil { + return zerrors.ThrowInternal(err, "QUERY-2f4g5", "Errors.Internal") + } + result = append(result, count) + } + return nil + }, resourceCountsListQuery, lastID, limit) + if err != nil { + return nil, zerrors.ThrowInternal(err, "QUERY-3f4g5", "Errors.Internal") + } + return result, nil +} diff --git a/internal/query/resource_counts_list.sql b/internal/query/resource_counts_list.sql new file mode 100644 index 0000000000..0d4abf87eb --- /dev/null +++ b/internal/query/resource_counts_list.sql @@ -0,0 +1,12 @@ +SELECT id, + instance_id, + table_name, + parent_type, + parent_id, + resource_name, + updated_at, + amount +FROM projections.resource_counts +WHERE id > $1 +ORDER BY id +LIMIT $2; diff --git a/internal/query/resource_counts_test.go b/internal/query/resource_counts_test.go new file mode 100644 index 0000000000..2829a660ef --- /dev/null +++ b/internal/query/resource_counts_test.go @@ -0,0 +1,109 @@ +package query + +import ( + "context" + _ "embed" + "regexp" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/domain" +) + +func TestQueries_ListResourceCounts(t *testing.T) { + columns := []string{"id", "instance_id", "table_name", "parent_type", "parent_id", "resource_name", "updated_at", "amount"} + type args struct { + lastID int + limit int + } + tests := []struct { + name string + args args + expects func(sqlmock.Sqlmock) + wantResult []ResourceCount + wantErr bool + }{ + { + name: "query error", + args: args{ + lastID: 0, + limit: 10, + }, + expects: func(mock sqlmock.Sqlmock) { + mock.ExpectQuery(regexp.QuoteMeta(resourceCountsListQuery)). + WithArgs(0, 10). + WillReturnError(assert.AnError) + }, + wantErr: true, + }, + { + name: "success", + args: args{ + lastID: 0, + limit: 10, + }, + expects: func(mock sqlmock.Sqlmock) { + mock.ExpectQuery(regexp.QuoteMeta(resourceCountsListQuery)). + WithArgs(0, 10). + WillReturnRows( + sqlmock.NewRows(columns). + AddRow(1, "instance_1", "table", "instance", "parent_1", "resource_name", time.Unix(1, 2), 5). + AddRow(2, "instance_2", "table", "instance", "parent_2", "resource_name", time.Unix(1, 2), 6), + ) + }, + wantResult: []ResourceCount{ + { + ID: 1, + InstanceID: "instance_1", + TableName: "table", + ParentType: domain.CountParentTypeInstance, + ParentID: "parent_1", + Resource: "resource_name", + UpdatedAt: time.Unix(1, 2), + Amount: 5, + }, + { + ID: 2, + InstanceID: "instance_2", + TableName: "table", + ParentType: domain.CountParentTypeInstance, + ParentID: "parent_2", + Resource: "resource_name", + UpdatedAt: time.Unix(1, 2), + Amount: 6, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + db, mock, err := sqlmock.New() + require.NoError(t, err) + defer func() { + err := mock.ExpectationsWereMet() + require.NoError(t, err) + }() + defer db.Close() + tt.expects(mock) + mock.ExpectClose() + q := &Queries{ + client: &database.DB{ + DB: db, + }, + } + + gotResult, err := q.ListResourceCounts(context.Background(), tt.args.lastID, tt.args.limit) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, tt.wantResult, gotResult, "ListResourceCounts() result mismatch") + }) + } +} diff --git a/internal/query/session.go b/internal/query/session.go index 111eb462a0..ff0cbd8d42 100644 --- a/internal/query/session.go +++ b/internal/query/session.go @@ -113,6 +113,24 @@ func sessionCheckPermission(ctx context.Context, resourceOwner string, creator s return permissionCheck(ctx, domain.PermissionSessionRead, resourceOwner, "") } +func sessionsPermissionCheckV2(ctx context.Context, query sq.SelectBuilder, enabled bool) sq.SelectBuilder { + if !enabled { + return query + } + join, args := PermissionClause( + ctx, + SessionColumnResourceOwner, + domain.PermissionSessionRead, + // Allow if user is creator + OwnedRowsPermissionOption(SessionColumnCreator), + // Allow if session belongs to the user + OwnedRowsPermissionOption(SessionColumnUserID), + // Allow if session belongs to the same useragent + ConnectionPermissionOption(SessionColumnUserAgentFingerprintID, authz.GetCtxData(ctx).AgentID), + ) + return query.JoinClause(join, args...) +} + func (q *SessionsSearchQueries) toQuery(query sq.SelectBuilder) sq.SelectBuilder { query = q.SearchRequest.toQuery(query) for _, q := range q.Queries { @@ -282,21 +300,23 @@ func (q *Queries) sessionByID(ctx context.Context, shouldTriggerBulk bool, id st } func (q *Queries) SearchSessions(ctx context.Context, queries *SessionsSearchQueries, permissionCheck domain.PermissionCheck) (*Sessions, error) { - sessions, err := q.searchSessions(ctx, queries) + permissionCheckV2 := PermissionV2(ctx, permissionCheck) + sessions, err := q.searchSessions(ctx, queries, permissionCheckV2) if err != nil { return nil, err } - if permissionCheck != nil { + if permissionCheck != nil && !permissionCheckV2 { sessionsCheckPermission(ctx, sessions, permissionCheck) } return sessions, nil } -func (q *Queries) searchSessions(ctx context.Context, queries *SessionsSearchQueries) (sessions *Sessions, err error) { +func (q *Queries) searchSessions(ctx context.Context, queries *SessionsSearchQueries, permissionCheckV2 bool) (sessions *Sessions, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() query, scan := prepareSessionsQuery() + query = sessionsPermissionCheckV2(ctx, query, permissionCheckV2) stmt, args, err := queries.toQuery(query). Where(sq.Eq{ SessionColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), diff --git a/internal/query/system_features.go b/internal/query/system_features.go index dcbbb7d6fe..34410cb9b8 100644 --- a/internal/query/system_features.go +++ b/internal/query/system_features.go @@ -20,17 +20,15 @@ func (f *FeatureSource[T]) set(level feature.Level, value any) { type SystemFeatures struct { Details *domain.ObjectDetails - LoginDefaultOrg FeatureSource[bool] - TriggerIntrospectionProjections FeatureSource[bool] - LegacyIntrospection FeatureSource[bool] - UserSchema FeatureSource[bool] - TokenExchange FeatureSource[bool] - ImprovedPerformance FeatureSource[[]feature.ImprovedPerformanceType] - OIDCSingleV1SessionTermination FeatureSource[bool] - DisableUserTokenEvent FeatureSource[bool] - EnableBackChannelLogout FeatureSource[bool] - LoginV2 FeatureSource[*feature.LoginV2] - PermissionCheckV2 FeatureSource[bool] + LoginDefaultOrg FeatureSource[bool] + UserSchema FeatureSource[bool] + TokenExchange FeatureSource[bool] + ImprovedPerformance FeatureSource[[]feature.ImprovedPerformanceType] + OIDCSingleV1SessionTermination FeatureSource[bool] + DisableUserTokenEvent FeatureSource[bool] + EnableBackChannelLogout FeatureSource[bool] + LoginV2 FeatureSource[*feature.LoginV2] + PermissionCheckV2 FeatureSource[bool] } func (q *Queries) GetSystemFeatures(ctx context.Context) (_ *SystemFeatures, err error) { diff --git a/internal/query/system_features_model.go b/internal/query/system_features_model.go index 69e1f35968..67045f314d 100644 --- a/internal/query/system_features_model.go +++ b/internal/query/system_features_model.go @@ -56,8 +56,6 @@ func (m *SystemFeaturesReadModel) Query() *eventstore.SearchQueryBuilder { EventTypes( feature_v2.SystemResetEventType, feature_v2.SystemLoginDefaultOrgEventType, - feature_v2.SystemTriggerIntrospectionProjectionsEventType, - feature_v2.SystemLegacyIntrospectionEventType, feature_v2.SystemUserSchemaEventType, feature_v2.SystemTokenExchangeEventType, feature_v2.SystemImprovedPerformanceEventType, @@ -81,15 +79,10 @@ func reduceSystemFeatureSet[T any](features *SystemFeatures, event *feature_v2.S return err } switch key { - case feature.KeyUnspecified, - feature.KeyActionsDeprecated: + case feature.KeyUnspecified: return nil case feature.KeyLoginDefaultOrg: features.LoginDefaultOrg.set(level, event.Value) - case feature.KeyTriggerIntrospectionProjections: - features.TriggerIntrospectionProjections.set(level, event.Value) - case feature.KeyLegacyIntrospection: - features.LegacyIntrospection.set(level, event.Value) case feature.KeyUserSchema: features.UserSchema.set(level, event.Value) case feature.KeyTokenExchange: diff --git a/internal/query/system_features_test.go b/internal/query/system_features_test.go index 5a58ac23d7..7aa12a6a8f 100644 --- a/internal/query/system_features_test.go +++ b/internal/query/system_features_test.go @@ -49,14 +49,6 @@ func TestQueries_GetSystemFeatures(t *testing.T) { context.Background(), aggregate, feature_v2.SystemLoginDefaultOrgEventType, false, )), - eventFromEventPusher(feature_v2.NewSetEvent( - context.Background(), aggregate, - feature_v2.SystemTriggerIntrospectionProjectionsEventType, true, - )), - eventFromEventPusher(feature_v2.NewSetEvent( - context.Background(), aggregate, - feature_v2.SystemLegacyIntrospectionEventType, false, - )), eventFromEventPusher(feature_v2.NewSetEvent( context.Background(), aggregate, feature_v2.SystemUserSchemaEventType, false, @@ -71,14 +63,6 @@ func TestQueries_GetSystemFeatures(t *testing.T) { Level: feature.LevelSystem, Value: false, }, - TriggerIntrospectionProjections: FeatureSource[bool]{ - Level: feature.LevelSystem, - Value: true, - }, - LegacyIntrospection: FeatureSource[bool]{ - Level: feature.LevelSystem, - Value: false, - }, UserSchema: FeatureSource[bool]{ Level: feature.LevelSystem, Value: false, @@ -93,14 +77,6 @@ func TestQueries_GetSystemFeatures(t *testing.T) { context.Background(), aggregate, feature_v2.SystemLoginDefaultOrgEventType, false, )), - eventFromEventPusher(feature_v2.NewSetEvent( - context.Background(), aggregate, - feature_v2.SystemTriggerIntrospectionProjectionsEventType, true, - )), - eventFromEventPusher(feature_v2.NewSetEvent( - context.Background(), aggregate, - feature_v2.SystemLegacyIntrospectionEventType, false, - )), eventFromEventPusher(feature_v2.NewSetEvent( context.Background(), aggregate, feature_v2.SystemUserSchemaEventType, false, @@ -109,10 +85,6 @@ func TestQueries_GetSystemFeatures(t *testing.T) { context.Background(), aggregate, feature_v2.SystemResetEventType, )), - eventFromEventPusher(feature_v2.NewSetEvent( - context.Background(), aggregate, - feature_v2.SystemTriggerIntrospectionProjectionsEventType, true, - )), ), ), want: &SystemFeatures{ @@ -123,14 +95,6 @@ func TestQueries_GetSystemFeatures(t *testing.T) { Level: feature.LevelUnspecified, Value: false, }, - TriggerIntrospectionProjections: FeatureSource[bool]{ - Level: feature.LevelSystem, - Value: true, - }, - LegacyIntrospection: FeatureSource[bool]{ - Level: feature.LevelUnspecified, - Value: false, - }, UserSchema: FeatureSource[bool]{ Level: feature.LevelUnspecified, Value: false, @@ -145,14 +109,6 @@ func TestQueries_GetSystemFeatures(t *testing.T) { context.Background(), aggregate, feature_v2.SystemLoginDefaultOrgEventType, false, )), - eventFromEventPusher(feature_v2.NewSetEvent( - context.Background(), aggregate, - feature_v2.SystemTriggerIntrospectionProjectionsEventType, true, - )), - eventFromEventPusher(feature_v2.NewSetEvent( - context.Background(), aggregate, - feature_v2.SystemLegacyIntrospectionEventType, false, - )), eventFromEventPusher(feature_v2.NewSetEvent( context.Background(), aggregate, feature_v2.SystemUserSchemaEventType, false, @@ -161,10 +117,6 @@ func TestQueries_GetSystemFeatures(t *testing.T) { context.Background(), aggregate, feature_v2.SystemResetEventType, )), - eventFromEventPusher(feature_v2.NewSetEvent( - context.Background(), aggregate, - feature_v2.SystemTriggerIntrospectionProjectionsEventType, true, - )), ), ), want: &SystemFeatures{ @@ -175,14 +127,6 @@ func TestQueries_GetSystemFeatures(t *testing.T) { Level: feature.LevelUnspecified, Value: false, }, - TriggerIntrospectionProjections: FeatureSource[bool]{ - Level: feature.LevelSystem, - Value: true, - }, - LegacyIntrospection: FeatureSource[bool]{ - Level: feature.LevelUnspecified, - Value: false, - }, UserSchema: FeatureSource[bool]{ Level: feature.LevelUnspecified, Value: false, diff --git a/internal/query/user.go b/internal/query/user.go index c0fc3de97c..66c7abb228 100644 --- a/internal/query/user.go +++ b/internal/query/user.go @@ -132,6 +132,24 @@ func usersCheckPermission(ctx context.Context, users *Users, permissionCheck dom ) } +func userPermissionCheckV2(ctx context.Context, query sq.SelectBuilder, enabled bool, filters []SearchQuery) sq.SelectBuilder { + return userPermissionCheckV2WithCustomColumns(ctx, query, enabled, filters, UserResourceOwnerCol, UserIDCol) +} + +func userPermissionCheckV2WithCustomColumns(ctx context.Context, query sq.SelectBuilder, enabled bool, filters []SearchQuery, userResourceOwnerCol, userID Column) sq.SelectBuilder { + if !enabled { + return query + } + join, args := PermissionClause( + ctx, + userResourceOwnerCol, + domain.PermissionUserRead, + SingleOrgPermissionOption(filters), + OwnedRowsPermissionOption(userID), + ) + return query.JoinClause(join, args...) +} + type UserSearchQueries struct { SearchRequest Queries []SearchQuery @@ -600,8 +618,9 @@ func (q *Queries) CountUsers(ctx context.Context, queries *UserSearchQueries) (c return count, err } -func (q *Queries) SearchUsers(ctx context.Context, queries *UserSearchQueries, filterOrgIds string, permissionCheck domain.PermissionCheck) (*Users, error) { - users, err := q.searchUsers(ctx, queries, filterOrgIds, permissionCheck != nil && authz.GetFeatures(ctx).PermissionCheckV2) +func (q *Queries) SearchUsers(ctx context.Context, queries *UserSearchQueries, permissionCheck domain.PermissionCheck) (*Users, error) { + permissionCheckV2 := PermissionV2(ctx, permissionCheck) + users, err := q.searchUsers(ctx, queries, permissionCheckV2) if err != nil { return nil, err } @@ -611,22 +630,15 @@ func (q *Queries) SearchUsers(ctx context.Context, queries *UserSearchQueries, f return users, nil } -func (q *Queries) searchUsers(ctx context.Context, queries *UserSearchQueries, filterOrgIds string, permissionCheckV2 bool) (users *Users, err error) { +func (q *Queries) searchUsers(ctx context.Context, queries *UserSearchQueries, permissionCheckV2 bool) (users *Users, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() query, scan := prepareUsersQuery() - query = queries.toQuery(query).Where(sq.Eq{ + query = userPermissionCheckV2(ctx, query, permissionCheckV2, queries.Queries) + stmt, args, err := queries.toQuery(query).Where(sq.Eq{ UserInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(), - }) - if permissionCheckV2 { - query, err = wherePermittedOrgsOrCurrentUser(ctx, query, filterOrgIds, UserResourceOwnerCol.identifier(), UserIDCol.identifier(), domain.PermissionUserRead) - if err != nil { - return nil, zerrors.ThrowInternal(err, "AUTHZ-HS4us", "Errors.Internal") - } - } - - stmt, args, err := query.ToSql() + }).ToSql() if err != nil { return nil, zerrors.ThrowInternal(err, "QUERY-Dgbg2", "Errors.Query.SQLStatement") } diff --git a/internal/query/user_auth_method.go b/internal/query/user_auth_method.go index 8b26389f1a..fce34967cf 100644 --- a/internal/query/user_auth_method.go +++ b/internal/query/user_auth_method.go @@ -104,6 +104,19 @@ func authMethodsCheckPermission(ctx context.Context, methods *AuthMethods, permi ) } +func userAuthMethodPermissionCheckV2(ctx context.Context, query sq.SelectBuilder, enabled bool) sq.SelectBuilder { + if !enabled { + return query + } + join, args := PermissionClause( + ctx, + UserAuthMethodColumnResourceOwner, + domain.PermissionUserRead, + OwnedRowsPermissionOption(UserIDCol), + ) + return query.JoinClause(join, args...) +} + type AuthMethod struct { UserID string CreationDate time.Time @@ -137,11 +150,12 @@ func (q *UserAuthMethodSearchQueries) hasUserID() bool { } func (q *Queries) SearchUserAuthMethods(ctx context.Context, queries *UserAuthMethodSearchQueries, permissionCheck domain.PermissionCheck) (userAuthMethods *AuthMethods, err error) { - methods, err := q.searchUserAuthMethods(ctx, queries) + permissionCheckV2 := PermissionV2(ctx, permissionCheck) + methods, err := q.searchUserAuthMethods(ctx, queries, permissionCheckV2) if err != nil { return nil, err } - if permissionCheck != nil && len(methods.AuthMethods) > 0 { + if permissionCheck != nil && len(methods.AuthMethods) > 0 && !permissionCheckV2 { // when userID for query is provided, only one check has to be done if queries.hasUserID() { if err := userCheckPermission(ctx, methods.AuthMethods[0].ResourceOwner, methods.AuthMethods[0].UserID, permissionCheck); err != nil { @@ -154,11 +168,12 @@ func (q *Queries) SearchUserAuthMethods(ctx context.Context, queries *UserAuthMe return methods, nil } -func (q *Queries) searchUserAuthMethods(ctx context.Context, queries *UserAuthMethodSearchQueries) (userAuthMethods *AuthMethods, err error) { +func (q *Queries) searchUserAuthMethods(ctx context.Context, queries *UserAuthMethodSearchQueries, permissionCheckV2 bool) (userAuthMethods *AuthMethods, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() query, scan := prepareUserAuthMethodsQuery() + query = userAuthMethodPermissionCheckV2(ctx, query, permissionCheckV2) stmt, args, err := queries.toQuery(query).Where(sq.Eq{UserAuthMethodColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()}).ToSql() if err != nil { return nil, zerrors.ThrowInvalidArgument(err, "QUERY-j9NJd", "Errors.Query.InvalidRequest") diff --git a/internal/query/user_grant.go b/internal/query/user_grant.go index ebd4ab7c0c..da38d48821 100644 --- a/internal/query/user_grant.go +++ b/internal/query/user_grant.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "errors" + "slices" "time" sq "github.com/Masterminds/squirrel" @@ -44,8 +45,9 @@ type UserGrant struct { OrgName string `json:"org_name,omitempty"` OrgPrimaryDomain string `json:"org_primary_domain,omitempty"` - ProjectID string `json:"project_id,omitempty"` - ProjectName string `json:"project_name,omitempty"` + ProjectResourceOwner string `json:"project_resource_owner,omitempty"` + ProjectID string `json:"project_id,omitempty"` + ProjectName string `json:"project_name,omitempty"` GrantedOrgID string `json:"granted_org_id,omitempty"` GrantedOrgName string `json:"granted_org_name,omitempty"` @@ -57,6 +59,27 @@ type UserGrants struct { UserGrants []*UserGrant } +func userGrantsCheckPermission(ctx context.Context, grants *UserGrants, permissionCheck domain.PermissionCheck) { + grants.UserGrants = slices.DeleteFunc(grants.UserGrants, + func(grant *UserGrant) bool { + return userGrantCheckPermission(ctx, grant.ResourceOwner, grant.ProjectID, grant.GrantID, grant.UserID, permissionCheck) != nil + }, + ) +} + +func userGrantCheckPermission(ctx context.Context, resourceOwner, projectID, grantID, userID string, permissionCheck domain.PermissionCheck) error { + // you should always be able to read your own permissions + if authz.GetCtxData(ctx).UserID == userID { + return nil + } + // check permission on the project grant + if grantID != "" { + return permissionCheck(ctx, domain.PermissionUserGrantRead, resourceOwner, grantID) + } + // check on project + return permissionCheck(ctx, domain.PermissionUserGrantRead, resourceOwner, projectID) +} + type UserGrantsQueries struct { SearchRequest Queries []SearchQuery @@ -70,6 +93,21 @@ func (q *UserGrantsQueries) toQuery(query sq.SelectBuilder) sq.SelectBuilder { return query } +func userGrantPermissionCheckV2(ctx context.Context, query sq.SelectBuilder, enabled bool, queries *UserGrantsQueries) sq.SelectBuilder { + if !enabled { + return query + } + join, args := PermissionClause( + ctx, + UserGrantResourceOwner, + domain.PermissionUserGrantRead, + SingleOrgPermissionOption(queries.Queries), + WithProjectsPermissionOption(UserGrantProjectID), + OwnedRowsPermissionOption(UserGrantUserID), + ) + return query.JoinClause(join, args...) +} + func NewUserGrantUserIDSearchQuery(id string) (SearchQuery, error) { return NewTextQuery(UserGrantUserID, id, TextEquals) } @@ -78,14 +116,6 @@ func NewUserGrantProjectIDSearchQuery(id string) (SearchQuery, error) { return NewTextQuery(UserGrantProjectID, id, TextEquals) } -func NewUserGrantProjectIDsSearchQuery(ids []string) (SearchQuery, error) { - list := make([]interface{}, len(ids)) - for i, value := range ids { - list[i] = value - } - return NewListQuery(UserGrantProjectID, list, ListIn) -} - func NewUserGrantProjectOwnerSearchQuery(id string) (SearchQuery, error) { return NewTextQuery(ProjectColumnResourceOwner, id, TextEquals) } @@ -94,6 +124,10 @@ func NewUserGrantResourceOwnerSearchQuery(id string) (SearchQuery, error) { return NewTextQuery(UserGrantResourceOwner, id, TextEquals) } +func NewUserGrantUserResourceOwnerSearchQuery(id string) (SearchQuery, error) { + return NewTextQuery(UserResourceOwnerCol, id, TextEquals) +} + func NewUserGrantGrantIDSearchQuery(id string) (SearchQuery, error) { return NewTextQuery(UserGrantGrantID, id, TextEquals) } @@ -102,6 +136,14 @@ func NewUserGrantIDSearchQuery(id string) (SearchQuery, error) { return NewTextQuery(UserGrantID, id, TextEquals) } +func NewUserGrantInIDsSearchQuery(ids []string) (SearchQuery, error) { + list := make([]interface{}, len(ids)) + for i, value := range ids { + list[i] = value + } + return NewListQuery(UserGrantID, list, ListIn) +} + func NewUserGrantUserTypeQuery(typ domain.UserType) (SearchQuery, error) { return NewNumberQuery(UserTypeCol, typ, NumberEquals) } @@ -262,7 +304,20 @@ func (q *Queries) UserGrant(ctx context.Context, shouldTriggerBulk bool, queries return grant, err } -func (q *Queries) UserGrants(ctx context.Context, queries *UserGrantsQueries, shouldTriggerBulk bool) (grants *UserGrants, err error) { +func (q *Queries) UserGrants(ctx context.Context, queries *UserGrantsQueries, shouldTriggerBulk bool, permissionCheck domain.PermissionCheck) (*UserGrants, error) { + // removed as permission v2 is not implemented yet for project grant level permissions + // permissionCheckV2 := PermissionV2(ctx, permissionCheck) + grants, err := q.userGrants(ctx, queries, shouldTriggerBulk, false) + if err != nil { + return nil, err + } + if permissionCheck != nil { // && !authz.GetFeatures(ctx).PermissionCheckV2 { + userGrantsCheckPermission(ctx, grants, permissionCheck) + } + return grants, nil +} + +func (q *Queries) userGrants(ctx context.Context, queries *UserGrantsQueries, shouldTriggerBulk bool, permissionCheckV2 bool) (grants *UserGrants, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -274,6 +329,7 @@ func (q *Queries) UserGrants(ctx context.Context, queries *UserGrantsQueries, sh } query, scan := prepareUserGrantsQuery() + query = userGrantPermissionCheckV2(ctx, query, permissionCheckV2, queries) eq := sq.Eq{UserGrantInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()} stmt, args, err := queries.toQuery(query).Where(eq).ToSql() if err != nil { @@ -324,6 +380,7 @@ func prepareUserGrantQuery() (sq.SelectBuilder, func(*sql.Row) (*UserGrant, erro UserGrantProjectID.identifier(), ProjectColumnName.identifier(), + ProjectColumnResourceOwner.identifier(), GrantedOrgColumnId.identifier(), GrantedOrgColumnName.identifier(), @@ -334,7 +391,8 @@ func prepareUserGrantQuery() (sq.SelectBuilder, func(*sql.Row) (*UserGrant, erro LeftJoin(join(HumanUserIDCol, UserGrantUserID)). LeftJoin(join(OrgColumnID, UserGrantResourceOwner)). LeftJoin(join(ProjectColumnID, UserGrantProjectID)). - LeftJoin(join(GrantedOrgColumnId, UserResourceOwnerCol)). + LeftJoin(join(ProjectGrantColumnGrantID, UserGrantGrantID) + " AND " + ProjectGrantColumnProjectID.identifier() + " = " + UserGrantProjectID.identifier()). + LeftJoin(join(GrantedOrgColumnId, ProjectGrantColumnGrantedOrgID)). LeftJoin(join(LoginNameUserIDCol, UserGrantUserID)). Where( sq.Eq{LoginNameIsPrimaryCol.identifier(): true}, @@ -356,7 +414,8 @@ func prepareUserGrantQuery() (sq.SelectBuilder, func(*sql.Row) (*UserGrant, erro orgName sql.NullString orgDomain sql.NullString - projectName sql.NullString + projectName sql.NullString + projectResourceOwner sql.NullString grantedOrgID sql.NullString grantedOrgName sql.NullString @@ -389,6 +448,7 @@ func prepareUserGrantQuery() (sq.SelectBuilder, func(*sql.Row) (*UserGrant, erro &g.ProjectID, &projectName, + &projectResourceOwner, &grantedOrgID, &grantedOrgName, @@ -413,6 +473,7 @@ func prepareUserGrantQuery() (sq.SelectBuilder, func(*sql.Row) (*UserGrant, erro g.OrgName = orgName.String g.OrgPrimaryDomain = orgDomain.String g.ProjectName = projectName.String + g.ProjectResourceOwner = projectResourceOwner.String g.GrantedOrgID = grantedOrgID.String g.GrantedOrgName = grantedOrgName.String g.GrantedOrgDomain = grantedOrgDomain.String @@ -447,6 +508,7 @@ func prepareUserGrantsQuery() (sq.SelectBuilder, func(*sql.Rows) (*UserGrants, e UserGrantProjectID.identifier(), ProjectColumnName.identifier(), + ProjectColumnResourceOwner.identifier(), GrantedOrgColumnId.identifier(), GrantedOrgColumnName.identifier(), @@ -459,7 +521,8 @@ func prepareUserGrantsQuery() (sq.SelectBuilder, func(*sql.Rows) (*UserGrants, e LeftJoin(join(HumanUserIDCol, UserGrantUserID)). LeftJoin(join(OrgColumnID, UserGrantResourceOwner)). LeftJoin(join(ProjectColumnID, UserGrantProjectID)). - LeftJoin(join(GrantedOrgColumnId, UserResourceOwnerCol)). + LeftJoin(join(ProjectGrantColumnGrantID, UserGrantGrantID) + " AND " + ProjectGrantColumnProjectID.identifier() + " = " + UserGrantProjectID.identifier()). + LeftJoin(join(GrantedOrgColumnId, ProjectGrantColumnGrantedOrgID)). LeftJoin(join(LoginNameUserIDCol, UserGrantUserID)). Where( sq.Eq{LoginNameIsPrimaryCol.identifier(): true}, @@ -488,7 +551,8 @@ func prepareUserGrantsQuery() (sq.SelectBuilder, func(*sql.Rows) (*UserGrants, e grantedOrgName sql.NullString grantedOrgDomain sql.NullString - projectName sql.NullString + projectName sql.NullString + projectResourceOwner sql.NullString ) err := rows.Scan( @@ -517,6 +581,7 @@ func prepareUserGrantsQuery() (sq.SelectBuilder, func(*sql.Rows) (*UserGrants, e &g.ProjectID, &projectName, + &projectResourceOwner, &grantedOrgID, &grantedOrgName, @@ -540,6 +605,7 @@ func prepareUserGrantsQuery() (sq.SelectBuilder, func(*sql.Rows) (*UserGrants, e g.OrgName = orgName.String g.OrgPrimaryDomain = orgDomain.String g.ProjectName = projectName.String + g.ProjectResourceOwner = projectResourceOwner.String g.GrantedOrgID = grantedOrgID.String g.GrantedOrgName = grantedOrgName.String g.GrantedOrgDomain = grantedOrgDomain.String diff --git a/internal/query/user_grant_test.go b/internal/query/user_grant_test.go index 6a640c2ef2..dde04f2c88 100644 --- a/internal/query/user_grant_test.go +++ b/internal/query/user_grant_test.go @@ -37,6 +37,7 @@ var ( ", projections.orgs1.primary_domain" + ", projections.user_grants5.project_id" + ", projections.projects4.name" + + ", projections.projects4.resource_owner" + ", granted_orgs.id" + ", granted_orgs.name" + ", granted_orgs.primary_domain" + @@ -45,7 +46,8 @@ var ( " LEFT JOIN projections.users14_humans ON projections.user_grants5.user_id = projections.users14_humans.user_id AND projections.user_grants5.instance_id = projections.users14_humans.instance_id" + " LEFT JOIN projections.orgs1 ON projections.user_grants5.resource_owner = projections.orgs1.id AND projections.user_grants5.instance_id = projections.orgs1.instance_id" + " LEFT JOIN projections.projects4 ON projections.user_grants5.project_id = projections.projects4.id AND projections.user_grants5.instance_id = projections.projects4.instance_id" + - " LEFT JOIN projections.orgs1 AS granted_orgs ON projections.users14.resource_owner = granted_orgs.id AND projections.users14.instance_id = granted_orgs.instance_id" + + " LEFT JOIN projections.project_grants4 ON projections.user_grants5.grant_id = projections.project_grants4.grant_id AND projections.user_grants5.instance_id = projections.project_grants4.instance_id AND projections.project_grants4.project_id = projections.user_grants5.project_id" + + " LEFT JOIN projections.orgs1 AS granted_orgs ON projections.project_grants4.granted_org_id = granted_orgs.id AND projections.project_grants4.instance_id = granted_orgs.instance_id" + " LEFT JOIN projections.login_names3 ON projections.user_grants5.user_id = projections.login_names3.user_id AND projections.user_grants5.instance_id = projections.login_names3.instance_id" + " WHERE projections.login_names3.is_primary = $1") userGrantCols = []string{ @@ -71,6 +73,7 @@ var ( "primary_domain", "project_id", "name", // project name + "resource_owner", // project_grant resource owner "id", // granted org id "name", // granted org name "primary_domain", // granted org domain @@ -98,6 +101,7 @@ var ( ", projections.orgs1.primary_domain" + ", projections.user_grants5.project_id" + ", projections.projects4.name" + + ", projections.projects4.resource_owner" + ", granted_orgs.id" + ", granted_orgs.name" + ", granted_orgs.primary_domain" + @@ -107,7 +111,8 @@ var ( " LEFT JOIN projections.users14_humans ON projections.user_grants5.user_id = projections.users14_humans.user_id AND projections.user_grants5.instance_id = projections.users14_humans.instance_id" + " LEFT JOIN projections.orgs1 ON projections.user_grants5.resource_owner = projections.orgs1.id AND projections.user_grants5.instance_id = projections.orgs1.instance_id" + " LEFT JOIN projections.projects4 ON projections.user_grants5.project_id = projections.projects4.id AND projections.user_grants5.instance_id = projections.projects4.instance_id" + - " LEFT JOIN projections.orgs1 AS granted_orgs ON projections.users14.resource_owner = granted_orgs.id AND projections.users14.instance_id = granted_orgs.instance_id" + + " LEFT JOIN projections.project_grants4 ON projections.user_grants5.grant_id = projections.project_grants4.grant_id AND projections.user_grants5.instance_id = projections.project_grants4.instance_id AND projections.project_grants4.project_id = projections.user_grants5.project_id" + + " LEFT JOIN projections.orgs1 AS granted_orgs ON projections.project_grants4.granted_org_id = granted_orgs.id AND projections.project_grants4.instance_id = granted_orgs.instance_id" + " LEFT JOIN projections.login_names3 ON projections.user_grants5.user_id = projections.login_names3.user_id AND projections.user_grants5.instance_id = projections.login_names3.instance_id" + " WHERE projections.login_names3.is_primary = $1") userGrantsCols = append( @@ -175,6 +180,7 @@ func Test_UserGrantPrepares(t *testing.T) { "primary-domain", "project-id", "project-name", + "project-resource-owner", "granted-org-id", "granted-org-name", "granted-org-domain", @@ -182,31 +188,32 @@ func Test_UserGrantPrepares(t *testing.T) { ), }, object: &UserGrant{ - ID: "id", - CreationDate: testNow, - ChangeDate: testNow, - Sequence: 20211111, - Roles: database.TextArray[string]{"role-key"}, - GrantID: "grant-id", - State: domain.UserGrantStateActive, - UserID: "user-id", - Username: "username", - UserType: domain.UserTypeHuman, - UserResourceOwner: "resource-owner", - FirstName: "first-name", - LastName: "last-name", - Email: "email", - DisplayName: "display-name", - AvatarURL: "avatar-key", - PreferredLoginName: "login-name", - ResourceOwner: "ro", - OrgName: "org-name", - OrgPrimaryDomain: "primary-domain", - ProjectID: "project-id", - ProjectName: "project-name", - GrantedOrgID: "granted-org-id", - GrantedOrgName: "granted-org-name", - GrantedOrgDomain: "granted-org-domain", + ID: "id", + CreationDate: testNow, + ChangeDate: testNow, + Sequence: 20211111, + Roles: database.TextArray[string]{"role-key"}, + GrantID: "grant-id", + State: domain.UserGrantStateActive, + UserID: "user-id", + Username: "username", + UserType: domain.UserTypeHuman, + UserResourceOwner: "resource-owner", + FirstName: "first-name", + LastName: "last-name", + Email: "email", + DisplayName: "display-name", + AvatarURL: "avatar-key", + PreferredLoginName: "login-name", + ResourceOwner: "ro", + OrgName: "org-name", + OrgPrimaryDomain: "primary-domain", + ProjectID: "project-id", + ProjectName: "project-name", + ProjectResourceOwner: "project-resource-owner", + GrantedOrgID: "granted-org-id", + GrantedOrgName: "granted-org-name", + GrantedOrgDomain: "granted-org-domain", }, }, { @@ -239,6 +246,7 @@ func Test_UserGrantPrepares(t *testing.T) { "primary-domain", "project-id", "project-name", + "project-resource-owner", "granted-org-id", "granted-org-name", "granted-org-domain", @@ -246,31 +254,32 @@ func Test_UserGrantPrepares(t *testing.T) { ), }, object: &UserGrant{ - ID: "id", - CreationDate: testNow, - ChangeDate: testNow, - Sequence: 20211111, - Roles: database.TextArray[string]{"role-key"}, - GrantID: "grant-id", - State: domain.UserGrantStateActive, - UserID: "user-id", - Username: "username", - UserType: domain.UserTypeMachine, - UserResourceOwner: "resource-owner", - FirstName: "", - LastName: "", - Email: "", - DisplayName: "", - AvatarURL: "", - PreferredLoginName: "login-name", - ResourceOwner: "ro", - OrgName: "org-name", - OrgPrimaryDomain: "primary-domain", - ProjectID: "project-id", - ProjectName: "project-name", - GrantedOrgID: "granted-org-id", - GrantedOrgName: "granted-org-name", - GrantedOrgDomain: "granted-org-domain", + ID: "id", + CreationDate: testNow, + ChangeDate: testNow, + Sequence: 20211111, + Roles: database.TextArray[string]{"role-key"}, + GrantID: "grant-id", + State: domain.UserGrantStateActive, + UserID: "user-id", + Username: "username", + UserType: domain.UserTypeMachine, + UserResourceOwner: "resource-owner", + FirstName: "", + LastName: "", + Email: "", + DisplayName: "", + AvatarURL: "", + PreferredLoginName: "login-name", + ResourceOwner: "ro", + OrgName: "org-name", + OrgPrimaryDomain: "primary-domain", + ProjectID: "project-id", + ProjectName: "project-name", + ProjectResourceOwner: "project-resource-owner", + GrantedOrgID: "granted-org-id", + GrantedOrgName: "granted-org-name", + GrantedOrgDomain: "granted-org-domain", }, }, { @@ -303,6 +312,7 @@ func Test_UserGrantPrepares(t *testing.T) { nil, "project-id", "project-name", + "project-resource-owner", "granted-org-id", "granted-org-name", "granted-org-domain", @@ -310,31 +320,32 @@ func Test_UserGrantPrepares(t *testing.T) { ), }, object: &UserGrant{ - ID: "id", - CreationDate: testNow, - ChangeDate: testNow, - Sequence: 20211111, - Roles: database.TextArray[string]{"role-key"}, - GrantID: "grant-id", - State: domain.UserGrantStateActive, - UserID: "user-id", - Username: "username", - UserType: domain.UserTypeHuman, - UserResourceOwner: "resource-owner", - FirstName: "first-name", - LastName: "last-name", - Email: "email", - DisplayName: "display-name", - AvatarURL: "avatar-key", - PreferredLoginName: "login-name", - ResourceOwner: "ro", - OrgName: "", - OrgPrimaryDomain: "", - ProjectID: "project-id", - ProjectName: "project-name", - GrantedOrgID: "granted-org-id", - GrantedOrgName: "granted-org-name", - GrantedOrgDomain: "granted-org-domain", + ID: "id", + CreationDate: testNow, + ChangeDate: testNow, + Sequence: 20211111, + Roles: database.TextArray[string]{"role-key"}, + GrantID: "grant-id", + State: domain.UserGrantStateActive, + UserID: "user-id", + Username: "username", + UserType: domain.UserTypeHuman, + UserResourceOwner: "resource-owner", + FirstName: "first-name", + LastName: "last-name", + Email: "email", + DisplayName: "display-name", + AvatarURL: "avatar-key", + PreferredLoginName: "login-name", + ResourceOwner: "ro", + OrgName: "", + OrgPrimaryDomain: "", + ProjectID: "project-id", + ProjectName: "project-name", + ProjectResourceOwner: "project-resource-owner", + GrantedOrgID: "granted-org-id", + GrantedOrgName: "granted-org-name", + GrantedOrgDomain: "granted-org-domain", }, }, { @@ -367,6 +378,7 @@ func Test_UserGrantPrepares(t *testing.T) { "primary-domain", "project-id", nil, + nil, "granted-org-id", "granted-org-name", "granted-org-domain", @@ -374,31 +386,32 @@ func Test_UserGrantPrepares(t *testing.T) { ), }, object: &UserGrant{ - ID: "id", - CreationDate: testNow, - ChangeDate: testNow, - Sequence: 20211111, - Roles: database.TextArray[string]{"role-key"}, - GrantID: "grant-id", - State: domain.UserGrantStateActive, - UserID: "user-id", - Username: "username", - UserType: domain.UserTypeHuman, - UserResourceOwner: "resource-owner", - FirstName: "first-name", - LastName: "last-name", - Email: "email", - DisplayName: "display-name", - AvatarURL: "avatar-key", - PreferredLoginName: "login-name", - ResourceOwner: "ro", - OrgName: "org-name", - OrgPrimaryDomain: "primary-domain", - ProjectID: "project-id", - ProjectName: "", - GrantedOrgID: "granted-org-id", - GrantedOrgName: "granted-org-name", - GrantedOrgDomain: "granted-org-domain", + ID: "id", + CreationDate: testNow, + ChangeDate: testNow, + Sequence: 20211111, + Roles: database.TextArray[string]{"role-key"}, + GrantID: "grant-id", + State: domain.UserGrantStateActive, + UserID: "user-id", + Username: "username", + UserType: domain.UserTypeHuman, + UserResourceOwner: "resource-owner", + FirstName: "first-name", + LastName: "last-name", + Email: "email", + DisplayName: "display-name", + AvatarURL: "avatar-key", + PreferredLoginName: "login-name", + ResourceOwner: "ro", + OrgName: "org-name", + OrgPrimaryDomain: "primary-domain", + ProjectID: "project-id", + ProjectName: "", + ProjectResourceOwner: "", + GrantedOrgID: "granted-org-id", + GrantedOrgName: "granted-org-name", + GrantedOrgDomain: "granted-org-domain", }, }, { @@ -431,6 +444,7 @@ func Test_UserGrantPrepares(t *testing.T) { "primary-domain", "project-id", "project-name", + "project-resource-owner", "granted-org-id", "granted-org-name", "granted-org-domain", @@ -438,31 +452,32 @@ func Test_UserGrantPrepares(t *testing.T) { ), }, object: &UserGrant{ - ID: "id", - CreationDate: testNow, - ChangeDate: testNow, - Sequence: 20211111, - Roles: database.TextArray[string]{"role-key"}, - GrantID: "grant-id", - State: domain.UserGrantStateActive, - UserID: "user-id", - Username: "username", - UserType: domain.UserTypeHuman, - UserResourceOwner: "resource-owner", - FirstName: "first-name", - LastName: "last-name", - Email: "email", - DisplayName: "display-name", - AvatarURL: "avatar-key", - PreferredLoginName: "", - ResourceOwner: "ro", - OrgName: "org-name", - OrgPrimaryDomain: "primary-domain", - ProjectID: "project-id", - ProjectName: "project-name", - GrantedOrgID: "granted-org-id", - GrantedOrgName: "granted-org-name", - GrantedOrgDomain: "granted-org-domain", + ID: "id", + CreationDate: testNow, + ChangeDate: testNow, + Sequence: 20211111, + Roles: database.TextArray[string]{"role-key"}, + GrantID: "grant-id", + State: domain.UserGrantStateActive, + UserID: "user-id", + Username: "username", + UserType: domain.UserTypeHuman, + UserResourceOwner: "resource-owner", + FirstName: "first-name", + LastName: "last-name", + Email: "email", + DisplayName: "display-name", + AvatarURL: "avatar-key", + PreferredLoginName: "", + ResourceOwner: "ro", + OrgName: "org-name", + OrgPrimaryDomain: "primary-domain", + ProjectID: "project-id", + ProjectName: "project-name", + ProjectResourceOwner: "project-resource-owner", + GrantedOrgID: "granted-org-id", + GrantedOrgName: "granted-org-name", + GrantedOrgDomain: "granted-org-domain", }, }, { @@ -525,6 +540,7 @@ func Test_UserGrantPrepares(t *testing.T) { "primary-domain", "project-id", "project-name", + "project-resource-owner", "granted-org-id", "granted-org-name", "granted-org-domain", @@ -538,31 +554,32 @@ func Test_UserGrantPrepares(t *testing.T) { }, UserGrants: []*UserGrant{ { - ID: "id", - CreationDate: testNow, - ChangeDate: testNow, - Sequence: 20211111, - Roles: database.TextArray[string]{"role-key"}, - GrantID: "grant-id", - State: domain.UserGrantStateActive, - UserID: "user-id", - Username: "username", - UserType: domain.UserTypeHuman, - UserResourceOwner: "resource-owner", - FirstName: "first-name", - LastName: "last-name", - Email: "email", - DisplayName: "display-name", - AvatarURL: "avatar-key", - PreferredLoginName: "login-name", - ResourceOwner: "ro", - OrgName: "org-name", - OrgPrimaryDomain: "primary-domain", - ProjectID: "project-id", - ProjectName: "project-name", - GrantedOrgID: "granted-org-id", - GrantedOrgName: "granted-org-name", - GrantedOrgDomain: "granted-org-domain", + ID: "id", + CreationDate: testNow, + ChangeDate: testNow, + Sequence: 20211111, + Roles: database.TextArray[string]{"role-key"}, + GrantID: "grant-id", + State: domain.UserGrantStateActive, + UserID: "user-id", + Username: "username", + UserType: domain.UserTypeHuman, + UserResourceOwner: "resource-owner", + FirstName: "first-name", + LastName: "last-name", + Email: "email", + DisplayName: "display-name", + AvatarURL: "avatar-key", + PreferredLoginName: "login-name", + ResourceOwner: "ro", + OrgName: "org-name", + OrgPrimaryDomain: "primary-domain", + ProjectID: "project-id", + ProjectName: "project-name", + ProjectResourceOwner: "project-resource-owner", + GrantedOrgID: "granted-org-id", + GrantedOrgName: "granted-org-name", + GrantedOrgDomain: "granted-org-domain", }, }, }, @@ -598,6 +615,7 @@ func Test_UserGrantPrepares(t *testing.T) { "primary-domain", "project-id", "project-name", + "project-resource-owner", "granted-org-id", "granted-org-name", "granted-org-domain", @@ -611,31 +629,32 @@ func Test_UserGrantPrepares(t *testing.T) { }, UserGrants: []*UserGrant{ { - ID: "id", - CreationDate: testNow, - ChangeDate: testNow, - Sequence: 20211111, - Roles: database.TextArray[string]{"role-key"}, - GrantID: "grant-id", - State: domain.UserGrantStateActive, - UserID: "user-id", - Username: "username", - UserType: domain.UserTypeMachine, - UserResourceOwner: "resource-owner", - FirstName: "", - LastName: "", - Email: "", - DisplayName: "", - AvatarURL: "", - PreferredLoginName: "login-name", - ResourceOwner: "ro", - OrgName: "org-name", - OrgPrimaryDomain: "primary-domain", - ProjectID: "project-id", - ProjectName: "project-name", - GrantedOrgID: "granted-org-id", - GrantedOrgName: "granted-org-name", - GrantedOrgDomain: "granted-org-domain", + ID: "id", + CreationDate: testNow, + ChangeDate: testNow, + Sequence: 20211111, + Roles: database.TextArray[string]{"role-key"}, + GrantID: "grant-id", + State: domain.UserGrantStateActive, + UserID: "user-id", + Username: "username", + UserType: domain.UserTypeMachine, + UserResourceOwner: "resource-owner", + FirstName: "", + LastName: "", + Email: "", + DisplayName: "", + AvatarURL: "", + PreferredLoginName: "login-name", + ResourceOwner: "ro", + OrgName: "org-name", + OrgPrimaryDomain: "primary-domain", + ProjectID: "project-id", + ProjectName: "project-name", + ProjectResourceOwner: "project-resource-owner", + GrantedOrgID: "granted-org-id", + GrantedOrgName: "granted-org-name", + GrantedOrgDomain: "granted-org-domain", }, }, }, @@ -671,6 +690,7 @@ func Test_UserGrantPrepares(t *testing.T) { nil, "project-id", "project-name", + "project-resource-owner", "granted-org-id", "granted-org-name", "granted-org-domain", @@ -684,31 +704,32 @@ func Test_UserGrantPrepares(t *testing.T) { }, UserGrants: []*UserGrant{ { - ID: "id", - CreationDate: testNow, - ChangeDate: testNow, - Sequence: 20211111, - Roles: database.TextArray[string]{"role-key"}, - GrantID: "grant-id", - State: domain.UserGrantStateActive, - UserID: "user-id", - Username: "username", - UserType: domain.UserTypeMachine, - UserResourceOwner: "resource-owner", - FirstName: "first-name", - LastName: "last-name", - Email: "email", - DisplayName: "display-name", - AvatarURL: "avatar-key", - PreferredLoginName: "login-name", - ResourceOwner: "ro", - OrgName: "", - OrgPrimaryDomain: "", - ProjectID: "project-id", - ProjectName: "project-name", - GrantedOrgID: "granted-org-id", - GrantedOrgName: "granted-org-name", - GrantedOrgDomain: "granted-org-domain", + ID: "id", + CreationDate: testNow, + ChangeDate: testNow, + Sequence: 20211111, + Roles: database.TextArray[string]{"role-key"}, + GrantID: "grant-id", + State: domain.UserGrantStateActive, + UserID: "user-id", + Username: "username", + UserType: domain.UserTypeMachine, + UserResourceOwner: "resource-owner", + FirstName: "first-name", + LastName: "last-name", + Email: "email", + DisplayName: "display-name", + AvatarURL: "avatar-key", + PreferredLoginName: "login-name", + ResourceOwner: "ro", + OrgName: "", + OrgPrimaryDomain: "", + ProjectID: "project-id", + ProjectName: "project-name", + ProjectResourceOwner: "project-resource-owner", + GrantedOrgID: "granted-org-id", + GrantedOrgName: "granted-org-name", + GrantedOrgDomain: "granted-org-domain", }, }, }, @@ -744,6 +765,7 @@ func Test_UserGrantPrepares(t *testing.T) { "primary-domain", "project-id", nil, + nil, "granted-org-id", "granted-org-name", "granted-org-domain", @@ -757,31 +779,32 @@ func Test_UserGrantPrepares(t *testing.T) { }, UserGrants: []*UserGrant{ { - ID: "id", - CreationDate: testNow, - ChangeDate: testNow, - Sequence: 20211111, - Roles: database.TextArray[string]{"role-key"}, - GrantID: "grant-id", - State: domain.UserGrantStateActive, - UserID: "user-id", - Username: "username", - UserType: domain.UserTypeHuman, - UserResourceOwner: "resource-owner", - FirstName: "first-name", - LastName: "last-name", - Email: "email", - DisplayName: "display-name", - AvatarURL: "avatar-key", - PreferredLoginName: "login-name", - ResourceOwner: "ro", - OrgName: "org-name", - OrgPrimaryDomain: "primary-domain", - ProjectID: "project-id", - ProjectName: "", - GrantedOrgID: "granted-org-id", - GrantedOrgName: "granted-org-name", - GrantedOrgDomain: "granted-org-domain", + ID: "id", + CreationDate: testNow, + ChangeDate: testNow, + Sequence: 20211111, + Roles: database.TextArray[string]{"role-key"}, + GrantID: "grant-id", + State: domain.UserGrantStateActive, + UserID: "user-id", + Username: "username", + UserType: domain.UserTypeHuman, + UserResourceOwner: "resource-owner", + FirstName: "first-name", + LastName: "last-name", + Email: "email", + DisplayName: "display-name", + AvatarURL: "avatar-key", + PreferredLoginName: "login-name", + ResourceOwner: "ro", + OrgName: "org-name", + OrgPrimaryDomain: "primary-domain", + ProjectID: "project-id", + ProjectName: "", + ProjectResourceOwner: "", + GrantedOrgID: "granted-org-id", + GrantedOrgName: "granted-org-name", + GrantedOrgDomain: "granted-org-domain", }, }, }, @@ -817,6 +840,7 @@ func Test_UserGrantPrepares(t *testing.T) { "primary-domain", "project-id", "project-name", + "project-resource-owner", "granted-org-id", "granted-org-name", "granted-org-domain", @@ -830,31 +854,32 @@ func Test_UserGrantPrepares(t *testing.T) { }, UserGrants: []*UserGrant{ { - ID: "id", - CreationDate: testNow, - ChangeDate: testNow, - Sequence: 20211111, - Roles: database.TextArray[string]{"role-key"}, - GrantID: "grant-id", - State: domain.UserGrantStateActive, - UserID: "user-id", - Username: "username", - UserType: domain.UserTypeHuman, - UserResourceOwner: "resource-owner", - FirstName: "first-name", - LastName: "last-name", - Email: "email", - DisplayName: "display-name", - AvatarURL: "avatar-key", - PreferredLoginName: "", - ResourceOwner: "ro", - OrgName: "org-name", - OrgPrimaryDomain: "primary-domain", - ProjectID: "project-id", - ProjectName: "project-name", - GrantedOrgID: "granted-org-id", - GrantedOrgName: "granted-org-name", - GrantedOrgDomain: "granted-org-domain", + ID: "id", + CreationDate: testNow, + ChangeDate: testNow, + Sequence: 20211111, + Roles: database.TextArray[string]{"role-key"}, + GrantID: "grant-id", + State: domain.UserGrantStateActive, + UserID: "user-id", + Username: "username", + UserType: domain.UserTypeHuman, + UserResourceOwner: "resource-owner", + FirstName: "first-name", + LastName: "last-name", + Email: "email", + DisplayName: "display-name", + AvatarURL: "avatar-key", + PreferredLoginName: "", + ResourceOwner: "ro", + OrgName: "org-name", + OrgPrimaryDomain: "primary-domain", + ProjectID: "project-id", + ProjectName: "project-name", + ProjectResourceOwner: "project-resource-owner", + GrantedOrgID: "granted-org-id", + GrantedOrgName: "granted-org-name", + GrantedOrgDomain: "granted-org-domain", }, }, }, @@ -890,6 +915,7 @@ func Test_UserGrantPrepares(t *testing.T) { "primary-domain", "project-id", "project-name", + "project-resource-owner", "granted-org-id", "granted-org-name", "granted-org-domain", @@ -917,6 +943,7 @@ func Test_UserGrantPrepares(t *testing.T) { "primary-domain", "project-id", "project-name", + "project-resource-owner", "granted-org-id", "granted-org-name", "granted-org-domain", @@ -930,58 +957,60 @@ func Test_UserGrantPrepares(t *testing.T) { }, UserGrants: []*UserGrant{ { - ID: "id", - CreationDate: testNow, - ChangeDate: testNow, - Sequence: 20211111, - Roles: database.TextArray[string]{"role-key"}, - GrantID: "grant-id", - State: domain.UserGrantStateActive, - UserID: "user-id", - Username: "username", - UserType: domain.UserTypeHuman, - UserResourceOwner: "resource-owner", - FirstName: "first-name", - LastName: "last-name", - Email: "email", - DisplayName: "display-name", - AvatarURL: "avatar-key", - PreferredLoginName: "login-name", - ResourceOwner: "ro", - OrgName: "org-name", - OrgPrimaryDomain: "primary-domain", - ProjectID: "project-id", - ProjectName: "project-name", - GrantedOrgID: "granted-org-id", - GrantedOrgName: "granted-org-name", - GrantedOrgDomain: "granted-org-domain", + ID: "id", + CreationDate: testNow, + ChangeDate: testNow, + Sequence: 20211111, + Roles: database.TextArray[string]{"role-key"}, + GrantID: "grant-id", + State: domain.UserGrantStateActive, + UserID: "user-id", + Username: "username", + UserType: domain.UserTypeHuman, + UserResourceOwner: "resource-owner", + FirstName: "first-name", + LastName: "last-name", + Email: "email", + DisplayName: "display-name", + AvatarURL: "avatar-key", + PreferredLoginName: "login-name", + ResourceOwner: "ro", + OrgName: "org-name", + OrgPrimaryDomain: "primary-domain", + ProjectID: "project-id", + ProjectName: "project-name", + ProjectResourceOwner: "project-resource-owner", + GrantedOrgID: "granted-org-id", + GrantedOrgName: "granted-org-name", + GrantedOrgDomain: "granted-org-domain", }, { - ID: "id", - CreationDate: testNow, - ChangeDate: testNow, - Sequence: 20211111, - Roles: database.TextArray[string]{"role-key"}, - GrantID: "grant-id", - State: domain.UserGrantStateActive, - UserID: "user-id", - Username: "username", - UserType: domain.UserTypeHuman, - UserResourceOwner: "resource-owner", - FirstName: "first-name", - LastName: "last-name", - Email: "email", - DisplayName: "display-name", - AvatarURL: "avatar-key", - PreferredLoginName: "login-name", - ResourceOwner: "ro", - OrgName: "org-name", - OrgPrimaryDomain: "primary-domain", - ProjectID: "project-id", - ProjectName: "project-name", - GrantedOrgID: "granted-org-id", - GrantedOrgName: "granted-org-name", - GrantedOrgDomain: "granted-org-domain", + ID: "id", + CreationDate: testNow, + ChangeDate: testNow, + Sequence: 20211111, + Roles: database.TextArray[string]{"role-key"}, + GrantID: "grant-id", + State: domain.UserGrantStateActive, + UserID: "user-id", + Username: "username", + UserType: domain.UserTypeHuman, + UserResourceOwner: "resource-owner", + FirstName: "first-name", + LastName: "last-name", + Email: "email", + DisplayName: "display-name", + AvatarURL: "avatar-key", + PreferredLoginName: "login-name", + ResourceOwner: "ro", + OrgName: "org-name", + OrgPrimaryDomain: "primary-domain", + ProjectID: "project-id", + ProjectName: "project-name", + ProjectResourceOwner: "project-resource-owner", + GrantedOrgID: "granted-org-id", + GrantedOrgName: "granted-org-name", + GrantedOrgDomain: "granted-org-domain", }, }, }, diff --git a/internal/query/user_membership.go b/internal/query/user_membership.go index cb7588624f..069210a830 100644 --- a/internal/query/user_membership.go +++ b/internal/query/user_membership.go @@ -63,7 +63,15 @@ type MembershipSearchQuery struct { } func NewMembershipUserIDQuery(userID string) (SearchQuery, error) { - return NewTextQuery(membershipUserID.setTable(membershipAlias), userID, TextEquals) + return NewTextQuery(MembershipUserID.setTable(membershipAlias), userID, TextEquals) +} + +func NewMembershipCreationDateQuery(timestamp time.Time, comparison TimestampComparison) (SearchQuery, error) { + return NewTimestampQuery(MembershipCreationDate.setTable(membershipAlias), timestamp, comparison) +} + +func NewMembershipChangeDateQuery(timestamp time.Time, comparison TimestampComparison) (SearchQuery, error) { + return NewTimestampQuery(MembershipChangeDate.setTable(membershipAlias), timestamp, comparison) } func NewMembershipOrgIDQuery(value string) (SearchQuery, error) { @@ -137,7 +145,7 @@ func (q *Queries) Memberships(ctx context.Context, queries *MembershipSearchQuer wg.Wait() } - query, queryArgs, scan := prepareMembershipsQuery(queries) + query, queryArgs, scan := prepareMembershipsQuery(ctx, queries, false) eq := sq.Eq{membershipInstanceID.identifier(): authz.GetInstance(ctx).InstanceID()} stmt, args, err := queries.toQuery(query).Where(eq).ToSql() if err != nil { @@ -166,7 +174,7 @@ var ( name: "members", instanceIDCol: projection.MemberInstanceID, } - membershipUserID = Column{ + MembershipUserID = Column{ name: projection.MemberUserIDCol, table: membershipAlias, } @@ -174,11 +182,11 @@ var ( name: projection.MemberRolesCol, table: membershipAlias, } - membershipCreationDate = Column{ + MembershipCreationDate = Column{ name: projection.MemberCreationDate, table: membershipAlias, } - membershipChangeDate = Column{ + MembershipChangeDate = Column{ name: projection.MemberChangeDate, table: membershipAlias, } @@ -216,11 +224,11 @@ var ( } ) -func getMembershipFromQuery(queries *MembershipSearchQuery) (string, []interface{}) { - orgMembers, orgMembersArgs := prepareOrgMember(queries) - iamMembers, iamMembersArgs := prepareIAMMember(queries) - projectMembers, projectMembersArgs := prepareProjectMember(queries) - projectGrantMembers, projectGrantMembersArgs := prepareProjectGrantMember(queries) +func getMembershipFromQuery(ctx context.Context, queries *MembershipSearchQuery, permissionV2 bool) (string, []interface{}) { + orgMembers, orgMembersArgs := prepareOrgMember(ctx, queries, permissionV2) + iamMembers, iamMembersArgs := prepareIAMMember(ctx, queries, permissionV2) + projectMembers, projectMembersArgs := prepareProjectMember(ctx, queries, permissionV2) + projectGrantMembers, projectGrantMembersArgs := prepareProjectGrantMember(ctx, queries, permissionV2) args := make([]interface{}, 0) args = append(append(append(append(args, orgMembersArgs...), iamMembersArgs...), projectMembersArgs...), projectGrantMembersArgs...) @@ -236,13 +244,13 @@ func getMembershipFromQuery(queries *MembershipSearchQuery) (string, []interface args } -func prepareMembershipsQuery(queries *MembershipSearchQuery) (sq.SelectBuilder, []interface{}, func(*sql.Rows) (*Memberships, error)) { - query, args := getMembershipFromQuery(queries) +func prepareMembershipsQuery(ctx context.Context, queries *MembershipSearchQuery, permissionV2 bool) (sq.SelectBuilder, []interface{}, func(*sql.Rows) (*Memberships, error)) { + query, args := getMembershipFromQuery(ctx, queries, permissionV2) return sq.Select( - membershipUserID.identifier(), + MembershipUserID.identifier(), membershipRoles.identifier(), - membershipCreationDate.identifier(), - membershipChangeDate.identifier(), + MembershipCreationDate.identifier(), + MembershipChangeDate.identifier(), membershipSequence.identifier(), membershipResourceOwner.identifier(), membershipOrgID.identifier(), @@ -257,7 +265,7 @@ func prepareMembershipsQuery(queries *MembershipSearchQuery) (sq.SelectBuilder, ).From(query). LeftJoin(join(ProjectColumnID, membershipProjectID)). LeftJoin(join(OrgColumnID, membershipOrgID)). - LeftJoin(join(ProjectGrantColumnGrantID, membershipGrantID)). + LeftJoin(join(ProjectGrantColumnGrantID, membershipGrantID) + " AND " + membershipProjectID.identifier() + " = " + ProjectGrantColumnProjectID.identifier()). LeftJoin(join(InstanceColumnID, membershipInstanceID)). PlaceholderFormat(sq.Dollar), args, @@ -340,7 +348,7 @@ func prepareMembershipsQuery(queries *MembershipSearchQuery) (sq.SelectBuilder, } } -func prepareOrgMember(query *MembershipSearchQuery) (string, []interface{}) { +func prepareOrgMember(ctx context.Context, query *MembershipSearchQuery, permissionV2 bool) (string, []interface{}) { builder := sq.Select( OrgMemberUserID.identifier(), OrgMemberRoles.identifier(), @@ -354,6 +362,7 @@ func prepareOrgMember(query *MembershipSearchQuery) (string, []interface{}) { "NULL::TEXT AS "+membershipProjectID.name, "NULL::TEXT AS "+membershipGrantID.name, ).From(orgMemberTable.identifier()) + builder = administratorOrgPermissionCheckV2(ctx, builder, permissionV2) for _, q := range query.Queries { if q.Col().table.name == membershipAlias.name || q.Col().table.name == orgMemberTable.name { @@ -363,7 +372,7 @@ func prepareOrgMember(query *MembershipSearchQuery) (string, []interface{}) { return builder.MustSql() } -func prepareIAMMember(query *MembershipSearchQuery) (string, []interface{}) { +func prepareIAMMember(ctx context.Context, query *MembershipSearchQuery, permissionV2 bool) (string, []interface{}) { builder := sq.Select( InstanceMemberUserID.identifier(), InstanceMemberRoles.identifier(), @@ -377,6 +386,7 @@ func prepareIAMMember(query *MembershipSearchQuery) (string, []interface{}) { "NULL::TEXT AS "+membershipProjectID.name, "NULL::TEXT AS "+membershipGrantID.name, ).From(instanceMemberTable.identifier()) + builder = administratorInstancePermissionCheckV2(ctx, builder, permissionV2) for _, q := range query.Queries { if q.Col().table.name == membershipAlias.name || q.Col().table.name == instanceMemberTable.name { @@ -386,7 +396,7 @@ func prepareIAMMember(query *MembershipSearchQuery) (string, []interface{}) { return builder.MustSql() } -func prepareProjectMember(query *MembershipSearchQuery) (string, []interface{}) { +func prepareProjectMember(ctx context.Context, query *MembershipSearchQuery, permissionV2 bool) (string, []interface{}) { builder := sq.Select( ProjectMemberUserID.identifier(), ProjectMemberRoles.identifier(), @@ -400,6 +410,7 @@ func prepareProjectMember(query *MembershipSearchQuery) (string, []interface{}) ProjectMemberProjectID.identifier(), "NULL::TEXT AS "+membershipGrantID.name, ).From(projectMemberTable.identifier()) + builder = administratorProjectPermissionCheckV2(ctx, builder, permissionV2) for _, q := range query.Queries { if q.Col().table.name == membershipAlias.name || q.Col().table.name == projectMemberTable.name { @@ -410,7 +421,7 @@ func prepareProjectMember(query *MembershipSearchQuery) (string, []interface{}) return builder.MustSql() } -func prepareProjectGrantMember(query *MembershipSearchQuery) (string, []interface{}) { +func prepareProjectGrantMember(ctx context.Context, query *MembershipSearchQuery, permissionV2 bool) (string, []interface{}) { builder := sq.Select( ProjectGrantMemberUserID.identifier(), ProjectGrantMemberRoles.identifier(), @@ -424,6 +435,7 @@ func prepareProjectGrantMember(query *MembershipSearchQuery) (string, []interfac ProjectGrantMemberProjectID.identifier(), ProjectGrantMemberGrantID.identifier(), ).From(projectGrantMemberTable.identifier()) + builder = administratorProjectGrantPermissionCheckV2(ctx, builder, permissionV2) for _, q := range query.Queries { if q.Col().table.name == membershipAlias.name || q.Col().table.name == projectMemberTable.name || q.Col().table.name == projectGrantMemberTable.name { diff --git a/internal/query/user_membership_test.go b/internal/query/user_membership_test.go index b0170182d1..ce48770fd8 100644 --- a/internal/query/user_membership_test.go +++ b/internal/query/user_membership_test.go @@ -1,6 +1,7 @@ package query import ( + "context" "database/sql" "database/sql/driver" "errors" @@ -85,7 +86,7 @@ var ( ") AS members" + " LEFT JOIN projections.projects4 ON members.project_id = projections.projects4.id AND members.instance_id = projections.projects4.instance_id" + " LEFT JOIN projections.orgs1 ON members.org_id = projections.orgs1.id AND members.instance_id = projections.orgs1.instance_id" + - " LEFT JOIN projections.project_grants4 ON members.grant_id = projections.project_grants4.grant_id AND members.instance_id = projections.project_grants4.instance_id" + + " LEFT JOIN projections.project_grants4 ON members.grant_id = projections.project_grants4.grant_id AND members.instance_id = projections.project_grants4.instance_id AND members.project_id = projections.project_grants4.project_id" + " LEFT JOIN projections.instances ON members.instance_id = projections.instances.id") membershipCols = []string{ "user_id", @@ -461,7 +462,7 @@ func Test_MembershipPrepares(t *testing.T) { func prepareMembershipWrapper() func() (sq.SelectBuilder, func(*sql.Rows) (*Memberships, error)) { return func() (sq.SelectBuilder, func(*sql.Rows) (*Memberships, error)) { - builder, _, fun := prepareMembershipsQuery(&MembershipSearchQuery{}) + builder, _, fun := prepareMembershipsQuery(context.Background(), &MembershipSearchQuery{}, false) return builder, fun } } diff --git a/internal/query/user_metadata.go b/internal/query/user_metadata.go index 534c707593..385c176e0a 100644 --- a/internal/query/user_metadata.go +++ b/internal/query/user_metadata.go @@ -4,12 +4,14 @@ import ( "context" "database/sql" "errors" + "slices" "time" sq "github.com/Masterminds/squirrel" "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/telemetry/tracing" @@ -36,6 +38,28 @@ type UserMetadataSearchQueries struct { Queries []SearchQuery } +func userMetadataCheckPermission(ctx context.Context, userMetadataList *UserMetadataList, permissionCheck domain.PermissionCheck) { + userMetadataList.Metadata = slices.DeleteFunc(userMetadataList.Metadata, + func(userMetadata *UserMetadata) bool { + return userCheckPermission(ctx, userMetadata.ResourceOwner, userMetadata.UserID, permissionCheck) != nil + }, + ) +} + +func userMetadataPermissionCheckV2(ctx context.Context, query sq.SelectBuilder, enabled bool, queries *UserMetadataSearchQueries) sq.SelectBuilder { + if !enabled { + return query + } + join, args := PermissionClause( + ctx, + UserMetadataResourceOwnerCol, + domain.PermissionUserRead, + SingleOrgPermissionOption(queries.Queries), + OwnedRowsPermissionOption(UserMetadataUserIDCol), + ) + return query.JoinClause(join, args...) +} + var ( userMetadataTable = table{ name: projection.UserMetadataProjectionTable, @@ -139,7 +163,19 @@ func (q *Queries) SearchUserMetadataForUsers(ctx context.Context, shouldTriggerB return metadata, err } -func (q *Queries) SearchUserMetadata(ctx context.Context, shouldTriggerBulk bool, userID string, queries *UserMetadataSearchQueries, withOwnerRemoved bool) (metadata *UserMetadataList, err error) { +func (q *Queries) SearchUserMetadata(ctx context.Context, shouldTriggerBulk bool, userID string, queries *UserMetadataSearchQueries, permissionCheck domain.PermissionCheck) (metadata *UserMetadataList, err error) { + permissionCheckV2 := PermissionV2(ctx, permissionCheck) + users, err := q.searchUserMetadata(ctx, shouldTriggerBulk, userID, queries, permissionCheckV2) + if err != nil { + return nil, err + } + if permissionCheck != nil && !authz.GetFeatures(ctx).PermissionCheckV2 { + userMetadataCheckPermission(ctx, users, permissionCheck) + } + return users, nil +} + +func (q *Queries) searchUserMetadata(ctx context.Context, shouldTriggerBulk bool, userID string, queries *UserMetadataSearchQueries, permissionCheckV2 bool) (metadata *UserMetadataList, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -151,6 +187,7 @@ func (q *Queries) SearchUserMetadata(ctx context.Context, shouldTriggerBulk bool } query, scan := prepareUserMetadataListQuery() + query = userMetadataPermissionCheckV2(ctx, query, permissionCheckV2, queries) eq := sq.Eq{ UserMetadataUserIDCol.identifier(): userID, UserMetadataInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(), diff --git a/internal/query/user_personal_access_token.go b/internal/query/user_personal_access_token.go index 2b9aa44dc3..49281d9f90 100644 --- a/internal/query/user_personal_access_token.go +++ b/internal/query/user_personal_access_token.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "errors" + "slices" "time" sq "github.com/Masterminds/squirrel" @@ -11,12 +12,21 @@ import ( "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore/handler/v2" "github.com/zitadel/zitadel/internal/query/projection" "github.com/zitadel/zitadel/internal/telemetry/tracing" "github.com/zitadel/zitadel/internal/zerrors" ) +func patsCheckPermission(ctx context.Context, tokens *PersonalAccessTokens, permissionCheck domain.PermissionCheck) { + tokens.PersonalAccessTokens = slices.DeleteFunc(tokens.PersonalAccessTokens, + func(token *PersonalAccessToken) bool { + return userCheckPermission(ctx, token.ResourceOwner, token.UserID, permissionCheck) != nil + }, + ) +} + var ( personalAccessTokensTable = table{ name: projection.PersonalAccessTokenProjectionTable, @@ -86,7 +96,7 @@ type PersonalAccessTokenSearchQueries struct { Queries []SearchQuery } -func (q *Queries) PersonalAccessTokenByID(ctx context.Context, shouldTriggerBulk bool, id string, withOwnerRemoved bool, queries ...SearchQuery) (pat *PersonalAccessToken, err error) { +func (q *Queries) PersonalAccessTokenByID(ctx context.Context, shouldTriggerBulk bool, id string, queries ...SearchQuery) (pat *PersonalAccessToken, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -102,11 +112,9 @@ func (q *Queries) PersonalAccessTokenByID(ctx context.Context, shouldTriggerBulk query = q.toQuery(query) } eq := sq.Eq{ - PersonalAccessTokenColumnID.identifier(): id, - PersonalAccessTokenColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), - } - if !withOwnerRemoved { - eq[PersonalAccessTokenColumnOwnerRemoved.identifier()] = false + PersonalAccessTokenColumnID.identifier(): id, + PersonalAccessTokenColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), + PersonalAccessTokenColumnOwnerRemoved.identifier(): false, } stmt, args, err := query.Where(eq).ToSql() if err != nil { @@ -123,18 +131,34 @@ func (q *Queries) PersonalAccessTokenByID(ctx context.Context, shouldTriggerBulk return pat, nil } -func (q *Queries) SearchPersonalAccessTokens(ctx context.Context, queries *PersonalAccessTokenSearchQueries, withOwnerRemoved bool) (personalAccessTokens *PersonalAccessTokens, err error) { +// SearchPersonalAccessTokens returns personal access token resources. +// If permissionCheck is nil, the PATs are not filtered. +// If permissionCheck is not nil and the PermissionCheckV2 feature flag is false, the returned PATs are filtered in-memory by the given permission check. +// If permissionCheck is not nil and the PermissionCheckV2 feature flag is true, the returned PATs are filtered in the database. +func (q *Queries) SearchPersonalAccessTokens(ctx context.Context, queries *PersonalAccessTokenSearchQueries, permissionCheck domain.PermissionCheck) (authNKeys *PersonalAccessTokens, err error) { + permissionCheckV2 := PermissionV2(ctx, permissionCheck) + keys, err := q.searchPersonalAccessTokens(ctx, queries, permissionCheckV2) + if err != nil { + return nil, err + } + if permissionCheck != nil && !authz.GetFeatures(ctx).PermissionCheckV2 { + patsCheckPermission(ctx, keys, permissionCheck) + } + return keys, nil +} + +func (q *Queries) searchPersonalAccessTokens(ctx context.Context, queries *PersonalAccessTokenSearchQueries, permissionCheckV2 bool) (personalAccessTokens *PersonalAccessTokens, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() query, scan := preparePersonalAccessTokensQuery() + query = queries.toQuery(query) + query = userPermissionCheckV2WithCustomColumns(ctx, query, permissionCheckV2, queries.Queries, PersonalAccessTokenColumnResourceOwner, PersonalAccessTokenColumnUserID) eq := sq.Eq{ - PersonalAccessTokenColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), + PersonalAccessTokenColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), + PersonalAccessTokenColumnOwnerRemoved.identifier(): false, } - if !withOwnerRemoved { - eq[PersonalAccessTokenColumnOwnerRemoved.identifier()] = false - } - stmt, args, err := queries.toQuery(query).Where(eq).ToSql() + stmt, args, err := query.Where(eq).ToSql() if err != nil { return nil, zerrors.ThrowInvalidArgument(err, "QUERY-Hjw2w", "Errors.Query.InvalidRequest") } @@ -160,6 +184,18 @@ func NewPersonalAccessTokenUserIDSearchQuery(value string) (SearchQuery, error) return NewTextQuery(PersonalAccessTokenColumnUserID, value, TextEquals) } +func NewPersonalAccessTokenIDQuery(id string) (SearchQuery, error) { + return NewTextQuery(PersonalAccessTokenColumnID, id, TextEquals) +} + +func NewPersonalAccessTokenCreationDateQuery(ts time.Time, compare TimestampComparison) (SearchQuery, error) { + return NewTimestampQuery(PersonalAccessTokenColumnCreationDate, ts, compare) +} + +func NewPersonalAccessTokenExpirationDateDateQuery(ts time.Time, compare TimestampComparison) (SearchQuery, error) { + return NewTimestampQuery(PersonalAccessTokenColumnExpiration, ts, compare) +} + func (r *PersonalAccessTokenSearchQueries) AppendMyResourceOwnerQuery(orgID string) error { query, err := NewPersonalAccessTokenResourceOwnerSearchQuery(orgID) if err != nil { diff --git a/internal/query/userinfo.go b/internal/query/userinfo.go index 0e749f09b3..aa2920dfba 100644 --- a/internal/query/userinfo.go +++ b/internal/query/userinfo.go @@ -31,12 +31,6 @@ var oidcUserInfoTriggerHandlers = sync.OnceValue(func() []*handler.Handler { } }) -// TriggerOIDCUserInfoProjections triggers all projections -// relevant to userinfo queries concurrently. -func TriggerOIDCUserInfoProjections(ctx context.Context) { - triggerBatch(ctx, oidcUserInfoTriggerHandlers()...) -} - var ( //go:embed userinfo_by_id.sql oidcUserInfoQueryTmpl string diff --git a/internal/query/v2-default.json b/internal/query/v2-default.json new file mode 100644 index 0000000000..f5d3be6fd7 --- /dev/null +++ b/internal/query/v2-default.json @@ -0,0 +1,1779 @@ +{ + "de":{ + "common": { + "back": "Zurück" + }, + "accounts": { + "title": "Konten", + "description": "Wählen Sie das Konto aus, das Sie verwenden möchten.", + "addAnother": "Ein weiteres Konto hinzufügen", + "noResults": "Keine Konten gefunden" + }, + "loginname": { + "title": "Willkommen zurück!", + "description": "Geben Sie Ihre Anmeldedaten ein.", + "register": "Neuen Benutzer registrieren" + }, + "password": { + "verify": { + "title": "Passwort", + "description": "Geben Sie Ihr Passwort ein.", + "resetPassword": "Passwort zurücksetzen", + "submit": "Weiter" + }, + "set": { + "title": "Passwort festlegen", + "description": "Legen Sie das Passwort für Ihr Konto fest", + "codeSent": "Ein Code wurde an Ihre E-Mail-Adresse gesendet.", + "noCodeReceived": "Keinen Code erhalten?", + "resend": "Erneut senden", + "submit": "Weiter" + }, + "change": { + "title": "Passwort ändern", + "description": "Legen Sie das Passwort für Ihr Konto fest", + "submit": "Weiter" + } + }, + "idp": { + "title": "Mit SSO anmelden", + "description": "Wählen Sie einen der folgenden Anbieter, um sich anzumelden", + "signInWithApple": "Mit Apple anmelden", + "signInWithGoogle": "Mit Google anmelden", + "signInWithAzureAD": "Mit AzureAD anmelden", + "signInWithGithub": "Mit GitHub anmelden", + "signInWithGitlab": "Mit GitLab anmelden", + "loginSuccess": { + "title": "Anmeldung erfolgreich", + "description": "Sie haben sich erfolgreich angemeldet!" + }, + "linkingSuccess": { + "title": "Konto verknüpft", + "description": "Sie haben Ihr Konto erfolgreich verknüpft!" + }, + "registerSuccess": { + "title": "Registrierung erfolgreich", + "description": "Sie haben sich erfolgreich registriert!" + }, + "loginError": { + "title": "Anmeldung fehlgeschlagen", + "description": "Beim Anmelden ist ein Fehler aufgetreten." + }, + "linkingError": { + "title": "Konto-Verknüpfung fehlgeschlagen", + "description": "Beim Verknüpfen Ihres Kontos ist ein Fehler aufgetreten." + } + }, + "mfa": { + "verify": { + "title": "Bestätigen Sie Ihre Identität", + "description": "Wählen Sie einen der folgenden Faktoren.", + "noResults": "Keine zweiten Faktoren verfügbar, um sie einzurichten." + }, + "set": { + "title": "2-Faktor einrichten", + "description": "Wählen Sie einen der folgenden zweiten Faktoren.", + "skip": "Überspringen" + } + }, + "otp": { + "verify": { + "title": "2-Faktor bestätigen", + "totpDescription": "Geben Sie den Code aus Ihrer Authentifizierungs-App ein.", + "smsDescription": "Geben Sie den Code ein, den Sie per SMS erhalten haben.", + "emailDescription": "Geben Sie den Code ein, den Sie per E-Mail erhalten haben.", + "noCodeReceived": "Keinen Code erhalten?", + "resendCode": "Code erneut senden", + "submit": "Weiter" + }, + "set": { + "title": "2-Faktor einrichten", + "totpDescription": "Scannen Sie den QR-Code mit Ihrer Authentifizierungs-App.", + "smsDescription": "Geben Sie Ihre Telefonnummer ein, um einen Code per SMS zu erhalten.", + "emailDescription": "Geben Sie Ihre E-Mail-Adresse ein, um einen Code per E-Mail zu erhalten.", + "totpRegisterDescription": "Scannen Sie den QR-Code oder navigieren Sie manuell zur URL.", + "submit": "Weiter" + } + }, + "passkey": { + "verify": { + "title": "Mit einem Passkey authentifizieren", + "description": "Ihr Gerät wird nach Ihrem Fingerabdruck, Gesicht oder Bildschirmsperre fragen", + "usePassword": "Passwort verwenden", + "submit": "Weiter" + }, + "set": { + "title": "Passkey einrichten", + "description": "Ihr Gerät wird nach Ihrem Fingerabdruck, Gesicht oder Bildschirmsperre fragen", + "info": { + "description": "Ein Passkey ist eine Authentifizierungsmethode auf einem Gerät wie Ihr Fingerabdruck, Apple FaceID oder ähnliches.", + "link": "Passwortlose Authentifizierung" + }, + "skip": "Überspringen", + "submit": "Weiter" + } + }, + "u2f": { + "verify": { + "title": "2-Faktor bestätigen", + "description": "Bestätigen Sie Ihr Konto mit Ihrem Gerät." + }, + "set": { + "title": "2-Faktor einrichten", + "description": "Richten Sie ein Gerät als zweiten Faktor ein.", + "submit": "Weiter" + } + }, + "register": { + "methods": { + "passkey": "Passkey", + "password": "Password" + }, + "disabled": { + "title": "Registrierung deaktiviert", + "description": "Die Registrierung ist deaktiviert. Bitte wenden Sie sich an den Administrator." + }, + "missingdata": { + "title": "Registrierung fehlgeschlagen", + "description": "Einige Daten fehlen. Bitte überprüfen Sie Ihre Eingaben." + }, + "title": "Registrieren", + "description": "Erstellen Sie Ihr ZITADEL-Konto.", + "selectMethod": "Wählen Sie die Methode, mit der Sie sich authentifizieren möchten", + "agreeTo": "Um sich zu registrieren, müssen Sie den Nutzungsbedingungen zustimmen", + "termsOfService": "Nutzungsbedingungen", + "privacyPolicy": "Datenschutzrichtlinie", + "submit": "Weiter", + "password": { + "title": "Passwort festlegen", + "description": "Legen Sie das Passwort für Ihr Konto fest", + "submit": "Weiter" + } + }, + "invite": { + "title": "Benutzer einladen", + "description": "Geben Sie die E-Mail-Adresse des Benutzers ein, den Sie einladen möchten.", + "info": "Der Benutzer erhält eine E-Mail mit einem Link, um sich zu registrieren.", + "notAllowed": "Sie haben keine Berechtigung, Benutzer einzuladen.", + "submit": "Einladen", + "success": { + "title": "Einladung erfolgreich", + "description": "Der Benutzer wurde erfolgreich eingeladen.", + "verified": "Der Benutzer wurde eingeladen und hat seine E-Mail bereits verifiziert.", + "notVerifiedYet": "Der Benutzer wurde eingeladen. Er erhält eine E-Mail mit weiteren Anweisungen.", + "submit": "Weiteren Benutzer einladen" + } + }, + "signedin": { + "title": "Willkommen {user}!", + "description": "Sie sind angemeldet.", + "continue": "Weiter", + "error": { + "title": "Fehler", + "description": "Ein Fehler ist aufgetreten." + } + }, + "verify": { + "userIdMissing": "Keine Benutzer-ID angegeben!", + "success": "Erfolgreich verifiziert", + "setupAuthenticator": "Authentifikator einrichten", + "verify": { + "title": "Benutzer verifizieren", + "description": "Geben Sie den Code ein, der in der Bestätigungs-E-Mail angegeben ist.", + "noCodeReceived": "Keinen Code erhalten?", + "resendCode": "Code erneut senden", + "submit": "Weiter" + } + }, + "authenticator": { + "title": "Authentifizierungsmethode auswählen", + "description": "Wählen Sie die Methode, mit der Sie sich authentifizieren möchten.", + "noMethodsAvailable": "Keine Authentifizierungsmethoden verfügbar", + "allSetup": "Sie haben bereits einen Authentifikator eingerichtet!", + "linkWithIDP": "oder verknüpfe mit einem Identitätsanbieter" + }, + "device": { + "usercode": { + "title": "Gerätecode", + "description": "Geben Sie den Code ein.", + "submit": "Weiter" + }, + "request": { + "title": "{appName} möchte eine Verbindung herstellen:", + "disclaimer": "{appName} hat Zugriff auf:", + "description": "Durch Klicken auf Zulassen erlauben Sie {appName} und Zitadel, Ihre Informationen gemäß ihren jeweiligen Nutzungsbedingungen und Datenschutzrichtlinien zu verwenden. Sie können diesen Zugriff jederzeit widerrufen.", + "submit": "Zulassen", + "deny": "Ablehnen" + }, + "scope": { + "openid": "Überprüfen Ihrer Identität.", + "email": "Zugriff auf Ihre E-Mail-Adresse.", + "profile": "Zugriff auf Ihre vollständigen Profilinformationen.", + "offline_access": "Erlauben Sie den Offline-Zugriff auf Ihr Konto." + } + }, + "error": { + "noUserCode": "Kein Benutzercode angegeben!", + "noDeviceRequest": " Es wurde keine Geräteanforderung gefunden. Bitte überprüfen Sie die URL.", + "unknownContext": "Der Kontext des Benutzers konnte nicht ermittelt werden. Stellen Sie sicher, dass Sie zuerst den Benutzernamen eingeben oder einen loginName als Suchparameter angeben.", + "sessionExpired": "Ihre aktuelle Sitzung ist abgelaufen. Bitte melden Sie sich erneut an.", + "failedLoading": "Daten konnten nicht geladen werden. Bitte versuchen Sie es erneut.", + "tryagain": "Erneut versuchen" + } + }, + "en":{ + "common": { + "back": "Back" + }, + "accounts": { + "title": "Accounts", + "description": "Select the account you want to use.", + "addAnother": "Add another account", + "noResults": "No accounts found" + }, + "loginname": { + "title": "Welcome back!", + "description": "Enter your login data.", + "register": "Register new user" + }, + "password": { + "verify": { + "title": "Password", + "description": "Enter your password.", + "resetPassword": "Reset Password", + "submit": "Continue" + }, + "set": { + "title": "Set Password", + "description": "Set the password for your account", + "codeSent": "A code has been sent to your email address.", + "noCodeReceived": "Didn't receive a code?", + "resend": "Resend code", + "submit": "Continue" + }, + "change": { + "title": "Change Password", + "description": "Set the password for your account", + "submit": "Continue" + } + }, + "idp": { + "title": "Sign in with SSO", + "description": "Select one of the following providers to sign in", + "signInWithApple": "Sign in with Apple", + "signInWithGoogle": "Sign in with Google", + "signInWithAzureAD": "Sign in with AzureAD", + "signInWithGithub": "Sign in with GitHub", + "signInWithGitlab": "Sign in with GitLab", + "loginSuccess": { + "title": "Login successful", + "description": "You have successfully been loggedIn!" + }, + "linkingSuccess": { + "title": "Account linked", + "description": "You have successfully linked your account!" + }, + "registerSuccess": { + "title": "Registration successful", + "description": "You have successfully registered!" + }, + "loginError": { + "title": "Login failed", + "description": "An error occurred while trying to login." + }, + "linkingError": { + "title": "Account linking failed", + "description": "An error occurred while trying to link your account." + } + }, + "mfa": { + "verify": { + "title": "Verify your identity", + "description": "Choose one of the following factors.", + "noResults": "No second factors available to setup." + }, + "set": { + "title": "Set up 2-Factor", + "description": "Choose one of the following second factors.", + "skip": "Skip" + } + }, + "otp": { + "verify": { + "title": "Verify 2-Factor", + "totpDescription": "Enter the code from your authenticator app.", + "smsDescription": "Enter the code you received via SMS.", + "emailDescription": "Enter the code you received via email.", + "noCodeReceived": "Didn't receive a code?", + "resendCode": "Resend code", + "submit": "Continue" + }, + "set": { + "title": "Set up 2-Factor", + "totpDescription": "Scan the QR code with your authenticator app.", + "smsDescription": "Enter your phone number to receive a code via SMS.", + "emailDescription": "Enter your email address to receive a code via email.", + "totpRegisterDescription": "Scan the QR Code or navigate to the URL manually.", + "submit": "Continue" + } + }, + "passkey": { + "verify": { + "title": "Authenticate with a passkey", + "description": "Your device will ask for your fingerprint, face, or screen lock", + "usePassword": "Use password", + "submit": "Continue" + }, + "set": { + "title": "Setup a passkey", + "description": "Your device will ask for your fingerprint, face, or screen lock", + "info": { + "description": "A passkey is an authentication method on a device like your fingerprint, Apple FaceID or similar. ", + "link": "Passwordless Authentication" + }, + "skip": "Skip", + "submit": "Continue" + } + }, + "u2f": { + "verify": { + "title": "Verify 2-Factor", + "description": "Verify your account with your device." + }, + "set": { + "title": "Set up 2-Factor", + "description": "Set up a device as a second factor.", + "submit": "Continue" + } + }, + "register": { + "methods": { + "passkey": "Passkey", + "password": "Password" + }, + "disabled": { + "title": "Registration disabled", + "description": "The registration is disabled. Please contact your administrator." + }, + "missingdata": { + "title": "Missing data", + "description": "Provide email, first and last name to register." + }, + "title": "Register", + "description": "Create your ZITADEL account.", + "selectMethod": "Select the method you would like to authenticate", + "agreeTo": "To register you must agree to the terms and conditions", + "termsOfService": "Terms of Service", + "privacyPolicy": "Privacy Policy", + "submit": "Continue", + "password": { + "title": "Set Password", + "description": "Set the password for your account", + "submit": "Continue" + } + }, + "invite": { + "title": "Invite User", + "description": "Provide the email address and the name of the user you want to invite.", + "info": "The user will receive an email with further instructions.", + "notAllowed": "Your settings do not allow you to invite users.", + "submit": "Continue", + "success": { + "title": "User invited", + "description": "The email has successfully been sent.", + "verified": "The user has been invited and has already verified his email.", + "notVerifiedYet": "The user has been invited. They will receive an email with further instructions.", + "submit": "Invite another user" + } + }, + "signedin": { + "title": "Welcome {user}!", + "description": "You are signed in.", + "continue": "Continue", + "error": { + "title": "Error", + "description": "An error occurred while trying to sign in." + } + }, + "verify": { + "userIdMissing": "No userId provided!", + "success": "The user has been verified successfully.", + "setupAuthenticator": "Setup authenticator", + "verify": { + "title": "Verify user", + "description": "Enter the Code provided in the verification email.", + "noCodeReceived": "Didn't receive a code?", + "resendCode": "Resend code", + "submit": "Continue" + } + }, + "authenticator": { + "title": "Choose authentication method", + "description": "Select the method you would like to authenticate", + "noMethodsAvailable": "No authentication methods available", + "allSetup": "You have already setup an authenticator!", + "linkWithIDP": "or link with an Identity Provider" + }, + "device": { + "usercode": { + "title": "Device code", + "description": "Enter the code displayed on your app or device.", + "submit": "Continue" + }, + "request": { + "title": "{appName} would like to connect", + "description": "{appName} will have access to:", + "disclaimer": "By clicking Allow, you allow {appName} and Zitadel to use your information in accordance with their respective terms of service and privacy policies. You can revoke this access at any time.", + "submit": "Allow", + "deny": "Deny" + }, + "scope": { + "openid": "Verify your identity.", + "email": "View your email address.", + "profile": "View your full profile information.", + "offline_access": "Allow offline access to your account." + } + }, + "error": { + "noUserCode": "No user code provided!", + "noDeviceRequest": "No device request found.", + "unknownContext": "Could not get the context of the user. Make sure to enter the username first or provide a loginName as searchParam.", + "sessionExpired": "Your current session has expired. Please login again.", + "failedLoading": "Failed to load data. Please try again.", + "tryagain": "Try Again" + } + }, + "es":{ + "common": { + "back": "Atrás" + }, + "accounts": { + "title": "Cuentas", + "description": "Selecciona la cuenta que deseas usar.", + "addAnother": "Agregar otra cuenta", + "noResults": "No se encontraron cuentas" + }, + "loginname": { + "title": "¡Bienvenido de nuevo!", + "description": "Introduce tus datos de acceso.", + "register": "Registrar nuevo usuario" + }, + "password": { + "verify": { + "title": "Contraseña", + "description": "Introduce tu contraseña.", + "resetPassword": "Restablecer contraseña", + "submit": "Continuar" + }, + "set": { + "title": "Establecer Contraseña", + "description": "Establece la contraseña para tu cuenta", + "codeSent": "Se ha enviado un código a su correo electrónico.", + "noCodeReceived": "¿No recibiste un código?", + "resend": "Reenviar código", + "submit": "Continuar" + }, + "change": { + "title": "Cambiar Contraseña", + "description": "Establece la contraseña para tu cuenta", + "submit": "Continuar" + } + }, + "idp": { + "title": "Iniciar sesión con SSO", + "description": "Selecciona uno de los siguientes proveedores para iniciar sesión", + "signInWithApple": "Iniciar sesión con Apple", + "signInWithGoogle": "Iniciar sesión con Google", + "signInWithAzureAD": "Iniciar sesión con AzureAD", + "signInWithGithub": "Iniciar sesión con GitHub", + "signInWithGitlab": "Iniciar sesión con GitLab", + "loginSuccess": { + "title": "Inicio de sesión exitoso", + "description": "¡Has iniciado sesión con éxito!" + }, + "linkingSuccess": { + "title": "Cuenta vinculada", + "description": "¡Has vinculado tu cuenta con éxito!" + }, + "registerSuccess": { + "title": "Registro exitoso", + "description": "¡Te has registrado con éxito!" + }, + "loginError": { + "title": "Error de inicio de sesión", + "description": "Ocurrió un error al intentar iniciar sesión." + }, + "linkingError": { + "title": "Error al vincular la cuenta", + "description": "Ocurrió un error al intentar vincular tu cuenta." + } + }, + "mfa": { + "verify": { + "title": "Verifica tu identidad", + "description": "Elige uno de los siguientes factores.", + "noResults": "No hay factores secundarios disponibles para configurar." + }, + "set": { + "title": "Configurar autenticación de 2 factores", + "description": "Elige uno de los siguientes factores secundarios.", + "skip": "Omitir" + } + }, + "otp": { + "verify": { + "title": "Verificar autenticación de 2 factores", + "totpDescription": "Introduce el código de tu aplicación de autenticación.", + "smsDescription": "Introduce el código que recibiste por SMS.", + "emailDescription": "Introduce el código que recibiste por correo electrónico.", + "noCodeReceived": "¿No recibiste un código?", + "resendCode": "Reenviar código", + "submit": "Continuar" + }, + "set": { + "title": "Configurar autenticación de 2 factores", + "totpDescription": "Escanea el código QR con tu aplicación de autenticación.", + "smsDescription": "Introduce tu número de teléfono para recibir un código por SMS.", + "emailDescription": "Introduce tu dirección de correo electrónico para recibir un código por correo electrónico.", + "totpRegisterDescription": "Escanea el código QR o navega manualmente a la URL.", + "submit": "Continuar" + } + }, + "passkey": { + "verify": { + "title": "Autenticar con una clave de acceso", + "description": "Tu dispositivo pedirá tu huella digital, rostro o bloqueo de pantalla", + "usePassword": "Usar contraseña", + "submit": "Continuar" + }, + "set": { + "title": "Configurar una clave de acceso", + "description": "Tu dispositivo pedirá tu huella digital, rostro o bloqueo de pantalla", + "info": { + "description": "Una clave de acceso es un método de autenticación en un dispositivo como tu huella digital, Apple FaceID o similar.", + "link": "Autenticación sin contraseña" + }, + "skip": "Omitir", + "submit": "Continuar" + } + }, + "u2f": { + "verify": { + "title": "Verificar autenticación de 2 factores", + "description": "Verifica tu cuenta con tu dispositivo." + }, + "set": { + "title": "Configurar autenticación de 2 factores", + "description": "Configura un dispositivo como segundo factor.", + "submit": "Continuar" + } + }, + "register": { + "methods": { + "passkey": "Clave de acceso", + "password": "Contraseña" + }, + "disabled": { + "title": "Registro deshabilitado", + "description": "Registrarse está deshabilitado en este momento." + }, + "missingdata": { + "title": "Datos faltantes", + "description": "No se proporcionaron datos suficientes para el registro." + }, + "title": "Registrarse", + "description": "Crea tu cuenta ZITADEL.", + "selectMethod": "Selecciona el método con el que deseas autenticarte", + "agreeTo": "Para registrarte debes aceptar los términos y condiciones", + "termsOfService": "Términos de Servicio", + "privacyPolicy": "Política de Privacidad", + "submit": "Continuar", + "password": { + "title": "Establecer Contraseña", + "description": "Establece la contraseña para tu cuenta", + "submit": "Continuar" + } + }, + "invite": { + "title": "Invitar usuario", + "description": "Introduce el correo electrónico del usuario que deseas invitar.", + "info": "El usuario recibirá un correo electrónico con un enlace para completar el registro.", + "notAllowed": "No tienes permiso para invitar usuarios.", + "submit": "Invitar usuario", + "success": { + "title": "¡Usuario invitado!", + "description": "El usuario ha sido invitado.", + "verified": "El usuario ha sido invitado y ya ha verificado su correo electrónico.", + "notVerifiedYet": "El usuario ha sido invitado. Recibirá un correo electrónico con más instrucciones.", + "submit": "Invitar a otro usuario" + } + }, + "signedin": { + "title": "¡Bienvenido {user}!", + "description": "Has iniciado sesión.", + "continue": "Continuar", + "error": { + "title": "Error", + "description": "Ocurrió un error al iniciar sesión." + } + }, + "verify": { + "userIdMissing": "¡No se proporcionó userId!", + "success": "¡Verificación exitosa!", + "setupAuthenticator": "Configurar autenticador", + "verify": { + "title": "Verificar usuario", + "description": "Introduce el código proporcionado en el correo electrónico de verificación.", + "noCodeReceived": "¿No recibiste un código?", + "resendCode": "Reenviar código", + "submit": "Continuar" + } + }, + "authenticator": { + "title": "Seleccionar método de autenticación", + "description": "Selecciona el método con el que deseas autenticarte", + "noMethodsAvailable": "No hay métodos de autenticación disponibles", + "allSetup": "¡Ya has configurado un autenticador!", + "linkWithIDP": "o vincúlalo con un proveedor de identidad" + }, + "device": { + "usercode": { + "title": "Código del dispositivo", + "description": "Introduce el código.", + "submit": "Continuar" + }, + "request": { + "title": "{appName} desea conectarse:", + "description": "{appName} tendrá acceso a:", + "disclaimer": "Al hacer clic en Permitir, autorizas a {appName} y a Zitadel a usar tu información de acuerdo con sus respectivos términos de servicio y políticas de privacidad. Puedes revocar este acceso en cualquier momento.", + "submit": "Permitir", + "deny": "Denegar" + }, + "scope": { + "openid": "Verifica tu identidad.", + "email": "Accede a tu dirección de correo electrónico.", + "profile": "Accede a la información completa de tu perfil.", + "offline_access": "Permitir acceso sin conexión a tu cuenta." + } + }, + "error": { + "noUserCode": "¡No se proporcionó código de usuario!", + "noDeviceRequest": "No se encontró ninguna solicitud de dispositivo.", + "unknownContext": "No se pudo obtener el contexto del usuario. Asegúrate de ingresar primero el nombre de usuario o proporcionar un loginName como parámetro de búsqueda.", + "sessionExpired": "Tu sesión actual ha expirado. Por favor, inicia sesión de nuevo.", + "failedLoading": "No se pudieron cargar los datos. Por favor, inténtalo de nuevo.", + "tryagain": "Intentar de nuevo" + } + }, + "it":{ + "common": { + "back": "Indietro" + }, + "accounts": { + "title": "Account", + "description": "Seleziona l'account che desideri utilizzare.", + "addAnother": "Aggiungi un altro account", + "noResults": "Nessun account trovato" + }, + "loginname": { + "title": "Bentornato!", + "description": "Inserisci i tuoi dati di accesso.", + "register": "Registrati come nuovo utente" + }, + "password": { + "verify": { + "title": "Password", + "description": "Inserisci la tua password.", + "resetPassword": "Reimposta Password", + "submit": "Continua" + }, + "set": { + "title": "Imposta Password", + "description": "Imposta la password per il tuo account", + "codeSent": "Un codice è stato inviato al tuo indirizzo email.", + "noCodeReceived": "Non hai ricevuto un codice?", + "resend": "Invia di nuovo", + "submit": "Continua" + }, + "change": { + "title": "Cambia Password", + "description": "Imposta la password per il tuo account", + "submit": "Continua" + } + }, + "idp": { + "title": "Accedi con SSO", + "description": "Seleziona uno dei seguenti provider per accedere", + "signInWithApple": "Accedi con Apple", + "signInWithGoogle": "Accedi con Google", + "signInWithAzureAD": "Accedi con AzureAD", + "signInWithGithub": "Accedi con GitHub", + "signInWithGitlab": "Accedi con GitLab", + "loginSuccess": { + "title": "Accesso riuscito", + "description": "Accesso effettuato con successo!" + }, + "linkingSuccess": { + "title": "Account collegato", + "description": "Hai collegato con successo il tuo account!" + }, + "registerSuccess": { + "title": "Registrazione riuscita", + "description": "Registrazione effettuata con successo!" + }, + "loginError": { + "title": "Accesso fallito", + "description": "Si è verificato un errore durante il tentativo di accesso." + }, + "linkingError": { + "title": "Collegamento account fallito", + "description": "Si è verificato un errore durante il tentativo di collegare il tuo account." + } + }, + "mfa": { + "verify": { + "title": "Verifica la tua identità", + "description": "Scegli uno dei seguenti fattori.", + "noResults": "Nessun secondo fattore disponibile per la configurazione." + }, + "set": { + "title": "Configura l'autenticazione a 2 fattori", + "description": "Scegli uno dei seguenti secondi fattori.", + "skip": "Salta" + } + }, + "otp": { + "verify": { + "title": "Verifica l'autenticazione a 2 fattori", + "totpDescription": "Inserisci il codice dalla tua app di autenticazione.", + "smsDescription": "Inserisci il codice ricevuto via SMS.", + "emailDescription": "Inserisci il codice ricevuto via email.", + "noCodeReceived": "Non hai ricevuto un codice?", + "resendCode": "Invia di nuovo il codice", + "submit": "Continua" + }, + "set": { + "title": "Configura l'autenticazione a 2 fattori", + "totpDescription": "Scansiona il codice QR con la tua app di autenticazione.", + "smsDescription": "Inserisci il tuo numero di telefono per ricevere un codice via SMS.", + "emailDescription": "Inserisci il tuo indirizzo email per ricevere un codice via email.", + "totpRegisterDescription": "Scansiona il codice QR o naviga manualmente all'URL.", + "submit": "Continua" + } + }, + "passkey": { + "verify": { + "title": "Autenticati con una passkey", + "description": "Il tuo dispositivo chiederà la tua impronta digitale, il volto o il blocco schermo", + "usePassword": "Usa password", + "submit": "Continua" + }, + "set": { + "title": "Configura una passkey", + "description": "Il tuo dispositivo chiederà la tua impronta digitale, il volto o il blocco schermo", + "info": { + "description": "Una passkey è un metodo di autenticazione su un dispositivo come la tua impronta digitale, Apple FaceID o simili.", + "link": "Autenticazione senza password" + }, + "skip": "Salta", + "submit": "Continua" + } + }, + "u2f": { + "verify": { + "title": "Verifica l'autenticazione a 2 fattori", + "description": "Verifica il tuo account con il tuo dispositivo." + }, + "set": { + "title": "Configura l'autenticazione a 2 fattori", + "description": "Configura un dispositivo come secondo fattore.", + "submit": "Continua" + } + }, + "register": { + "methods": { + "passkey": "Passkey", + "password": "Password" + }, + "disabled": { + "title": "Registration disabled", + "description": "Registrazione disabilitata. Contatta l'amministratore di sistema per assistenza." + }, + "missingdata": { + "title": "Registrazione", + "description": "Inserisci i tuoi dati per registrarti." + }, + "title": "Registrati", + "description": "Crea il tuo account ZITADEL.", + "selectMethod": "Seleziona il metodo con cui desideri autenticarti", + "agreeTo": "Per registrarti devi accettare i termini e le condizioni", + "termsOfService": "Termini di Servizio", + "privacyPolicy": "Informativa sulla Privacy", + "submit": "Continua", + "password": { + "title": "Imposta Password", + "description": "Imposta la password per il tuo account", + "submit": "Continua" + } + }, + "invite": { + "title": "Invita Utente", + "description": "Inserisci l'indirizzo email dell'utente che desideri invitare.", + "info": "L'utente riceverà un'email con ulteriori istruzioni.", + "notAllowed": "Non hai i permessi per invitare un utente.", + "submit": "Invita Utente", + "success": { + "title": "Invito inviato", + "description": "L'utente è stato invitato con successo.", + "verified": "L'utente è stato invitato e ha già verificato la sua email.", + "notVerifiedYet": "L'utente è stato invitato. Riceverà un'email con ulteriori istruzioni.", + "submit": "Invita un altro utente" + } + }, + "signedin": { + "title": "Benvenuto {user}!", + "description": "Sei connesso.", + "continue": "Continua", + "error": { + "title": "Errore", + "description": "Si è verificato un errore durante il tentativo di accesso." + } + }, + "verify": { + "userIdMissing": "Nessun userId fornito!", + "success": "Verifica effettuata con successo!", + "setupAuthenticator": "Configura autenticatore", + "verify": { + "title": "Verifica utente", + "description": "Inserisci il codice fornito nell'email di verifica.", + "noCodeReceived": "Non hai ricevuto un codice?", + "resendCode": "Invia di nuovo il codice", + "submit": "Continua" + } + }, + "authenticator": { + "title": "Seleziona metodo di autenticazione", + "description": "Seleziona il metodo con cui desideri autenticarti", + "noMethodsAvailable": "Nessun metodo di autenticazione disponibile", + "allSetup": "Hai già configurato un autenticatore!", + "linkWithIDP": "o collega con un Identity Provider" + }, + "device": { + "usercode": { + "title": "Codice dispositivo", + "description": "Inserisci il codice.", + "submit": "Continua" + }, + "request": { + "title": "{appName} desidera connettersi:", + "description": "{appName} avrà accesso a:", + "disclaimer": "Cliccando su Consenti, autorizzi {appName} e Zitadel a utilizzare le tue informazioni in conformità con i rispettivi termini di servizio e politiche sulla privacy. Puoi revocare questo accesso in qualsiasi momento.", + "submit": "Consenti", + "deny": "Nega" + }, + "scope": { + "openid": "Verifica la tua identità.", + "email": "Accedi al tuo indirizzo email.", + "profile": "Accedi alle informazioni complete del tuo profilo.", + "offline_access": "Consenti l'accesso offline al tuo account." + } + }, + "error": { + "noUserCode": "Nessun codice utente fornito!", + "noDeviceRequest": "Nessuna richiesta di dispositivo trovata.", + "unknownContext": "Impossibile ottenere il contesto dell'utente. Assicurati di inserire prima il nome utente o di fornire un loginName come parametro di ricerca.", + "sessionExpired": "La tua sessione attuale è scaduta. Effettua nuovamente l'accesso.", + "failedLoading": "Impossibile caricare i dati. Riprova.", + "tryagain": "Riprova" + } + + }, + "pl":{ + "common": { + "back": "Powrót" + }, + "accounts": { + "title": "Konta", + "description": "Wybierz konto, którego chcesz użyć.", + "addAnother": "Dodaj kolejne konto", + "noResults": "Nie znaleziono kont" + }, + "loginname": { + "title": "Witamy ponownie!", + "description": "Wprowadź dane logowania.", + "register": "Zarejestruj nowego użytkownika" + }, + "password": { + "verify": { + "title": "Hasło", + "description": "Wprowadź swoje hasło.", + "resetPassword": "Zresetuj hasło", + "submit": "Kontynuuj" + }, + "set": { + "title": "Ustaw hasło", + "description": "Ustaw hasło dla swojego konta", + "codeSent": "Kod został wysłany na twój adres e-mail.", + "noCodeReceived": "Nie otrzymałeś kodu?", + "resend": "Wyślij kod ponownie", + "submit": "Kontynuuj" + }, + "change": { + "title": "Zmień hasło", + "description": "Ustaw nowe hasło dla swojego konta", + "submit": "Kontynuuj" + } + }, + "idp": { + "title": "Zaloguj się za pomocą SSO", + "description": "Wybierz jednego z poniższych dostawców, aby się zalogować", + "signInWithApple": "Zaloguj się przez Apple", + "signInWithGoogle": "Zaloguj się przez Google", + "signInWithAzureAD": "Zaloguj się przez AzureAD", + "signInWithGithub": "Zaloguj się przez GitHub", + "signInWithGitlab": "Zaloguj się przez GitLab", + "loginSuccess": { + "title": "Logowanie udane", + "description": "Zostałeś pomyślnie zalogowany!" + }, + "linkingSuccess": { + "title": "Konto powiązane", + "description": "Pomyślnie powiązałeś swoje konto!" + }, + "registerSuccess": { + "title": "Rejestracja udana", + "description": "Pomyślnie się zarejestrowałeś!" + }, + "loginError": { + "title": "Logowanie nieudane", + "description": "Wystąpił błąd podczas próby logowania." + }, + "linkingError": { + "title": "Powiązanie konta nie powiodło się", + "description": "Wystąpił błąd podczas próby powiązania konta." + } + }, + "mfa": { + "verify": { + "title": "Zweryfikuj swoją tożsamość", + "description": "Wybierz jeden z poniższych sposobów weryfikacji.", + "noResults": "Nie znaleziono dostępnych metod uwierzytelniania dwuskładnikowego." + }, + "set": { + "title": "Skonfiguruj uwierzytelnianie dwuskładnikowe", + "description": "Wybierz jedną z poniższych metod drugiego czynnika.", + "skip": "Pomiń" + } + }, + "otp": { + "verify": { + "title": "Zweryfikuj uwierzytelnianie dwuskładnikowe", + "totpDescription": "Wprowadź kod z aplikacji uwierzytelniającej.", + "smsDescription": "Wprowadź kod otrzymany SMS-em.", + "emailDescription": "Wprowadź kod otrzymany e-mailem.", + "noCodeReceived": "Nie otrzymałeś kodu?", + "resendCode": "Wyślij kod ponownie", + "submit": "Kontynuuj" + }, + "set": { + "title": "Skonfiguruj uwierzytelnianie dwuskładnikowe", + "totpDescription": "Zeskanuj kod QR za pomocą aplikacji uwierzytelniającej.", + "smsDescription": "Wprowadź swój numer telefonu, aby otrzymać kod SMS-em.", + "emailDescription": "Wprowadź swój adres e-mail, aby otrzymać kod e-mailem.", + "totpRegisterDescription": "Zeskanuj kod QR lub otwórz adres URL ręcznie.", + "submit": "Kontynuuj" + } + }, + "passkey": { + "verify": { + "title": "Uwierzytelnij się za pomocą klucza dostępu", + "description": "Twoje urządzenie poprosi o użycie odcisku palca, rozpoznawania twarzy lub blokady ekranu.", + "usePassword": "Użyj hasła", + "submit": "Kontynuuj" + }, + "set": { + "title": "Skonfiguruj klucz dostępu", + "description": "Twoje urządzenie poprosi o użycie odcisku palca, rozpoznawania twarzy lub blokady ekranu.", + "info": { + "description": "Klucz dostępu to metoda uwierzytelniania na urządzeniu, wykorzystująca np. odcisk palca, Apple FaceID lub podobne rozwiązania.", + "link": "Uwierzytelnianie bez hasła" + }, + "skip": "Pomiń", + "submit": "Kontynuuj" + } + }, + "u2f": { + "verify": { + "title": "Zweryfikuj uwierzytelnianie dwuskładnikowe", + "description": "Zweryfikuj swoje konto za pomocą urządzenia." + }, + "set": { + "title": "Skonfiguruj uwierzytelnianie dwuskładnikowe", + "description": "Skonfiguruj urządzenie jako dodatkowy czynnik uwierzytelniania.", + "submit": "Kontynuuj" + } + }, + "register": { + "methods": { + "passkey": "Klucz dostępu", + "password": "Hasło" + }, + "disabled": { + "title": "Rejestracja wyłączona", + "description": "Rejestracja jest wyłączona. Skontaktuj się z administratorem." + }, + "missingdata": { + "title": "Brak danych", + "description": "Podaj e-mail, imię i nazwisko, aby się zarejestrować." + }, + "title": "Rejestracja", + "description": "Utwórz konto ZITADEL.", + "selectMethod": "Wybierz metodę uwierzytelniania, której chcesz użyć", + "agreeTo": "Aby się zarejestrować, musisz zaakceptować warunki korzystania", + "termsOfService": "Regulamin", + "privacyPolicy": "Polityka prywatności", + "submit": "Kontynuuj", + "password": { + "title": "Ustaw hasło", + "description": "Ustaw hasło dla swojego konta", + "submit": "Kontynuuj" + } + }, + "invite": { + "title": "Zaproś użytkownika", + "description": "Podaj adres e-mail oraz imię i nazwisko użytkownika, którego chcesz zaprosić.", + "info": "Użytkownik otrzyma e-mail z dalszymi instrukcjami.", + "notAllowed": "Twoje ustawienia nie pozwalają na zapraszanie użytkowników.", + "submit": "Kontynuuj", + "success": { + "title": "Użytkownik zaproszony", + "description": "E-mail został pomyślnie wysłany.", + "verified": "Użytkownik został zaproszony i już zweryfikował swój e-mail.", + "notVerifiedYet": "Użytkownik został zaproszony. Otrzyma e-mail z dalszymi instrukcjami.", + "submit": "Zaproś kolejnego użytkownika" + } + }, + "signedin": { + "title": "Witaj {user}!", + "description": "Jesteś zalogowany.", + "continue": "Kontynuuj", + "error": { + "title": "Błąd", + "description": "Nie można załadować danych. Sprawdź połączenie z internetem lub spróbuj ponownie później." + } + }, + "verify": { + "userIdMissing": "Nie podano identyfikatora użytkownika!", + "success": "Użytkownik został pomyślnie zweryfikowany.", + "setupAuthenticator": "Skonfiguruj uwierzytelnianie", + "verify": { + "title": "Zweryfikuj użytkownika", + "description": "Wprowadź kod z wiadomości weryfikacyjnej.", + "noCodeReceived": "Nie otrzymałeś kodu?", + "resendCode": "Wyślij kod ponownie", + "submit": "Kontynuuj" + } + }, + "authenticator": { + "title": "Wybierz metodę uwierzytelniania", + "description": "Wybierz metodę, której chcesz użyć do uwierzytelnienia.", + "noMethodsAvailable": "Brak dostępnych metod uwierzytelniania", + "allSetup": "Już skonfigurowałeś metodę uwierzytelniania!", + "linkWithIDP": "lub połącz z dostawcą tożsamości" + }, + "device": { + "usercode": { + "title": "Kod urządzenia", + "description": "Wprowadź kod.", + "submit": "Kontynuuj" + }, + "request": { + "title": "{appName} chce się połączyć:", + "description": "{appName} będzie miało dostęp do:", + "disclaimer": "Klikając Zezwól, pozwalasz tej aplikacji i Zitadel na korzystanie z Twoich informacji zgodnie z ich odpowiednimi warunkami użytkowania i politykami prywatności. Możesz cofnąć ten dostęp w dowolnym momencie.", + "submit": "Zezwól", + "deny": "Odmów" + }, + "scope": { + "openid": "Zweryfikuj swoją tożsamość.", + "email": "Uzyskaj dostęp do swojego adresu e-mail.", + "profile": "Uzyskaj dostęp do pełnych informacji o swoim profilu.", + "offline_access": "Zezwól na dostęp offline do swojego konta." + } + }, + "error": { + "noUserCode": "Nie podano kodu użytkownika!", + "noDeviceRequest": "Nie znaleziono żądania urządzenia.", + "unknownContext": "Nie udało się pobrać kontekstu użytkownika. Upewnij się, że najpierw wprowadziłeś nazwę użytkownika lub podałeś login jako parametr wyszukiwania.", + "sessionExpired": "Twoja sesja wygasła. Zaloguj się ponownie.", + "failedLoading": "Nie udało się załadować danych. Spróbuj ponownie.", + "tryagain": "Spróbuj ponownie" + } + }, + "ru":{ + "common": { + "back": "Назад" + }, + "accounts": { + "title": "Аккаунты", + "description": "Выберите аккаунт, который хотите использовать.", + "addAnother": "Добавить другой аккаунт", + "noResults": "Аккаунты не найдены" + }, + "loginname": { + "title": "С возвращением!", + "description": "Введите свои данные для входа.", + "register": "Зарегистрировать нового пользователя" + }, + "password": { + "verify": { + "title": "Пароль", + "description": "Введите ваш пароль.", + "resetPassword": "Сбросить пароль", + "submit": "Продолжить" + }, + "set": { + "title": "Установить пароль", + "description": "Установите пароль для вашего аккаунта", + "codeSent": "Код отправлен на ваш адрес электронной почты.", + "noCodeReceived": "Не получили код?", + "resend": "Отправить код повторно", + "submit": "Продолжить" + }, + "change": { + "title": "Изменить пароль", + "description": "Установите пароль для вашего аккаунта", + "submit": "Продолжить" + } + }, + "idp": { + "title": "Войти через SSO", + "description": "Выберите одного из провайдеров для входа", + "signInWithApple": "Войти через Apple", + "signInWithGoogle": "Войти через Google", + "signInWithAzureAD": "Войти через AzureAD", + "signInWithGithub": "Войти через GitHub", + "signInWithGitlab": "Войти через GitLab", + "loginSuccess": { + "title": "Вход выполнен успешно", + "description": "Вы успешно вошли в систему!" + }, + "linkingSuccess": { + "title": "Аккаунт привязан", + "description": "Аккаунт успешно привязан!" + }, + "registerSuccess": { + "title": "Регистрация завершена", + "description": "Вы успешно зарегистрировались!" + }, + "loginError": { + "title": "Ошибка входа", + "description": "Произошла ошибка при попытке входа." + }, + "linkingError": { + "title": "Ошибка привязки аккаунта", + "description": "Произошла ошибка при попытке привязать аккаунт." + } + }, + "mfa": { + "verify": { + "title": "Подтвердите вашу личность", + "description": "Выберите один из следующих факторов.", + "noResults": "Нет доступных методов двухфакторной аутентификации" + }, + "set": { + "title": "Настройка двухфакторной аутентификации", + "description": "Выберите один из следующих методов.", + "skip": "Пропустить" + } + }, + "otp": { + "verify": { + "title": "Подтверждение 2FA", + "totpDescription": "Введите код из приложения-аутентификатора.", + "smsDescription": "Введите код, полученный по SMS.", + "emailDescription": "Введите код, полученный по email.", + "noCodeReceived": "Не получили код?", + "resendCode": "Отправить код повторно", + "submit": "Продолжить" + }, + "set": { + "title": "Настройка двухфакторной аутентификации", + "totpDescription": "Отсканируйте QR-код в приложении-аутентификаторе.", + "smsDescription": "Введите номер телефона для получения кода по SMS.", + "emailDescription": "Введите email для получения кода.", + "totpRegisterDescription": "Отсканируйте QR-код или перейдите по ссылке вручную.", + "submit": "Продолжить" + } + }, + "passkey": { + "verify": { + "title": "Аутентификация с помощью пасскей", + "description": "Устройство запросит отпечаток пальца, лицо или экранный замок", + "usePassword": "Использовать пароль", + "submit": "Продолжить" + }, + "set": { + "title": "Настройка пасскей", + "description": "Устройство запросит отпечаток пальца, лицо или экранный замок", + "info": { + "description": "Пасскей — метод аутентификации через устройство (отпечаток пальца, Apple FaceID и аналоги).", + "link": "Аутентификация без пароля" + }, + "skip": "Пропустить", + "submit": "Продолжить" + } + }, + "u2f": { + "verify": { + "title": "Подтверждение 2FA", + "description": "Подтвердите аккаунт с помощью устройства." + }, + "set": { + "title": "Настройка двухфакторной аутентификации", + "description": "Настройте устройство как второй фактор.", + "submit": "Продолжить" + } + }, + "register": { + "methods": { + "passkey": "Пасскей", + "password": "Пароль" + }, + "disabled": { + "title": "Регистрация отключена", + "description": "Регистрация недоступна. Обратитесь к администратору." + }, + "missingdata": { + "title": "Недостаточно данных", + "description": "Укажите email, имя и фамилию для регистрации." + }, + "title": "Регистрация", + "description": "Создайте свой аккаунт ZITADEL.", + "selectMethod": "Выберите метод аутентификации", + "agreeTo": "Для регистрации необходимо принять условия:", + "termsOfService": "Условия использования", + "privacyPolicy": "Политика конфиденциальности", + "submit": "Продолжить", + "password": { + "title": "Установить пароль", + "description": "Установите пароль для вашего аккаунта", + "submit": "Продолжить" + } + }, + "invite": { + "title": "Пригласить пользователя", + "description": "Укажите email и имя пользователя для приглашения.", + "info": "Пользователь получит email с инструкциями.", + "notAllowed": "Ваши настройки не позволяют приглашать пользователей.", + "submit": "Продолжить", + "success": { + "title": "Пользователь приглашён", + "description": "Письмо успешно отправлено.", + "verified": "Пользователь приглашён и уже подтвердил email.", + "notVerifiedYet": "Пользователь приглашён. Он получит email с инструкциями.", + "submit": "Пригласить другого пользователя" + } + }, + "signedin": { + "title": "Добро пожаловать, {user}!", + "description": "Вы вошли в систему.", + "continue": "Продолжить", + "error": { + "title": "Ошибка", + "description": "Не удалось войти в систему. Проверьте свои данные и попробуйте снова." + } + }, + "verify": { + "userIdMissing": "Не указан userId!", + "success": "Пользователь успешно подтверждён.", + "setupAuthenticator": "Настроить аутентификатор", + "verify": { + "title": "Подтверждение пользователя", + "description": "Введите код из письма подтверждения.", + "noCodeReceived": "Не получили код?", + "resendCode": "Отправить код повторно", + "submit": "Продолжить" + } + }, + "authenticator": { + "title": "Выбор метода аутентификации", + "description": "Выберите предпочитаемый метод аутентификации", + "noMethodsAvailable": "Нет доступных методов аутентификации", + "allSetup": "Аутентификатор уже настроен!", + "linkWithIDP": "или привязать через Identity Provider" + }, + "device": { + "usercode": { + "title": "Код устройства", + "description": "Введите код.", + "submit": "Продолжить" + }, + "request": { + "title": "{appName} хочет подключиться:", + "description": "{appName} получит доступ к:", + "disclaimer": "Нажимая «Разрешить», вы разрешаете этому приложению и Zitadel использовать вашу информацию в соответствии с их условиями использования и политиками конфиденциальности. Вы можете отозвать этот доступ в любое время.", + "submit": "Разрешить", + "deny": "Запретить" + }, + "scope": { + "openid": "Проверка вашей личности.", + "email": "Доступ к вашему адресу электронной почты.", + "profile": "Доступ к полной информации вашего профиля.", + "offline_access": "Разрешить офлайн-доступ к вашему аккаунту." + } + }, + "error": { + "noUserCode": "Не указан код пользователя!", + "noDeviceRequest": "Не найдена ни одна заявка на устройство.", + "unknownContext": "Не удалось получить контекст пользователя. Укажите имя пользователя или loginName в параметрах поиска.", + "sessionExpired": "Ваша сессия истекла. Войдите снова.", + "failedLoading": "Ошибка загрузки данных. Попробуйте ещё раз.", + "tryagain": "Попробовать снова" + } + }, + "zh":{ + "common": { + "back": "返回" + }, + "accounts": { + "title": "账户", + "description": "选择您想使用的账户。", + "addAnother": "添加另一个账户", + "noResults": "未找到账户" + }, + "loginname": { + "title": "欢迎回来!", + "description": "请输入您的登录信息。", + "register": "注册新用户" + }, + "password": { + "verify": { + "title": "密码", + "description": "请输入您的密码。", + "resetPassword": "重置密码", + "submit": "继续" + }, + "set": { + "title": "设置密码", + "description": "为您的账户设置密码", + "codeSent": "验证码已发送到您的邮箱。", + "noCodeReceived": "没有收到验证码?", + "resend": "重发验证码", + "submit": "继续" + }, + "change": { + "title": "更改密码", + "description": "为您的账户设置密码", + "submit": "继续" + } + }, + "idp": { + "title": "使用 SSO 登录", + "description": "选择以下提供商中的一个进行登录", + "signInWithApple": "用 Apple 登录", + "signInWithGoogle": "用 Google 登录", + "signInWithAzureAD": "用 AzureAD 登录", + "signInWithGithub": "用 GitHub 登录", + "signInWithGitlab": "用 GitLab 登录", + "loginSuccess": { + "title": "登录成功", + "description": "您已成功登录!" + }, + "linkingSuccess": { + "title": "账户已链接", + "description": "您已成功链接您的账户!" + }, + "registerSuccess": { + "title": "注册成功", + "description": "您已成功注册!" + }, + "loginError": { + "title": "登录失败", + "description": "登录时发生错误。" + }, + "linkingError": { + "title": "账户链接失败", + "description": "链接账户时发生错误。" + } + }, + "mfa": { + "verify": { + "title": "验证您的身份", + "description": "选择以下的一个因素。", + "noResults": "没有可设置的第二因素。" + }, + "set": { + "title": "设置双因素认证", + "description": "选择以下的一个第二因素。", + "skip": "跳过" + } + }, + "otp": { + "verify": { + "title": "验证双因素", + "totpDescription": "请输入认证应用程序中的验证码。", + "smsDescription": "输入通过短信收到的验证码。", + "emailDescription": "输入通过电子邮件收到的验证码。", + "noCodeReceived": "没有收到验证码?", + "resendCode": "重发验证码", + "submit": "继续" + }, + "set": { + "title": "设置双因素认证", + "totpDescription": "使用认证应用程序扫描二维码。", + "smsDescription": "输入您的电话号码以接收短信验证码。", + "emailDescription": "输入您的电子邮箱地址以接收电子邮件验证码。", + "totpRegisterDescription": "扫描二维码或手动导航到URL。", + "submit": "继续" + } + }, + "passkey": { + "verify": { + "title": "使用密钥认证", + "description": "您的设备将请求指纹、面部识别或屏幕锁", + "usePassword": "使用密码", + "submit": "继续" + }, + "set": { + "title": "设置密钥", + "description": "您的设备将请求指纹、面部识别或屏幕锁", + "info": { + "description": "密钥是在设备上如指纹、Apple FaceID 或类似的认证方法。", + "link": "无密码认证" + }, + "skip": "跳过", + "submit": "继续" + } + }, + "u2f": { + "verify": { + "title": "验证双因素", + "description": "使用您的设备验证帐户。" + }, + "set": { + "title": "设置双因素认证", + "description": "设置设备为第二因素。", + "submit": "继续" + } + }, + "register": { + "methods": { + "passkey": "密钥", + "password": "密码" + }, + "disabled": { + "title": "注册已禁用", + "description": "您的设置不允许注册新用户。" + }, + "missingdata": { + "title": "缺少数据", + "description": "请提供所有必需的数据。" + }, + "title": "注册", + "description": "创建您的 ZITADEL 账户。", + "selectMethod": "选择您想使用的认证方法", + "agreeTo": "注册即表示您同意条款和条件", + "termsOfService": "服务条款", + "privacyPolicy": "隐私政策", + "submit": "继续", + "password": { + "title": "设置密码", + "description": "为您的账户设置密码", + "submit": "继续" + } + }, + "invite": { + "title": "邀请用户", + "description": "提供您想邀请的用户的电子邮箱地址和姓名。", + "info": "用户将收到一封包含进一步说明的电子邮件。", + "notAllowed": "您的设置不允许邀请用户。", + "submit": "继续", + "success": { + "title": "用户已邀请", + "description": "邮件已成功发送。", + "verified": "用户已被邀请并已验证其电子邮件。", + "notVerifiedYet": "用户已被邀请。他们将收到一封包含进一步说明的电子邮件。", + "submit": "邀请另一位用户" + } + }, + "signedin": { + "title": "欢迎 {user}!", + "description": "您已登录。", + "continue": "继续", + "error": { + "title": "错误", + "description": "登录时发生错误。" + } + }, + "verify": { + "userIdMissing": "未提供用户 ID!", + "success": "用户验证成功。", + "setupAuthenticator": "设置认证器", + "verify": { + "title": "验证用户", + "description": "输入验证邮件中的验证码。", + "noCodeReceived": "没有收到验证码?", + "resendCode": "重发验证码", + "submit": "继续" + } + }, + "authenticator": { + "title": "选择认证方式", + "description": "选择您想使用的认证方法", + "noMethodsAvailable": "没有可用的认证方法", + "allSetup": "您已经设置好了一个认证器!", + "linkWithIDP": "或将其与身份提供者关联" + }, + "device": { + "usercode": { + "title": "设备代码", + "description": "输入代码。", + "submit": "继续" + }, + "request": { + "title": "{appName} 想要连接:", + "description": "{appName} 将访问:", + "disclaimer": "点击“允许”即表示您允许此应用程序和 Zitadel 根据其各自的服务条款和隐私政策使用您的信息。您可以随时撤销此访问权限。", + "submit": "允许", + "deny": "拒绝" + }, + "scope": { + "openid": "验证您的身份。", + "email": "访问您的电子邮件地址。", + "profile": "访问您的完整个人资料信息。", + "offline_access": "允许离线访问您的账户。" + } + }, + "error": { + "noUserCode": "未提供用户代码!", + "noDeviceRequest": "没有找到设备请求。", + "unknownContext": "无法获取用户的上下文。请先输入用户名或提供 loginName 作为搜索参数。", + "sessionExpired": "当前会话已过期,请重新登录。", + "failedLoading": "加载数据失败,请再试一次。", + "tryagain": "重试" + } + }, + "tr":{ + "common": { + "back": "Geri" + }, + "accounts": { + "title": "Hesaplar", + "description": "Kullanmak istediğiniz hesabı seçin.", + "addAnother": "Başka bir hesap ekle", + "noResults": "Hesap bulunamadı" + }, + "loginname": { + "title": "Tekrar hoş geldiniz!", + "description": "Giriş bilgilerinizi girin.", + "register": "Yeni kullanıcı kaydet" + }, + "password": { + "verify": { + "title": "Şifre", + "description": "Şifrenizi girin.", + "resetPassword": "Şifreyi Sıfırla", + "submit": "Devam Et" + }, + "set": { + "title": "Şifre Belirle", + "description": "Hesabınız için şifre belirleyin", + "codeSent": "E-posta adresinize bir kod gönderildi.", + "noCodeReceived": "Kod almadınız mı?", + "resend": "Kodu tekrar gönder", + "submit": "Devam Et" + }, + "change": { + "title": "Şifre Değiştir", + "description": "Hesabınız için şifre belirleyin", + "submit": "Devam Et" + } + }, + "idp": { + "title": "SSO ile giriş yap", + "description": "Giriş yapmak için aşağıdaki sağlayıcılardan birini seçin", + "signInWithApple": "Apple ile giriş yap", + "signInWithGoogle": "Google ile giriş yap", + "signInWithAzureAD": "AzureAD ile giriş yap", + "signInWithGithub": "GitHub ile giriş yap", + "signInWithGitlab": "GitLab ile giriş yap", + "loginSuccess": { + "title": "Giriş başarılı", + "description": "Başarıyla giriş yaptınız!" + }, + "linkingSuccess": { + "title": "Hesap bağlandı", + "description": "Hesabınızı başarıyla bağladınız!" + }, + "registerSuccess": { + "title": "Kayıt başarılı", + "description": "Başarıyla kayıt oldunuz!" + }, + "loginError": { + "title": "Giriş başarısız", + "description": "Giriş yapmaya çalışırken bir hata oluştu." + }, + "linkingError": { + "title": "Hesap bağlama başarısız", + "description": "Hesabınızı bağlamaya çalışırken bir hata oluştu." + } + }, + "mfa": { + "verify": { + "title": "Kimliğinizi doğrulayın", + "description": "Aşağıdaki faktörlerden birini seçin.", + "noResults": "Kurulum için kullanılabilir ikinci faktör yok." + }, + "set": { + "title": "2-Faktör kur", + "description": "Aşağıdaki ikinci faktörlerden birini seçin.", + "skip": "Atla" + } + }, + "otp": { + "verify": { + "title": "2-Faktör doğrula", + "totpDescription": "Kimlik doğrulayıcı uygulamanızdan kodu girin.", + "smsDescription": "SMS ile aldığınız kodu girin.", + "emailDescription": "E-posta ile aldığınız kodu girin.", + "noCodeReceived": "Kod almadınız mı?", + "resendCode": "Kodu tekrar gönder", + "submit": "Devam Et" + }, + "set": { + "title": "2-Faktör kur", + "totpDescription": "QR kodunu kimlik doğrulayıcı uygulamanızla tarayın.", + "smsDescription": "SMS ile kod almak için telefon numaranızı girin.", + "emailDescription": "E-posta ile kod almak için e-posta adresinizi girin.", + "totpRegisterDescription": "QR Kodunu tarayın veya URL'ye manuel olarak gidin.", + "submit": "Devam Et" + } + }, + "passkey": { + "verify": { + "title": "Passkey ile kimlik doğrula", + "description": "Cihazınız parmak izinizi, yüzünüzü veya ekran kilidinizi isteyecek", + "usePassword": "Şifre kullan", + "submit": "Devam Et" + }, + "set": { + "title": "Passkey kur", + "description": "Cihazınız parmak izinizi, yüzünüzü veya ekran kilidinizi isteyecek", + "info": { + "description": "Passkey, parmak iziniz, Apple FaceID veya benzeri gibi bir cihazda kimlik doğrulama yöntemidir.", + "link": "Şifresiz Kimlik Doğrulama" + }, + "skip": "Atla", + "submit": "Devam Et" + } + }, + "u2f": { + "verify": { + "title": "2-Faktör doğrula", + "description": "Cihazınızla hesabınızı doğrulayın." + }, + "set": { + "title": "2-Faktör kur", + "description": "Bir cihazı ikinci faktör olarak kurun.", + "submit": "Devam Et" + } + }, + "register": { + "methods": { + "passkey": "Passkey", + "password": "Şifre" + }, + "disabled": { + "title": "Kayıt devre dışı", + "description": "Kayıt devre dışı. Lütfen yöneticinizle iletişime geçin." + }, + "missingdata": { + "title": "Eksik veri", + "description": "Kayıt olmak için e-posta, ad ve soyadı sağlayın." + }, + "title": "Kayıt Ol", + "description": "ZITADEL hesabınızı oluşturun.", + "selectMethod": "Kimlik doğrulamak istediğiniz yöntemi seçin", + "agreeTo": "Kayıt olmak için şartları ve koşulları kabul etmelisiniz", + "termsOfService": "Hizmet Şartları", + "privacyPolicy": "Gizlilik Politikası", + "submit": "Devam Et", + "password": { + "title": "Şifre Belirle", + "description": "Hesabınız için şifre belirleyin", + "submit": "Devam Et" + } + }, + "invite": { + "title": "Kullanıcı Davet Et", + "description": "Davet etmek istediğiniz kullanıcının e-posta adresini ve adını sağlayın.", + "info": "Kullanıcı daha fazla talimat içeren bir e-posta alacaktır.", + "notAllowed": "Ayarlarınız kullanıcı davet etmenize izin vermiyor.", + "submit": "Devam Et", + "success": { + "title": "Kullanıcı davet edildi", + "description": "E-posta başarıyla gönderildi.", + "verified": "Kullanıcı davet edildi ve e-postasını zaten doğruladı.", + "notVerifiedYet": "Kullanıcı davet edildi. Daha fazla talimat içeren bir e-posta alacaklar.", + "submit": "Başka bir kullanıcı davet et" + } + }, + "signedin": { + "title": "Hoş geldiniz {user}!", + "description": "Giriş yaptınız.", + "continue": "Devam Et", + "error": { + "title": "Hata", + "description": "Giriş yapmaya çalışırken bir hata oluştu." + } + }, + "verify": { + "userIdMissing": "userId sağlanmadı!", + "success": "Kullanıcı başarıyla doğrulandı.", + "setupAuthenticator": "Kimlik doğrulayıcı kur", + "verify": { + "title": "Kullanıcıyı doğrula", + "description": "Doğrulama e-postasında sağlanan kodu girin.", + "noCodeReceived": "Kod almadınız mı?", + "resendCode": "Kodu tekrar gönder", + "submit": "Devam Et" + } + }, + "authenticator": { + "title": "Kimlik doğrulama yöntemi seç", + "description": "Kimlik doğrulamak istediğiniz yöntemi seçin", + "noMethodsAvailable": "Kullanılabilir kimlik doğrulama yöntemi yok", + "allSetup": "Zaten bir kimlik doğrulayıcı kurdunuz!", + "linkWithIDP": "veya bir Kimlik Sağlayıcısı ile bağla" + }, + "device": { + "usercode": { + "title": "Cihaz kodu", + "description": "Uygulamanızda veya cihazınızda görüntülenen kodu girin.", + "submit": "Devam Et" + }, + "request": { + "title": "{appName} bağlanmak istiyor", + "description": "{appName} şunlara erişecek:", + "disclaimer": "İzin Ver'e tıklayarak, {appName} ve Zitadel'in bilgilerinizi kendi hizmet şartları ve gizlilik politikalarına uygun olarak kullanmasına izin verirsiniz. Bu erişimi istediğiniz zaman iptal edebilirsiniz.", + "submit": "İzin Ver", + "deny": "Reddet" + }, + "scope": { + "openid": "Kimliğinizi doğrulayın.", + "email": "E-posta adresinizi görüntüleyin.", + "profile": "Tam profil bilgilerinizi görüntüleyin.", + "offline_access": "Hesabınıza çevrimdışı erişime izin verin." + } + }, + "error": { + "noUserCode": "Kullanıcı kodu sağlanmadı!", + "noDeviceRequest": "Cihaz isteği bulunamadı.", + "unknownContext": "Kullanıcının bağlamı alınamadı. Önce kullanıcı adını girdiğinizden emin olun veya arama parametresi olarak bir loginName sağlayın.", + "sessionExpired": "Mevcut oturumunuzun süresi doldu. Lütfen tekrar giriş yapın.", + "failedLoading": "Veri yüklenemedi. Lütfen tekrar deneyin.", + "tryagain": "Tekrar Dene" + } + } +} \ No newline at end of file diff --git a/internal/queue/queue.go b/internal/queue/queue.go index 22df8c2b5c..44e291bf4d 100644 --- a/internal/queue/queue.go +++ b/internal/queue/queue.go @@ -9,6 +9,7 @@ import ( "github.com/riverqueue/river/riverdriver/riverpgxv5" "github.com/riverqueue/river/rivertype" "github.com/riverqueue/rivercontrib/otelriver" + "github.com/robfig/cron/v3" "github.com/zitadel/logging" "github.com/zitadel/zitadel/internal/database" @@ -75,6 +76,26 @@ func (q *Queue) AddWorkers(w ...Worker) { } } +func (q *Queue) AddPeriodicJob(schedule cron.Schedule, jobArgs river.JobArgs, opts ...InsertOpt) (handle rivertype.PeriodicJobHandle) { + if q == nil { + logging.Info("skip adding periodic job because queue is not set") + return + } + options := new(river.InsertOpts) + for _, opt := range opts { + opt(options) + } + return q.client.PeriodicJobs().Add( + river.NewPeriodicJob( + schedule, + func() (river.JobArgs, *river.InsertOpts) { + return jobArgs, options + }, + nil, + ), + ) +} + type InsertOpt func(*river.InsertOpts) func WithMaxAttempts(maxAttempts uint8) InsertOpt { diff --git a/internal/repository/feature/feature_v2/eventstore.go b/internal/repository/feature/feature_v2/eventstore.go index 00618f56c2..293c1ee3cd 100644 --- a/internal/repository/feature/feature_v2/eventstore.go +++ b/internal/repository/feature/feature_v2/eventstore.go @@ -8,8 +8,6 @@ import ( func init() { eventstore.RegisterFilterEventMapper(AggregateType, SystemResetEventType, eventstore.GenericEventMapper[ResetEvent]) eventstore.RegisterFilterEventMapper(AggregateType, SystemLoginDefaultOrgEventType, eventstore.GenericEventMapper[SetEvent[bool]]) - eventstore.RegisterFilterEventMapper(AggregateType, SystemTriggerIntrospectionProjectionsEventType, eventstore.GenericEventMapper[SetEvent[bool]]) - eventstore.RegisterFilterEventMapper(AggregateType, SystemLegacyIntrospectionEventType, eventstore.GenericEventMapper[SetEvent[bool]]) eventstore.RegisterFilterEventMapper(AggregateType, SystemUserSchemaEventType, eventstore.GenericEventMapper[SetEvent[bool]]) eventstore.RegisterFilterEventMapper(AggregateType, SystemTokenExchangeEventType, eventstore.GenericEventMapper[SetEvent[bool]]) eventstore.RegisterFilterEventMapper(AggregateType, SystemImprovedPerformanceEventType, eventstore.GenericEventMapper[SetEvent[[]feature.ImprovedPerformanceType]]) @@ -21,12 +19,9 @@ func init() { eventstore.RegisterFilterEventMapper(AggregateType, InstanceResetEventType, eventstore.GenericEventMapper[ResetEvent]) eventstore.RegisterFilterEventMapper(AggregateType, InstanceLoginDefaultOrgEventType, eventstore.GenericEventMapper[SetEvent[bool]]) - eventstore.RegisterFilterEventMapper(AggregateType, InstanceTriggerIntrospectionProjectionsEventType, eventstore.GenericEventMapper[SetEvent[bool]]) - eventstore.RegisterFilterEventMapper(AggregateType, InstanceLegacyIntrospectionEventType, eventstore.GenericEventMapper[SetEvent[bool]]) eventstore.RegisterFilterEventMapper(AggregateType, InstanceUserSchemaEventType, eventstore.GenericEventMapper[SetEvent[bool]]) eventstore.RegisterFilterEventMapper(AggregateType, InstanceTokenExchangeEventType, eventstore.GenericEventMapper[SetEvent[bool]]) eventstore.RegisterFilterEventMapper(AggregateType, InstanceImprovedPerformanceEventType, eventstore.GenericEventMapper[SetEvent[[]feature.ImprovedPerformanceType]]) - eventstore.RegisterFilterEventMapper(AggregateType, InstanceWebKeyEventType, eventstore.GenericEventMapper[SetEvent[bool]]) eventstore.RegisterFilterEventMapper(AggregateType, InstanceDebugOIDCParentErrorEventType, eventstore.GenericEventMapper[SetEvent[bool]]) eventstore.RegisterFilterEventMapper(AggregateType, InstanceOIDCSingleV1SessionTerminationEventType, eventstore.GenericEventMapper[SetEvent[bool]]) eventstore.RegisterFilterEventMapper(AggregateType, InstanceDisableUserTokenEvent, eventstore.GenericEventMapper[SetEvent[bool]]) diff --git a/internal/repository/feature/feature_v2/feature.go b/internal/repository/feature/feature_v2/feature.go index d5e8941df2..2859b65ebf 100644 --- a/internal/repository/feature/feature_v2/feature.go +++ b/internal/repository/feature/feature_v2/feature.go @@ -11,34 +11,29 @@ import ( ) var ( - SystemResetEventType = resetEventTypeFromFeature(feature.LevelSystem) - SystemLoginDefaultOrgEventType = setEventTypeFromFeature(feature.LevelSystem, feature.KeyLoginDefaultOrg) - SystemTriggerIntrospectionProjectionsEventType = setEventTypeFromFeature(feature.LevelSystem, feature.KeyTriggerIntrospectionProjections) - SystemLegacyIntrospectionEventType = setEventTypeFromFeature(feature.LevelSystem, feature.KeyLegacyIntrospection) - SystemUserSchemaEventType = setEventTypeFromFeature(feature.LevelSystem, feature.KeyUserSchema) - SystemTokenExchangeEventType = setEventTypeFromFeature(feature.LevelSystem, feature.KeyTokenExchange) - SystemImprovedPerformanceEventType = setEventTypeFromFeature(feature.LevelSystem, feature.KeyImprovedPerformance) - SystemOIDCSingleV1SessionTerminationEventType = setEventTypeFromFeature(feature.LevelSystem, feature.KeyOIDCSingleV1SessionTermination) - SystemDisableUserTokenEvent = setEventTypeFromFeature(feature.LevelSystem, feature.KeyDisableUserTokenEvent) - SystemEnableBackChannelLogout = setEventTypeFromFeature(feature.LevelSystem, feature.KeyEnableBackChannelLogout) - SystemLoginVersion = setEventTypeFromFeature(feature.LevelSystem, feature.KeyLoginV2) - SystemPermissionCheckV2 = setEventTypeFromFeature(feature.LevelSystem, feature.KeyPermissionCheckV2) + SystemResetEventType = resetEventTypeFromFeature(feature.LevelSystem) + SystemLoginDefaultOrgEventType = setEventTypeFromFeature(feature.LevelSystem, feature.KeyLoginDefaultOrg) + SystemUserSchemaEventType = setEventTypeFromFeature(feature.LevelSystem, feature.KeyUserSchema) + SystemTokenExchangeEventType = setEventTypeFromFeature(feature.LevelSystem, feature.KeyTokenExchange) + SystemImprovedPerformanceEventType = setEventTypeFromFeature(feature.LevelSystem, feature.KeyImprovedPerformance) + SystemOIDCSingleV1SessionTerminationEventType = setEventTypeFromFeature(feature.LevelSystem, feature.KeyOIDCSingleV1SessionTermination) + SystemDisableUserTokenEvent = setEventTypeFromFeature(feature.LevelSystem, feature.KeyDisableUserTokenEvent) + SystemEnableBackChannelLogout = setEventTypeFromFeature(feature.LevelSystem, feature.KeyEnableBackChannelLogout) + SystemLoginVersion = setEventTypeFromFeature(feature.LevelSystem, feature.KeyLoginV2) + SystemPermissionCheckV2 = setEventTypeFromFeature(feature.LevelSystem, feature.KeyPermissionCheckV2) - InstanceResetEventType = resetEventTypeFromFeature(feature.LevelInstance) - InstanceLoginDefaultOrgEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyLoginDefaultOrg) - InstanceTriggerIntrospectionProjectionsEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyTriggerIntrospectionProjections) - InstanceLegacyIntrospectionEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyLegacyIntrospection) - InstanceUserSchemaEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyUserSchema) - InstanceTokenExchangeEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyTokenExchange) - InstanceImprovedPerformanceEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyImprovedPerformance) - InstanceWebKeyEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyWebKey) - InstanceDebugOIDCParentErrorEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyDebugOIDCParentError) - InstanceOIDCSingleV1SessionTerminationEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyOIDCSingleV1SessionTermination) - InstanceDisableUserTokenEvent = setEventTypeFromFeature(feature.LevelInstance, feature.KeyDisableUserTokenEvent) - InstanceEnableBackChannelLogout = setEventTypeFromFeature(feature.LevelInstance, feature.KeyEnableBackChannelLogout) - InstanceLoginVersion = setEventTypeFromFeature(feature.LevelInstance, feature.KeyLoginV2) - InstancePermissionCheckV2 = setEventTypeFromFeature(feature.LevelInstance, feature.KeyPermissionCheckV2) - InstanceConsoleUseV2UserApi = setEventTypeFromFeature(feature.LevelInstance, feature.KeyConsoleUseV2UserApi) + InstanceResetEventType = resetEventTypeFromFeature(feature.LevelInstance) + InstanceLoginDefaultOrgEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyLoginDefaultOrg) + InstanceUserSchemaEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyUserSchema) + InstanceTokenExchangeEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyTokenExchange) + InstanceImprovedPerformanceEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyImprovedPerformance) + InstanceDebugOIDCParentErrorEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyDebugOIDCParentError) + InstanceOIDCSingleV1SessionTerminationEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyOIDCSingleV1SessionTermination) + InstanceDisableUserTokenEvent = setEventTypeFromFeature(feature.LevelInstance, feature.KeyDisableUserTokenEvent) + InstanceEnableBackChannelLogout = setEventTypeFromFeature(feature.LevelInstance, feature.KeyEnableBackChannelLogout) + InstanceLoginVersion = setEventTypeFromFeature(feature.LevelInstance, feature.KeyLoginV2) + InstancePermissionCheckV2 = setEventTypeFromFeature(feature.LevelInstance, feature.KeyPermissionCheckV2) + InstanceConsoleUseV2UserApi = setEventTypeFromFeature(feature.LevelInstance, feature.KeyConsoleUseV2UserApi) ) const ( diff --git a/internal/repository/instance/eventstore.go b/internal/repository/instance/eventstore.go index 68621597a8..b8089152bb 100644 --- a/internal/repository/instance/eventstore.go +++ b/internal/repository/instance/eventstore.go @@ -130,4 +130,5 @@ func init() { eventstore.RegisterFilterEventMapper(AggregateType, NotificationPolicyChangedEventType, NotificationPolicyChangedEventMapper) eventstore.RegisterFilterEventMapper(AggregateType, TrustedDomainAddedEventType, eventstore.GenericEventMapper[TrustedDomainAddedEvent]) eventstore.RegisterFilterEventMapper(AggregateType, TrustedDomainRemovedEventType, eventstore.GenericEventMapper[TrustedDomainRemovedEvent]) + eventstore.RegisterFilterEventMapper(AggregateType, HostedLoginTranslationSet, HostedLoginTranslationSetEventMapper) } diff --git a/internal/repository/instance/hosted_login_translation.go b/internal/repository/instance/hosted_login_translation.go new file mode 100644 index 0000000000..05380521fc --- /dev/null +++ b/internal/repository/instance/hosted_login_translation.go @@ -0,0 +1,55 @@ +package instance + +import ( + "context" + + "golang.org/x/text/language" + + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/zerrors" +) + +const ( + HostedLoginTranslationSet = instanceEventTypePrefix + "hosted_login_translation.set" +) + +type HostedLoginTranslationSetEvent struct { + eventstore.BaseEvent `json:"-"` + + Translation map[string]any `json:"translation,omitempty"` + Language language.Tag `json:"language,omitempty"` + Level string `json:"level,omitempty"` +} + +func NewHostedLoginTranslationSetEvent(ctx context.Context, aggregate *eventstore.Aggregate, translation map[string]any, language language.Tag) *HostedLoginTranslationSetEvent { + return &HostedLoginTranslationSetEvent{ + BaseEvent: *eventstore.NewBaseEventForPush(ctx, aggregate, HostedLoginTranslationSet), + Translation: translation, + Language: language, + Level: string(aggregate.Type), + } +} + +func (e *HostedLoginTranslationSetEvent) Payload() any { + return e +} + +func (e *HostedLoginTranslationSetEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return nil +} + +func (e *HostedLoginTranslationSetEvent) Fields() []*eventstore.FieldOperation { + return nil +} + +func HostedLoginTranslationSetEventMapper(event eventstore.Event) (eventstore.Event, error) { + translationSet := &HostedLoginTranslationSetEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(event), + } + err := event.Unmarshal(translationSet) + if err != nil { + return nil, zerrors.ThrowInternal(err, "INST-lOxtJJ", "unable to unmarshal hosted login translation set event") + } + + return translationSet, nil +} diff --git a/internal/repository/org/aggregate.go b/internal/repository/org/aggregate.go index 58121afbcb..ff9be33085 100644 --- a/internal/repository/org/aggregate.go +++ b/internal/repository/org/aggregate.go @@ -1,6 +1,8 @@ package org import ( + "context" + "github.com/zitadel/zitadel/internal/eventstore" ) @@ -27,3 +29,7 @@ func NewAggregate(id string) *Aggregate { }, } } + +func AggregateFromWriteModel(ctx context.Context, wm *eventstore.WriteModel) *eventstore.Aggregate { + return eventstore.AggregateFromWriteModelCtx(ctx, wm, AggregateType, AggregateVersion) +} diff --git a/internal/repository/org/eventstore.go b/internal/repository/org/eventstore.go index d1efa75dfc..289bbbc608 100644 --- a/internal/repository/org/eventstore.go +++ b/internal/repository/org/eventstore.go @@ -114,4 +114,5 @@ func init() { eventstore.RegisterFilterEventMapper(AggregateType, NotificationPolicyAddedEventType, NotificationPolicyAddedEventMapper) eventstore.RegisterFilterEventMapper(AggregateType, NotificationPolicyChangedEventType, NotificationPolicyChangedEventMapper) eventstore.RegisterFilterEventMapper(AggregateType, NotificationPolicyRemovedEventType, NotificationPolicyRemovedEventMapper) + eventstore.RegisterFilterEventMapper(AggregateType, HostedLoginTranslationSet, HostedLoginTranslationSetEventMapper) } diff --git a/internal/repository/org/hosted_login_translation.go b/internal/repository/org/hosted_login_translation.go new file mode 100644 index 0000000000..e07bdc1e3b --- /dev/null +++ b/internal/repository/org/hosted_login_translation.go @@ -0,0 +1,55 @@ +package org + +import ( + "context" + + "golang.org/x/text/language" + + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/zerrors" +) + +const ( + HostedLoginTranslationSet = orgEventTypePrefix + "hosted_login_translation.set" +) + +type HostedLoginTranslationSetEvent struct { + eventstore.BaseEvent `json:"-"` + + Translation map[string]any `json:"translation,omitempty"` + Language language.Tag `json:"language,omitempty"` + Level string `json:"level,omitempty"` +} + +func NewHostedLoginTranslationSetEvent(ctx context.Context, aggregate *eventstore.Aggregate, translation map[string]any, language language.Tag) *HostedLoginTranslationSetEvent { + return &HostedLoginTranslationSetEvent{ + BaseEvent: *eventstore.NewBaseEventForPush(ctx, aggregate, HostedLoginTranslationSet), + Translation: translation, + Language: language, + Level: string(aggregate.Type), + } +} + +func (e *HostedLoginTranslationSetEvent) Payload() any { + return e +} + +func (e *HostedLoginTranslationSetEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return nil +} + +func (e *HostedLoginTranslationSetEvent) Fields() []*eventstore.FieldOperation { + return nil +} + +func HostedLoginTranslationSetEventMapper(event eventstore.Event) (eventstore.Event, error) { + translationSet := &HostedLoginTranslationSetEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(event), + } + err := event.Unmarshal(translationSet) + if err != nil { + return nil, zerrors.ThrowInternal(err, "ORG-BH82Eb", "unable to unmarshal hosted login translation set event") + } + + return translationSet, nil +} diff --git a/internal/repository/project/aggregate.go b/internal/repository/project/aggregate.go index 5391718812..15cb24278d 100644 --- a/internal/repository/project/aggregate.go +++ b/internal/repository/project/aggregate.go @@ -1,6 +1,8 @@ package project import ( + "context" + "github.com/zitadel/zitadel/internal/eventstore" ) @@ -23,3 +25,7 @@ func NewAggregate(id, resourceOwner string) *Aggregate { }, } } + +func AggregateFromWriteModel(ctx context.Context, wm *eventstore.WriteModel) *eventstore.Aggregate { + return eventstore.AggregateFromWriteModelCtx(ctx, wm, AggregateType, AggregateVersion) +} diff --git a/internal/repository/project/project.go b/internal/repository/project/project.go index 44f882b3e1..c86fed5272 100644 --- a/internal/repository/project/project.go +++ b/internal/repository/project/project.go @@ -184,10 +184,7 @@ func NewProjectChangeEvent( aggregate *eventstore.Aggregate, oldName string, changes []ProjectChanges, -) (*ProjectChangeEvent, error) { - if len(changes) == 0 { - return nil, zerrors.ThrowPreconditionFailed(nil, "PROJECT-mV9xc", "Errors.NoChangesFound") - } +) *ProjectChangeEvent { changeEvent := &ProjectChangeEvent{ BaseEvent: *eventstore.NewBaseEventForPush( ctx, @@ -199,7 +196,7 @@ func NewProjectChangeEvent( for _, change := range changes { change(changeEvent) } - return changeEvent, nil + return changeEvent } type ProjectChanges func(event *ProjectChangeEvent) diff --git a/internal/repository/user/machine.go b/internal/repository/user/machine.go index d76290931a..a466f92fe3 100644 --- a/internal/repository/user/machine.go +++ b/internal/repository/user/machine.go @@ -88,10 +88,7 @@ func NewMachineChangedEvent( ctx context.Context, aggregate *eventstore.Aggregate, changes []MachineChanges, -) (*MachineChangedEvent, error) { - if len(changes) == 0 { - return nil, zerrors.ThrowPreconditionFailed(nil, "USER-3M9fs", "Errors.NoChangesFound") - } +) *MachineChangedEvent { changeEvent := &MachineChangedEvent{ BaseEvent: *eventstore.NewBaseEventForPush( ctx, @@ -102,7 +99,7 @@ func NewMachineChangedEvent( for _, change := range changes { change(changeEvent) } - return changeEvent, nil + return changeEvent } type MachineChanges func(event *MachineChangedEvent) diff --git a/internal/serviceping/client.go b/internal/serviceping/client.go new file mode 100644 index 0000000000..87711aada6 --- /dev/null +++ b/internal/serviceping/client.go @@ -0,0 +1,153 @@ +package serviceping + +import ( + "bytes" + "context" + "crypto/rand" + "encoding/base64" + "fmt" + "io" + "net/http" + + "google.golang.org/grpc" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/query" + analytics "github.com/zitadel/zitadel/pkg/grpc/analytics/v2beta" +) + +const ( + pathBaseInformation = "/instances" + pathResourceCounts = "/resource_counts" +) + +type Client struct { + httpClient *http.Client + endpoint string +} + +func (c Client) ReportBaseInformation(ctx context.Context, in *analytics.ReportBaseInformationRequest, opts ...grpc.CallOption) (*analytics.ReportBaseInformationResponse, error) { + reportResponse := new(analytics.ReportBaseInformationResponse) + err := c.callTelemetryService(ctx, pathBaseInformation, in, reportResponse) + if err != nil { + return nil, err + } + return reportResponse, nil +} + +func (c Client) ReportResourceCounts(ctx context.Context, in *analytics.ReportResourceCountsRequest, opts ...grpc.CallOption) (*analytics.ReportResourceCountsResponse, error) { + reportResponse := new(analytics.ReportResourceCountsResponse) + err := c.callTelemetryService(ctx, pathResourceCounts, in, reportResponse) + if err != nil { + return nil, err + } + return reportResponse, nil +} + +func (c Client) callTelemetryService(ctx context.Context, path string, in proto.Message, out proto.Message) error { + requestBody, err := protojson.Marshal(in) + if err != nil { + return err + } + req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.endpoint+path, bytes.NewReader(requestBody)) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/json") + resp, err := c.httpClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + + if resp.StatusCode != http.StatusOK { + return &TelemetryError{ + StatusCode: resp.StatusCode, + Body: body, + } + } + + return protojson.UnmarshalOptions{ + AllowPartial: true, + DiscardUnknown: true, + }.Unmarshal(body, out) +} + +func NewClient(config *Config) Client { + return Client{ + httpClient: http.DefaultClient, + endpoint: config.Endpoint, + } +} + +func GenerateSystemID() (string, error) { + randBytes := make([]byte, 64) + if _, err := rand.Read(randBytes); err != nil { + return "", err + } + return base64.RawURLEncoding.EncodeToString(randBytes), nil +} + +func instanceInformationToPb(instances *query.Instances) []*analytics.InstanceInformation { + instanceInformation := make([]*analytics.InstanceInformation, len(instances.Instances)) + for i, instance := range instances.Instances { + domains := instanceDomainToPb(instance) + instanceInformation[i] = &analytics.InstanceInformation{ + Id: instance.ID, + Domains: domains, + CreatedAt: timestamppb.New(instance.CreationDate), + } + } + return instanceInformation +} + +func instanceDomainToPb(instance *query.Instance) []string { + domains := make([]string, len(instance.Domains)) + for i, domain := range instance.Domains { + domains[i] = domain.Domain + } + return domains +} + +func resourceCountsToPb(counts []query.ResourceCount) []*analytics.ResourceCount { + resourceCounts := make([]*analytics.ResourceCount, len(counts)) + for i, count := range counts { + resourceCounts[i] = &analytics.ResourceCount{ + InstanceId: count.InstanceID, + ParentType: countParentTypeToPb(count.ParentType), + ParentId: count.ParentID, + ResourceName: count.Resource, + TableName: count.TableName, + UpdatedAt: timestamppb.New(count.UpdatedAt), + Amount: uint32(count.Amount), + } + } + return resourceCounts +} + +func countParentTypeToPb(parentType domain.CountParentType) analytics.CountParentType { + switch parentType { + case domain.CountParentTypeInstance: + return analytics.CountParentType_COUNT_PARENT_TYPE_INSTANCE + case domain.CountParentTypeOrganization: + return analytics.CountParentType_COUNT_PARENT_TYPE_ORGANIZATION + default: + return analytics.CountParentType_COUNT_PARENT_TYPE_UNSPECIFIED + } +} + +type TelemetryError struct { + StatusCode int + Body []byte +} + +func (e *TelemetryError) Error() string { + return fmt.Sprintf("telemetry error %d: %s", e.StatusCode, e.Body) +} diff --git a/internal/serviceping/config.go b/internal/serviceping/config.go new file mode 100644 index 0000000000..13f2311324 --- /dev/null +++ b/internal/serviceping/config.go @@ -0,0 +1,18 @@ +package serviceping + +type Config struct { + Enabled bool + Endpoint string + Interval string + MaxAttempts uint8 + Telemetry TelemetryConfig +} + +type TelemetryConfig struct { + ResourceCount ResourceCount +} + +type ResourceCount struct { + Enabled bool + BulkSize int +} diff --git a/internal/serviceping/mock/mock_gen.go b/internal/serviceping/mock/mock_gen.go new file mode 100644 index 0000000000..6b4d2defbe --- /dev/null +++ b/internal/serviceping/mock/mock_gen.go @@ -0,0 +1,5 @@ +package mock + +//go:generate mockgen -package mock -destination queue.mock.go github.com/zitadel/zitadel/internal/serviceping Queue +//go:generate mockgen -package mock -destination queries.mock.go github.com/zitadel/zitadel/internal/serviceping Queries +//go:generate mockgen -package mock -destination telemetry.mock.go github.com/zitadel/zitadel/pkg/grpc/analytics/v2beta TelemetryServiceClient diff --git a/internal/serviceping/mock/queries.mock.go b/internal/serviceping/mock/queries.mock.go new file mode 100644 index 0000000000..593c4d5ff7 --- /dev/null +++ b/internal/serviceping/mock/queries.mock.go @@ -0,0 +1,72 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/zitadel/zitadel/internal/serviceping (interfaces: Queries) +// +// Generated by this command: +// +// mockgen -package mock -destination queries.mock.go github.com/zitadel/zitadel/internal/serviceping Queries +// + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + reflect "reflect" + + query "github.com/zitadel/zitadel/internal/query" + gomock "go.uber.org/mock/gomock" +) + +// MockQueries is a mock of Queries interface. +type MockQueries struct { + ctrl *gomock.Controller + recorder *MockQueriesMockRecorder + isgomock struct{} +} + +// MockQueriesMockRecorder is the mock recorder for MockQueries. +type MockQueriesMockRecorder struct { + mock *MockQueries +} + +// NewMockQueries creates a new mock instance. +func NewMockQueries(ctrl *gomock.Controller) *MockQueries { + mock := &MockQueries{ctrl: ctrl} + mock.recorder = &MockQueriesMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockQueries) EXPECT() *MockQueriesMockRecorder { + return m.recorder +} + +// ListResourceCounts mocks base method. +func (m *MockQueries) ListResourceCounts(ctx context.Context, lastID, size int) ([]query.ResourceCount, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListResourceCounts", ctx, lastID, size) + ret0, _ := ret[0].([]query.ResourceCount) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListResourceCounts indicates an expected call of ListResourceCounts. +func (mr *MockQueriesMockRecorder) ListResourceCounts(ctx, lastID, size any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListResourceCounts", reflect.TypeOf((*MockQueries)(nil).ListResourceCounts), ctx, lastID, size) +} + +// SearchInstances mocks base method. +func (m *MockQueries) SearchInstances(ctx context.Context, queries *query.InstanceSearchQueries) (*query.Instances, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SearchInstances", ctx, queries) + ret0, _ := ret[0].(*query.Instances) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SearchInstances indicates an expected call of SearchInstances. +func (mr *MockQueriesMockRecorder) SearchInstances(ctx, queries any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchInstances", reflect.TypeOf((*MockQueries)(nil).SearchInstances), ctx, queries) +} diff --git a/internal/serviceping/mock/queue.mock.go b/internal/serviceping/mock/queue.mock.go new file mode 100644 index 0000000000..e984352a8c --- /dev/null +++ b/internal/serviceping/mock/queue.mock.go @@ -0,0 +1,62 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/zitadel/zitadel/internal/serviceping (interfaces: Queue) +// +// Generated by this command: +// +// mockgen -package mock -destination queue.mock.go github.com/zitadel/zitadel/internal/serviceping Queue +// + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + reflect "reflect" + + river "github.com/riverqueue/river" + queue "github.com/zitadel/zitadel/internal/queue" + gomock "go.uber.org/mock/gomock" +) + +// MockQueue is a mock of Queue interface. +type MockQueue struct { + ctrl *gomock.Controller + recorder *MockQueueMockRecorder + isgomock struct{} +} + +// MockQueueMockRecorder is the mock recorder for MockQueue. +type MockQueueMockRecorder struct { + mock *MockQueue +} + +// NewMockQueue creates a new mock instance. +func NewMockQueue(ctrl *gomock.Controller) *MockQueue { + mock := &MockQueue{ctrl: ctrl} + mock.recorder = &MockQueueMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockQueue) EXPECT() *MockQueueMockRecorder { + return m.recorder +} + +// Insert mocks base method. +func (m *MockQueue) Insert(ctx context.Context, args river.JobArgs, opts ...queue.InsertOpt) error { + m.ctrl.T.Helper() + varargs := []any{ctx, args} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Insert", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// Insert indicates an expected call of Insert. +func (mr *MockQueueMockRecorder) Insert(ctx, args any, opts ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, args}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Insert", reflect.TypeOf((*MockQueue)(nil).Insert), varargs...) +} diff --git a/internal/serviceping/mock/telemetry.mock.go b/internal/serviceping/mock/telemetry.mock.go new file mode 100644 index 0000000000..536bf34671 --- /dev/null +++ b/internal/serviceping/mock/telemetry.mock.go @@ -0,0 +1,83 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/zitadel/zitadel/pkg/grpc/analytics/v2beta (interfaces: TelemetryServiceClient) +// +// Generated by this command: +// +// mockgen -package mock -destination telemetry.mock.go github.com/zitadel/zitadel/pkg/grpc/analytics/v2beta TelemetryServiceClient +// + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + reflect "reflect" + + analytics "github.com/zitadel/zitadel/pkg/grpc/analytics/v2beta" + gomock "go.uber.org/mock/gomock" + grpc "google.golang.org/grpc" +) + +// MockTelemetryServiceClient is a mock of TelemetryServiceClient interface. +type MockTelemetryServiceClient struct { + ctrl *gomock.Controller + recorder *MockTelemetryServiceClientMockRecorder + isgomock struct{} +} + +// MockTelemetryServiceClientMockRecorder is the mock recorder for MockTelemetryServiceClient. +type MockTelemetryServiceClientMockRecorder struct { + mock *MockTelemetryServiceClient +} + +// NewMockTelemetryServiceClient creates a new mock instance. +func NewMockTelemetryServiceClient(ctrl *gomock.Controller) *MockTelemetryServiceClient { + mock := &MockTelemetryServiceClient{ctrl: ctrl} + mock.recorder = &MockTelemetryServiceClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockTelemetryServiceClient) EXPECT() *MockTelemetryServiceClientMockRecorder { + return m.recorder +} + +// ReportBaseInformation mocks base method. +func (m *MockTelemetryServiceClient) ReportBaseInformation(ctx context.Context, in *analytics.ReportBaseInformationRequest, opts ...grpc.CallOption) (*analytics.ReportBaseInformationResponse, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, in} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ReportBaseInformation", varargs...) + ret0, _ := ret[0].(*analytics.ReportBaseInformationResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ReportBaseInformation indicates an expected call of ReportBaseInformation. +func (mr *MockTelemetryServiceClientMockRecorder) ReportBaseInformation(ctx, in any, opts ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, in}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReportBaseInformation", reflect.TypeOf((*MockTelemetryServiceClient)(nil).ReportBaseInformation), varargs...) +} + +// ReportResourceCounts mocks base method. +func (m *MockTelemetryServiceClient) ReportResourceCounts(ctx context.Context, in *analytics.ReportResourceCountsRequest, opts ...grpc.CallOption) (*analytics.ReportResourceCountsResponse, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, in} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ReportResourceCounts", varargs...) + ret0, _ := ret[0].(*analytics.ReportResourceCountsResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ReportResourceCounts indicates an expected call of ReportResourceCounts. +func (mr *MockTelemetryServiceClientMockRecorder) ReportResourceCounts(ctx, in any, opts ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, in}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReportResourceCounts", reflect.TypeOf((*MockTelemetryServiceClient)(nil).ReportResourceCounts), varargs...) +} diff --git a/internal/serviceping/report.go b/internal/serviceping/report.go new file mode 100644 index 0000000000..d31f6a8f74 --- /dev/null +++ b/internal/serviceping/report.go @@ -0,0 +1,17 @@ +package serviceping + +type ReportType uint + +const ( + ReportTypeBaseInformation ReportType = iota + ReportTypeResourceCounts +) + +type ServicePingReport struct { + ReportID string + ReportType ReportType +} + +func (r *ServicePingReport) Kind() string { + return "service_ping_report" +} diff --git a/internal/serviceping/worker.go b/internal/serviceping/worker.go new file mode 100644 index 0000000000..b95dd77fa1 --- /dev/null +++ b/internal/serviceping/worker.go @@ -0,0 +1,293 @@ +package serviceping + +import ( + "context" + "errors" + "fmt" + "math/rand" + "net/http" + "time" + + "github.com/muhlemmer/gu" + "github.com/riverqueue/river" + "github.com/robfig/cron/v3" + "github.com/zitadel/logging" + + "github.com/zitadel/zitadel/cmd/build" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/queue" + "github.com/zitadel/zitadel/internal/v2/system" + "github.com/zitadel/zitadel/internal/zerrors" + analytics "github.com/zitadel/zitadel/pkg/grpc/analytics/v2beta" +) + +const ( + QueueName = "service_ping_report" + minInterval = 30 * time.Minute +) + +var ( + ErrInvalidReportType = errors.New("invalid report type") + + _ river.Worker[*ServicePingReport] = (*Worker)(nil) +) + +type Worker struct { + river.WorkerDefaults[*ServicePingReport] + + reportClient analytics.TelemetryServiceClient + db Queries + queue Queue + + config *Config + systemID string + version string +} + +type Queries interface { + SearchInstances(ctx context.Context, queries *query.InstanceSearchQueries) (*query.Instances, error) + ListResourceCounts(ctx context.Context, lastID int, size int) ([]query.ResourceCount, error) +} + +type Queue interface { + Insert(ctx context.Context, args river.JobArgs, opts ...queue.InsertOpt) error +} + +// Register implements the [queue.Worker] interface. +func (w *Worker) Register(workers *river.Workers, queues map[string]river.QueueConfig) { + river.AddWorker[*ServicePingReport](workers, w) + queues[QueueName] = river.QueueConfig{ + MaxWorkers: 1, // for now, we only use a single worker to prevent too much side effects on other queues + } +} + +// Work implements the [river.Worker] interface. +func (w *Worker) Work(ctx context.Context, job *river.Job[*ServicePingReport]) (err error) { + defer func() { + err = w.handleClientError(err) + }() + switch job.Args.ReportType { + case ReportTypeBaseInformation: + reportID, err := w.reportBaseInformation(ctx) + if err != nil { + return err + } + return w.createReportJobs(ctx, reportID) + case ReportTypeResourceCounts: + return w.reportResourceCounts(ctx, job.Args.ReportID) + default: + logging.WithFields("reportType", job.Args.ReportType, "reportID", job.Args.ReportID). + Error("unknown job type") + return river.JobCancel(ErrInvalidReportType) + } +} + +func (w *Worker) reportBaseInformation(ctx context.Context) (string, error) { + instances, err := w.db.SearchInstances(ctx, &query.InstanceSearchQueries{}) + if err != nil { + return "", err + } + instanceInformation := instanceInformationToPb(instances) + resp, err := w.reportClient.ReportBaseInformation(ctx, &analytics.ReportBaseInformationRequest{ + SystemId: w.systemID, + Version: w.version, + Instances: instanceInformation, + }) + if err != nil { + return "", err + } + return resp.GetReportId(), nil +} + +func (w *Worker) reportResourceCounts(ctx context.Context, reportID string) error { + lastID := 0 + // iterate over the resource counts until there are no more counts to report + // or the context gets cancelled + for { + select { + case <-ctx.Done(): + return nil + default: + counts, err := w.db.ListResourceCounts(ctx, lastID, w.config.Telemetry.ResourceCount.BulkSize) + if err != nil { + return err + } + // if there are no counts, we can stop the loop + if len(counts) == 0 { + return nil + } + request := &analytics.ReportResourceCountsRequest{ + SystemId: w.systemID, + ResourceCounts: resourceCountsToPb(counts), + } + if reportID != "" { + request.ReportId = gu.Ptr(reportID) + } + resp, err := w.reportClient.ReportResourceCounts(ctx, request) + if err != nil { + return err + } + // in case the resource counts returned by the database are less than the bulk size, + // we can assume that we have reached the end of the resource counts and can stop the loop + if len(counts) < w.config.Telemetry.ResourceCount.BulkSize { + return nil + } + // update the lastID for the next iteration + lastID = counts[len(counts)-1].ID + // In case we get a report ID back from the server (it could be the first call of the report), + // we update it to use it for the next batch. + if resp.GetReportId() != "" && resp.GetReportId() != reportID { + reportID = resp.GetReportId() + } + } + } +} + +func (w *Worker) handleClientError(err error) error { + telemetryError := new(TelemetryError) + if !errors.As(err, &telemetryError) { + // If the error is not a TelemetryError, we can assume that it is a transient error + // and can be retried by the queue. + return err + } + switch telemetryError.StatusCode { + case http.StatusBadRequest, + http.StatusNotFound, + http.StatusNotImplemented, + http.StatusConflict, + http.StatusPreconditionFailed: + // In case of these errors, we can assume that a retry does not make sense, + // so we can cancel the job. + return river.JobCancel(err) + default: + // As of now we assume that all other errors are transient and can be retried. + // So we just return the error, which will be handled by the queue as a failed attempt. + return err + } +} + +func (w *Worker) createReportJobs(ctx context.Context, reportID string) error { + errs := make([]error, 0) + if w.config.Telemetry.ResourceCount.Enabled { + err := w.addReportJob(ctx, reportID, ReportTypeResourceCounts) + if err != nil { + errs = append(errs, err) + } + } + return errors.Join(errs...) +} + +func (w *Worker) addReportJob(ctx context.Context, reportID string, reportType ReportType) error { + job := &ServicePingReport{ + ReportID: reportID, + ReportType: reportType, + } + return w.queue.Insert(ctx, job, + queue.WithQueueName(QueueName), + queue.WithMaxAttempts(w.config.MaxAttempts), + ) +} + +type systemIDReducer struct { + id string +} + +func (s *systemIDReducer) Reduce() error { + return nil +} + +func (s *systemIDReducer) AppendEvents(events ...eventstore.Event) { + for _, event := range events { + if idEvent, ok := event.(*system.IDGeneratedEvent); ok { + s.id = idEvent.ID + } + } +} + +func (s *systemIDReducer) Query() *eventstore.SearchQueryBuilder { + return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). + AddQuery(). + AggregateTypes(system.AggregateType). + EventTypes(system.IDGeneratedType). + Builder() +} + +func Register( + ctx context.Context, + q *queue.Queue, + queries *query.Queries, + eventstoreClient *eventstore.Eventstore, + config *Config, +) error { + if !config.Enabled { + return nil + } + systemID := new(systemIDReducer) + err := eventstoreClient.FilterToQueryReducer(ctx, systemID) + if err != nil { + return err + } + q.AddWorkers(&Worker{ + reportClient: NewClient(config), + db: queries, + queue: q, + config: config, + systemID: systemID.id, + version: build.Version(), + }) + return nil +} + +func Start(config *Config, q *queue.Queue) error { + if !config.Enabled { + return nil + } + schedule, err := parseAndValidateSchedule(config.Interval) + if err != nil { + return err + } + q.AddPeriodicJob( + schedule, + &ServicePingReport{}, + queue.WithQueueName(QueueName), + queue.WithMaxAttempts(config.MaxAttempts), + ) + return nil +} + +func parseAndValidateSchedule(interval string) (cron.Schedule, error) { + if interval == "@daily" { + interval = randomizeDaily() + } + schedule, err := cron.ParseStandard(interval) + if err != nil { + return nil, zerrors.ThrowInvalidArgument(err, "SERV-NJqiof", "invalid interval") + } + var intervalDuration time.Duration + switch s := schedule.(type) { + case *cron.SpecSchedule: + // For cron.SpecSchedule, we need to calculate the interval duration + // by getting the next time and subtracting it from the time after that. + // This is because the schedule could be a specific time, that is less than 30 minutes away, + // but still run only once a day and therefore is valid. + next := s.Next(time.Now()) + nextAfter := s.Next(next) + intervalDuration = nextAfter.Sub(next) + case cron.ConstantDelaySchedule: + intervalDuration = s.Delay + } + if intervalDuration < minInterval { + return nil, zerrors.ThrowInvalidArgumentf(nil, "SERV-FJ12", "interval must be at least %s", minInterval) + } + logging.WithFields("interval", interval).Info("scheduling service ping") + return schedule, nil +} + +// randomizeDaily generates a random time for the daily cron job +// to prevent all systems from sending the report at the same time. +func randomizeDaily() string { + minute := rand.Intn(60) + hour := rand.Intn(24) + return fmt.Sprintf("%d %d * * *", minute, hour) +} diff --git a/internal/serviceping/worker_test.go b/internal/serviceping/worker_test.go new file mode 100644 index 0000000000..f5bd38d3eb --- /dev/null +++ b/internal/serviceping/worker_test.go @@ -0,0 +1,1126 @@ +package serviceping + +import ( + "context" + "fmt" + "net/http" + "reflect" + "testing" + "time" + + "github.com/muhlemmer/gu" + "github.com/riverqueue/river" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/queue" + "github.com/zitadel/zitadel/internal/serviceping/mock" + "github.com/zitadel/zitadel/internal/zerrors" + analytics "github.com/zitadel/zitadel/pkg/grpc/analytics/v2beta" +) + +var ( + testNow = time.Now() + errInsert = fmt.Errorf("insert error") +) + +func TestWorker_reportBaseInformation(t *testing.T) { + type fields struct { + reportClient func(*testing.T) analytics.TelemetryServiceClient + db func(*testing.T) Queries + systemID string + version string + } + type args struct { + ctx context.Context + } + type want struct { + reportID string + err error + } + tests := []struct { + name string + fields fields + args args + want want + }{ + { + name: "database error, error", + fields: fields{ + db: func(t *testing.T) Queries { + ctrl := gomock.NewController(t) + queries := mock.NewMockQueries(ctrl) + queries.EXPECT().SearchInstances(gomock.Any(), &query.InstanceSearchQueries{}).Return( + nil, zerrors.ThrowInternal(nil, "id", "db error"), + ) + return queries + }, + reportClient: func(t *testing.T) analytics.TelemetryServiceClient { + return mock.NewMockTelemetryServiceClient(gomock.NewController(t)) + }, + }, + want: want{ + reportID: "", + err: zerrors.ThrowInternal(nil, "id", "db error"), + }, + }, + { + name: "telemetry client error, error", + fields: fields{ + reportClient: func(t *testing.T) analytics.TelemetryServiceClient { + client := mock.NewMockTelemetryServiceClient(gomock.NewController(t)) + client.EXPECT().ReportBaseInformation(gomock.Any(), &analytics.ReportBaseInformationRequest{ + SystemId: "system-id", + Version: "version", + Instances: []*analytics.InstanceInformation{ + { + Id: "id", + Domains: []string{"domain", "domain2"}, + CreatedAt: timestamppb.New(testNow), + }, + }, + }).Return( + nil, status.Error(codes.Internal, "error"), + ) + return client + }, + db: func(t *testing.T) Queries { + queries := mock.NewMockQueries(gomock.NewController(t)) + queries.EXPECT().SearchInstances(gomock.Any(), &query.InstanceSearchQueries{}).Return( + &query.Instances{ + Instances: []*query.Instance{ + { + ID: "id", + CreationDate: testNow, + Domains: []*query.InstanceDomain{ + { + Domain: "domain", + }, + { + Domain: "domain2", + }, + }, + }, + }, + }, + nil, + ) + return queries + }, + systemID: "system-id", + version: "version", + }, + want: want{ + reportID: "", + err: status.Error(codes.Internal, "error"), + }, + }, + { + name: "report ok, reportID returned", + fields: fields{ + db: func(t *testing.T) Queries { + queries := mock.NewMockQueries(gomock.NewController(t)) + queries.EXPECT().SearchInstances(gomock.Any(), &query.InstanceSearchQueries{}).Return( + &query.Instances{ + Instances: []*query.Instance{ + { + ID: "id", + CreationDate: testNow, + Domains: []*query.InstanceDomain{ + { + Domain: "domain", + }, + { + Domain: "domain2", + }, + }, + }, + }, + }, + nil, + ) + return queries + }, + reportClient: func(t *testing.T) analytics.TelemetryServiceClient { + client := mock.NewMockTelemetryServiceClient(gomock.NewController(t)) + client.EXPECT().ReportBaseInformation(gomock.Any(), &analytics.ReportBaseInformationRequest{ + SystemId: "system-id", + Version: "version", + Instances: []*analytics.InstanceInformation{ + { + Id: "id", + Domains: []string{"domain", "domain2"}, + CreatedAt: timestamppb.New(testNow), + }, + }, + }).Return( + &analytics.ReportBaseInformationResponse{ReportId: "report-id"}, nil, + ) + return client + }, + systemID: "system-id", + version: "version", + }, + want: want{ + reportID: "report-id", + err: nil, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + w := &Worker{ + reportClient: tt.fields.reportClient(t), + db: tt.fields.db(t), + systemID: tt.fields.systemID, + version: tt.fields.version, + } + got, err := w.reportBaseInformation(tt.args.ctx) + assert.Equal(t, tt.want.reportID, got) + assert.ErrorIs(t, err, tt.want.err) + }) + } +} + +func TestWorker_reportResourceCounts(t *testing.T) { + type fields struct { + reportClient func(*testing.T) analytics.TelemetryServiceClient + db func(*testing.T) Queries + config *Config + systemID string + } + type args struct { + ctx context.Context + reportID string + } + tests := []struct { + name string + fields fields + args args + wantErr error + }{ + { + name: "database error, error", + fields: fields{ + db: func(t *testing.T) Queries { + queries := mock.NewMockQueries(gomock.NewController(t)) + queries.EXPECT().ListResourceCounts(gomock.Any(), 0, 1).Return( + nil, zerrors.ThrowInternal(nil, "id", "db error"), + ) + return queries + }, + reportClient: func(t *testing.T) analytics.TelemetryServiceClient { + return mock.NewMockTelemetryServiceClient(gomock.NewController(t)) + }, + config: &Config{ + Telemetry: TelemetryConfig{ + ResourceCount: ResourceCount{ + BulkSize: 1, + }, + }, + }, + systemID: "system-id", + }, + args: args{ + ctx: context.Background(), + reportID: "", + }, + wantErr: zerrors.ThrowInternal(nil, "id", "db error"), + }, + { + name: "no resource counts, no error", + fields: fields{ + db: func(t *testing.T) Queries { + queries := mock.NewMockQueries(gomock.NewController(t)) + queries.EXPECT().ListResourceCounts(gomock.Any(), 0, 1).Return( + []query.ResourceCount{}, nil, + ) + return queries + }, + reportClient: func(t *testing.T) analytics.TelemetryServiceClient { + return mock.NewMockTelemetryServiceClient(gomock.NewController(t)) + }, + config: &Config{ + Telemetry: TelemetryConfig{ + ResourceCount: ResourceCount{ + BulkSize: 1, + }, + }, + }, + systemID: "system-id", + }, + args: args{ + ctx: context.Background(), + reportID: "", + }, + wantErr: nil, + }, + { + name: "telemetry client error, error", + fields: fields{ + db: func(t *testing.T) Queries { + queries := mock.NewMockQueries(gomock.NewController(t)) + queries.EXPECT().ListResourceCounts(gomock.Any(), 0, 2).Return( + []query.ResourceCount{ + { + ID: 1, + InstanceID: "instance-id", + TableName: "table_name", + ParentType: domain.CountParentTypeInstance, + ParentID: "instance-id", + Resource: "resource", + UpdatedAt: testNow, + Amount: 10, + }, + }, nil, + ) + return queries + }, + reportClient: func(t *testing.T) analytics.TelemetryServiceClient { + client := mock.NewMockTelemetryServiceClient(gomock.NewController(t)) + client.EXPECT().ReportResourceCounts(gomock.Any(), &analytics.ReportResourceCountsRequest{ + SystemId: "system-id", + ReportId: nil, + ResourceCounts: []*analytics.ResourceCount{ + { + InstanceId: "instance-id", + TableName: "table_name", + ParentType: analytics.CountParentType_COUNT_PARENT_TYPE_INSTANCE, + ParentId: "instance-id", + ResourceName: "resource", + UpdatedAt: timestamppb.New(testNow), + Amount: 10, + }, + }, + }).Return( + nil, status.Error(codes.Internal, "error"), + ) + return client + }, + config: &Config{ + Telemetry: TelemetryConfig{ + ResourceCount: ResourceCount{ + BulkSize: 2, + }, + }, + }, + systemID: "system-id", + }, + args: args{ + ctx: context.Background(), + reportID: "", + }, + wantErr: status.Error(codes.Internal, "error"), + }, + { + name: "report ok, no additional counts, no error", + fields: fields{ + db: func(t *testing.T) Queries { + queries := mock.NewMockQueries(gomock.NewController(t)) + queries.EXPECT().ListResourceCounts(gomock.Any(), 0, 2).Return( + []query.ResourceCount{ + { + ID: 1, + InstanceID: "instance-id", + TableName: "table_name", + ParentType: domain.CountParentTypeInstance, + ParentID: "instance-id", + Resource: "resource", + UpdatedAt: testNow, + Amount: 10, + }, + }, nil, + ) + return queries + }, + reportClient: func(t *testing.T) analytics.TelemetryServiceClient { + client := mock.NewMockTelemetryServiceClient(gomock.NewController(t)) + client.EXPECT().ReportResourceCounts(gomock.Any(), &analytics.ReportResourceCountsRequest{ + SystemId: "system-id", + ReportId: nil, + ResourceCounts: []*analytics.ResourceCount{ + { + InstanceId: "instance-id", + TableName: "table_name", + ParentType: analytics.CountParentType_COUNT_PARENT_TYPE_INSTANCE, + ParentId: "instance-id", + ResourceName: "resource", + UpdatedAt: timestamppb.New(testNow), + Amount: 10, + }, + }, + }).Return( + &analytics.ReportResourceCountsResponse{ + ReportId: "report-id", + }, nil, + ) + return client + }, + config: &Config{ + Telemetry: TelemetryConfig{ + ResourceCount: ResourceCount{ + BulkSize: 2, + }, + }, + }, + systemID: "system-id", + }, + args: args{ + ctx: context.Background(), + reportID: "", + }, + wantErr: nil, + }, + { + name: "report ok, additional counts, no error", + fields: fields{ + db: func(t *testing.T) Queries { + queries := mock.NewMockQueries(gomock.NewController(t)) + queries.EXPECT().ListResourceCounts(gomock.Any(), 0, 2).Return( + []query.ResourceCount{ + { + ID: 1, + InstanceID: "instance-id", + TableName: "table_name", + ParentType: domain.CountParentTypeInstance, + ParentID: "instance-id", + Resource: "resource", + UpdatedAt: testNow, + Amount: 10, + }, + { + ID: 2, + InstanceID: "instance-id2", + TableName: "table_name", + ParentType: domain.CountParentTypeInstance, + ParentID: "instance-id2", + Resource: "resource", + UpdatedAt: testNow, + Amount: 5, + }, + }, nil, + ) + queries.EXPECT().ListResourceCounts(gomock.Any(), 2, 2).Return( + []query.ResourceCount{ + { + ID: 3, + InstanceID: "instance-id3", + TableName: "table_name", + ParentType: domain.CountParentTypeInstance, + ParentID: "instance-id3", + Resource: "resource", + UpdatedAt: testNow, + Amount: 20, + }, + }, nil, + ) + return queries + }, + reportClient: func(t *testing.T) analytics.TelemetryServiceClient { + client := mock.NewMockTelemetryServiceClient(gomock.NewController(t)) + client.EXPECT().ReportResourceCounts(gomock.Any(), &analytics.ReportResourceCountsRequest{ + SystemId: "system-id", + ReportId: nil, + ResourceCounts: []*analytics.ResourceCount{ + { + InstanceId: "instance-id", + TableName: "table_name", + ParentType: analytics.CountParentType_COUNT_PARENT_TYPE_INSTANCE, + ParentId: "instance-id", + ResourceName: "resource", + UpdatedAt: timestamppb.New(testNow), + Amount: 10, + }, + { + InstanceId: "instance-id2", + TableName: "table_name", + ParentType: analytics.CountParentType_COUNT_PARENT_TYPE_INSTANCE, + ParentId: "instance-id2", + ResourceName: "resource", + UpdatedAt: timestamppb.New(testNow), + Amount: 5, + }, + }, + }).Return( + &analytics.ReportResourceCountsResponse{ + ReportId: "report-id", + }, nil, + ) + client.EXPECT().ReportResourceCounts(gomock.Any(), &analytics.ReportResourceCountsRequest{ + SystemId: "system-id", + ReportId: gu.Ptr("report-id"), + ResourceCounts: []*analytics.ResourceCount{ + { + InstanceId: "instance-id3", + TableName: "table_name", + ParentType: analytics.CountParentType_COUNT_PARENT_TYPE_INSTANCE, + ParentId: "instance-id3", + ResourceName: "resource", + UpdatedAt: timestamppb.New(testNow), + Amount: 20, + }, + }, + }).Return( + &analytics.ReportResourceCountsResponse{ + ReportId: "report-id", + }, nil, + ) + return client + }, + config: &Config{ + Telemetry: TelemetryConfig{ + ResourceCount: ResourceCount{ + BulkSize: 2, + }, + }, + }, + systemID: "system-id", + }, + args: args{ + ctx: context.Background(), + reportID: "", + }, + wantErr: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + w := &Worker{ + reportClient: tt.fields.reportClient(t), + db: tt.fields.db(t), + config: tt.fields.config, + systemID: tt.fields.systemID, + } + err := w.reportResourceCounts(tt.args.ctx, tt.args.reportID) + assert.ErrorIs(t, err, tt.wantErr) + }) + } +} + +func TestWorker_Work(t *testing.T) { + type fields struct { + WorkerDefaults river.WorkerDefaults[*ServicePingReport] + reportClient func(*testing.T) analytics.TelemetryServiceClient + db func(*testing.T) Queries + queue func(*testing.T) Queue + config *Config + systemID string + version string + } + type args struct { + ctx context.Context + job *river.Job[*ServicePingReport] + } + tests := []struct { + name string + fields fields + args args + wantErr error + }{ + { + name: "unknown report type, cancel job", + fields: fields{ + db: func(t *testing.T) Queries { + return mock.NewMockQueries(gomock.NewController(t)) + }, + reportClient: func(t *testing.T) analytics.TelemetryServiceClient { + return mock.NewMockTelemetryServiceClient(gomock.NewController(t)) + }, + queue: func(t *testing.T) Queue { + return mock.NewMockQueue(gomock.NewController(t)) + }, + }, + args: args{ + ctx: context.Background(), + job: &river.Job[*ServicePingReport]{ + Args: &ServicePingReport{ + ReportType: 100000, + }, + }, + }, + wantErr: river.JobCancel(ErrInvalidReportType), + }, + { + name: "report base information, database error, retry job", + fields: fields{ + db: func(t *testing.T) Queries { + queries := mock.NewMockQueries(gomock.NewController(t)) + queries.EXPECT().SearchInstances(gomock.Any(), &query.InstanceSearchQueries{}).Return( + nil, zerrors.ThrowInternal(nil, "id", "db error"), + ) + return queries + }, + reportClient: func(t *testing.T) analytics.TelemetryServiceClient { + return mock.NewMockTelemetryServiceClient(gomock.NewController(t)) + }, + queue: func(t *testing.T) Queue { + return mock.NewMockQueue(gomock.NewController(t)) + }, + }, + args: args{ + ctx: context.Background(), + job: &river.Job[*ServicePingReport]{ + Args: &ServicePingReport{ + ReportType: ReportTypeBaseInformation, + }, + }, + }, + wantErr: zerrors.ThrowInternal(nil, "id", "db error"), + }, + { + name: "report base information, config error, cancel job", + fields: fields{ + db: func(t *testing.T) Queries { + queries := mock.NewMockQueries(gomock.NewController(t)) + queries.EXPECT().SearchInstances(gomock.Any(), &query.InstanceSearchQueries{}).Return( + &query.Instances{ + Instances: []*query.Instance{ + { + ID: "id", + CreationDate: testNow, + Domains: []*query.InstanceDomain{ + { + Domain: "domain", + }, + }, + }, + }, + }, nil, + ) + return queries + }, + reportClient: func(t *testing.T) analytics.TelemetryServiceClient { + client := mock.NewMockTelemetryServiceClient(gomock.NewController(t)) + client.EXPECT().ReportBaseInformation(gomock.Any(), &analytics.ReportBaseInformationRequest{ + SystemId: "system-id", + Version: "version", + Instances: []*analytics.InstanceInformation{ + { + Id: "id", + Domains: []string{"domain"}, + CreatedAt: timestamppb.New(testNow), + }, + }, + }).Return( + nil, &TelemetryError{StatusCode: http.StatusNotFound, Body: []byte("endpoint not found")}, + ) + return client + }, + queue: func(t *testing.T) Queue { + return mock.NewMockQueue(gomock.NewController(t)) + }, + systemID: "system-id", + version: "version", + }, + args: args{ + ctx: context.Background(), + job: &river.Job[*ServicePingReport]{ + Args: &ServicePingReport{ + ReportType: ReportTypeBaseInformation, + }, + }, + }, + wantErr: river.JobCancel(&TelemetryError{StatusCode: http.StatusNotFound, Body: []byte("endpoint not found")}), + }, + { + name: "report base information, no reports enabled, no error", + fields: fields{ + db: func(t *testing.T) Queries { + queries := mock.NewMockQueries(gomock.NewController(t)) + queries.EXPECT().SearchInstances(gomock.Any(), &query.InstanceSearchQueries{}).Return( + &query.Instances{ + Instances: []*query.Instance{ + { + ID: "id", + CreationDate: testNow, + Domains: []*query.InstanceDomain{ + { + Domain: "domain", + }, + }, + }, + }, + }, nil, + ) + return queries + }, + reportClient: func(t *testing.T) analytics.TelemetryServiceClient { + client := mock.NewMockTelemetryServiceClient(gomock.NewController(t)) + client.EXPECT().ReportBaseInformation(gomock.Any(), &analytics.ReportBaseInformationRequest{ + SystemId: "system-id", + Version: "version", + Instances: []*analytics.InstanceInformation{ + { + Id: "id", + Domains: []string{"domain"}, + CreatedAt: timestamppb.New(testNow), + }, + }, + }).Return( + &analytics.ReportBaseInformationResponse{ + ReportId: "report-id", + }, nil, + ) + return client + }, + queue: func(t *testing.T) Queue { + return mock.NewMockQueue(gomock.NewController(t)) + }, + config: &Config{ + Telemetry: TelemetryConfig{ + ResourceCount: ResourceCount{ + Enabled: false, + }, + }, + }, + systemID: "system-id", + version: "version", + }, + args: args{ + ctx: context.Background(), + job: &river.Job[*ServicePingReport]{ + Args: &ServicePingReport{ + ReportType: ReportTypeBaseInformation, + }, + }, + }, + }, + { + name: "report base information, job creation error, cancel job", + fields: fields{ + db: func(t *testing.T) Queries { + queries := mock.NewMockQueries(gomock.NewController(t)) + queries.EXPECT().SearchInstances(gomock.Any(), &query.InstanceSearchQueries{}).Return( + &query.Instances{ + Instances: []*query.Instance{ + { + ID: "id", + CreationDate: testNow, + Domains: []*query.InstanceDomain{ + { + Domain: "domain", + }, + }, + }, + }, + }, nil, + ) + return queries + }, + reportClient: func(t *testing.T) analytics.TelemetryServiceClient { + client := mock.NewMockTelemetryServiceClient(gomock.NewController(t)) + client.EXPECT().ReportBaseInformation(gomock.Any(), &analytics.ReportBaseInformationRequest{ + SystemId: "system-id", + Version: "version", + Instances: []*analytics.InstanceInformation{ + { + Id: "id", + Domains: []string{"domain"}, + CreatedAt: timestamppb.New(testNow), + }, + }, + }).Return( + &analytics.ReportBaseInformationResponse{ + ReportId: "report-id", + }, nil, + ) + return client + }, + queue: func(t *testing.T) Queue { + q := mock.NewMockQueue(gomock.NewController(t)) + q.EXPECT().Insert(gomock.Any(), + &ServicePingReport{ + ReportID: "report-id", + ReportType: ReportTypeResourceCounts, + }, + gomock.AssignableToTypeOf(reflect.TypeOf(queue.WithQueueName(QueueName))), + gomock.AssignableToTypeOf(reflect.TypeOf(queue.WithMaxAttempts(5)))). // TODO: better solution + Return(errInsert) + return q + }, + config: &Config{ + MaxAttempts: 5, + Telemetry: TelemetryConfig{ + ResourceCount: ResourceCount{ + Enabled: true, + }, + }, + }, + systemID: "system-id", + version: "version", + }, + args: args{ + ctx: context.Background(), + job: &river.Job[*ServicePingReport]{ + Args: &ServicePingReport{ + ReportType: ReportTypeBaseInformation, + }, + }, + }, + wantErr: errInsert, + }, + { + name: "report base information, success, no error", + fields: fields{ + db: func(t *testing.T) Queries { + queries := mock.NewMockQueries(gomock.NewController(t)) + queries.EXPECT().SearchInstances(gomock.Any(), &query.InstanceSearchQueries{}).Return( + &query.Instances{ + Instances: []*query.Instance{ + { + ID: "id", + CreationDate: testNow, + Domains: []*query.InstanceDomain{ + { + Domain: "domain", + }, + }, + }, + }, + }, nil, + ) + return queries + }, + reportClient: func(t *testing.T) analytics.TelemetryServiceClient { + client := mock.NewMockTelemetryServiceClient(gomock.NewController(t)) + client.EXPECT().ReportBaseInformation(gomock.Any(), &analytics.ReportBaseInformationRequest{ + SystemId: "system-id", + Version: "version", + Instances: []*analytics.InstanceInformation{ + { + Id: "id", + Domains: []string{"domain"}, + CreatedAt: timestamppb.New(testNow), + }, + }, + }).Return( + &analytics.ReportBaseInformationResponse{ + ReportId: "report-id", + }, nil, + ) + return client + }, + queue: func(t *testing.T) Queue { + q := mock.NewMockQueue(gomock.NewController(t)) + q.EXPECT().Insert(gomock.Any(), + &ServicePingReport{ + ReportID: "report-id", + ReportType: ReportTypeResourceCounts, + }, + gomock.AssignableToTypeOf(reflect.TypeOf(queue.WithQueueName(QueueName))), + gomock.AssignableToTypeOf(reflect.TypeOf(queue.WithMaxAttempts(5)))). + Return(nil) + return q + }, + config: &Config{ + MaxAttempts: 5, + Telemetry: TelemetryConfig{ + ResourceCount: ResourceCount{ + Enabled: true, + }, + }, + }, + systemID: "system-id", + version: "version", + }, + args: args{ + ctx: context.Background(), + job: &river.Job[*ServicePingReport]{ + Args: &ServicePingReport{ + ReportType: ReportTypeBaseInformation, + }, + }, + }, + }, + { + name: "report resource counts, service unavailable, retry job", + fields: fields{ + db: func(t *testing.T) Queries { + queries := mock.NewMockQueries(gomock.NewController(t)) + queries.EXPECT().ListResourceCounts(gomock.Any(), 0, 2).Return( + []query.ResourceCount{ + { + ID: 1, + InstanceID: "instance-id", + TableName: "table_name", + ParentType: domain.CountParentTypeInstance, + ParentID: "instance-id", + Resource: "resource", + UpdatedAt: testNow, + Amount: 10, + }, + }, nil, + ) + return queries + }, + reportClient: func(t *testing.T) analytics.TelemetryServiceClient { + client := mock.NewMockTelemetryServiceClient(gomock.NewController(t)) + client.EXPECT().ReportResourceCounts(gomock.Any(), &analytics.ReportResourceCountsRequest{ + SystemId: "system-id", + ReportId: gu.Ptr("report-id"), + ResourceCounts: []*analytics.ResourceCount{ + { + InstanceId: "instance-id", + TableName: "table_name", + ParentType: analytics.CountParentType_COUNT_PARENT_TYPE_INSTANCE, + ParentId: "instance-id", + ResourceName: "resource", + UpdatedAt: timestamppb.New(testNow), + Amount: 10, + }, + }, + }).Return( + nil, status.Error(codes.Unavailable, "service unavailable"), + ) + return client + }, + queue: func(t *testing.T) Queue { + return mock.NewMockQueue(gomock.NewController(t)) + }, + config: &Config{ + Telemetry: TelemetryConfig{ + ResourceCount: ResourceCount{ + BulkSize: 2, + }, + }, + }, + systemID: "system-id", + }, + args: args{ + ctx: context.Background(), + job: &river.Job[*ServicePingReport]{ + Args: &ServicePingReport{ + ReportType: ReportTypeResourceCounts, + ReportID: "report-id", + }, + }, + }, + wantErr: status.Error(codes.Unavailable, "service unavailable"), + }, + { + name: "report resource counts, precondition error, cancel job", + fields: fields{ + db: func(t *testing.T) Queries { + queries := mock.NewMockQueries(gomock.NewController(t)) + queries.EXPECT().ListResourceCounts(gomock.Any(), 0, 2).Return( + []query.ResourceCount{ + { + ID: 1, + InstanceID: "instance-id", + TableName: "table_name", + ParentType: domain.CountParentTypeInstance, + ParentID: "instance-id", + Resource: "resource", + UpdatedAt: testNow, + Amount: 10, + }, + }, nil, + ) + return queries + }, + reportClient: func(t *testing.T) analytics.TelemetryServiceClient { + client := mock.NewMockTelemetryServiceClient(gomock.NewController(t)) + client.EXPECT().ReportResourceCounts(gomock.Any(), &analytics.ReportResourceCountsRequest{ + SystemId: "system-id", + ReportId: gu.Ptr("report-id"), + ResourceCounts: []*analytics.ResourceCount{ + { + InstanceId: "instance-id", + TableName: "table_name", + ParentType: analytics.CountParentType_COUNT_PARENT_TYPE_INSTANCE, + ParentId: "instance-id", + ResourceName: "resource", + UpdatedAt: timestamppb.New(testNow), + Amount: 10, + }, + }, + }).Return( + nil, &TelemetryError{StatusCode: http.StatusPreconditionFailed, Body: []byte("report too old")}, + ) + return client + }, + queue: func(t *testing.T) Queue { + return mock.NewMockQueue(gomock.NewController(t)) + }, + config: &Config{ + Telemetry: TelemetryConfig{ + ResourceCount: ResourceCount{ + BulkSize: 2, + }, + }, + }, + systemID: "system-id", + }, + args: args{ + ctx: context.Background(), + job: &river.Job[*ServicePingReport]{ + Args: &ServicePingReport{ + ReportID: "report-id", + ReportType: ReportTypeResourceCounts, + }, + }, + }, + wantErr: river.JobCancel(&TelemetryError{StatusCode: http.StatusPreconditionFailed, Body: []byte("report too old")}), + }, + { + name: "report resource counts, success, no error", + fields: fields{ + db: func(t *testing.T) Queries { + queries := mock.NewMockQueries(gomock.NewController(t)) + queries.EXPECT().ListResourceCounts(gomock.Any(), 0, 2).Return( + []query.ResourceCount{ + { + ID: 1, + InstanceID: "instance-id", + TableName: "table_name", + ParentType: domain.CountParentTypeInstance, + ParentID: "instance-id", + Resource: "resource", + UpdatedAt: testNow, + Amount: 10, + }, + }, nil, + ) + return queries + }, + reportClient: func(t *testing.T) analytics.TelemetryServiceClient { + client := mock.NewMockTelemetryServiceClient(gomock.NewController(t)) + client.EXPECT().ReportResourceCounts(gomock.Any(), &analytics.ReportResourceCountsRequest{ + SystemId: "system-id", + ReportId: gu.Ptr("report-id"), + ResourceCounts: []*analytics.ResourceCount{ + { + InstanceId: "instance-id", + TableName: "table_name", + ParentType: analytics.CountParentType_COUNT_PARENT_TYPE_INSTANCE, + ParentId: "instance-id", + ResourceName: "resource", + UpdatedAt: timestamppb.New(testNow), + Amount: 10, + }, + }, + }).Return( + &analytics.ReportResourceCountsResponse{ + ReportId: "report-id", + }, nil, + ) + return client + }, + queue: func(t *testing.T) Queue { + return mock.NewMockQueue(gomock.NewController(t)) + }, + config: &Config{ + Telemetry: TelemetryConfig{ + ResourceCount: ResourceCount{ + BulkSize: 2, + }, + }, + }, + systemID: "system-id", + }, + args: args{ + ctx: context.Background(), + job: &river.Job[*ServicePingReport]{ + Args: &ServicePingReport{ + ReportID: "report-id", + ReportType: ReportTypeResourceCounts, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + w := &Worker{ + WorkerDefaults: river.WorkerDefaults[*ServicePingReport]{}, + reportClient: tt.fields.reportClient(t), + db: tt.fields.db(t), + queue: tt.fields.queue(t), + config: tt.fields.config, + systemID: tt.fields.systemID, + version: tt.fields.version, + } + err := w.Work(tt.args.ctx, tt.args.job) + assert.ErrorIs(t, err, tt.wantErr) + }) + } +} + +func Test_parseAndValidateSchedule(t *testing.T) { + type args struct { + interval string + } + tests := []struct { + name string + args args + wantNextStart time.Time + wantNextEnd time.Time + wantErr error + }{ + { + name: "@daily, returns randomized daily schedule", + args: args{ + interval: "@daily", + }, + wantNextStart: time.Now(), + wantNextEnd: time.Now().Add(24 * time.Hour), + }, + { + name: "invalid cron expression, returns error", + args: args{ + interval: "invalid cron", + }, + wantErr: zerrors.ThrowInvalidArgument(nil, "SERV-NJqiof", "invalid interval"), + }, + { + name: "valid cron expression, returns schedule", + args: args{ + interval: "0 0 * * *", + }, + wantNextStart: nextMidnight(), + wantNextEnd: nextMidnight(), + }, + { + name: "valid cron expression (extended syntax), returns schedule", + args: args{ + interval: "@midnight", + }, + wantNextStart: nextMidnight(), + wantNextEnd: nextMidnight(), + }, + { + name: "less than minInterval, returns error", + args: args{ + interval: "0/15 * * * *", + }, + wantErr: zerrors.ThrowInvalidArgumentf(nil, "SERV-FJ12", "interval must be at least %s", minInterval), + }, + { + name: "less than minInterval (extended syntax), returns error", + args: args{ + interval: "@every 15m", + }, + wantErr: zerrors.ThrowInvalidArgumentf(nil, "SERV-FJ12", "interval must be at least %s", minInterval), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := parseAndValidateSchedule(tt.args.interval) + assert.ErrorIs(t, err, tt.wantErr) + if tt.wantErr == nil { + now := time.Now() + assert.WithinRange(t, got.Next(now), tt.wantNextStart, tt.wantNextEnd) + } + }) + } +} + +func nextMidnight() time.Time { + year, month, day := time.Now().Date() + return time.Date(year, month, day+1, 0, 0, 0, 0, time.Local) +} diff --git a/internal/static/i18n/bg.yaml b/internal/static/i18n/bg.yaml index 898b776c23..b0e50fa9f1 100644 --- a/internal/static/i18n/bg.yaml +++ b/internal/static/i18n/bg.yaml @@ -117,6 +117,7 @@ Errors: AlreadyVerified: Телефонът вече е потвърден Empty: Телефонът е празен NotChanged: Телефонът не е сменен + VerifyingRemovalIsNotSupported: Премахването на проверката не се поддържа Address: NotFound: Адресът не е намерен NotChanged: Адресът не е променен @@ -195,7 +196,7 @@ Errors: AlreadyExists: Екземплярът вече съществува NotChanged: Екземплярът не е променен Org: - AlreadyExists: Името на организацията вече е заето + AlreadyExists: Името или идентификационният номер на организацията вече е зает. Invalid: Организацията е невалидна AlreadyDeactivated: Организацията вече е деактивирана AlreadyActive: Организацията вече е активна @@ -448,8 +449,6 @@ Errors: Invalid: Потребителското разрешение е невалидно NotChanged: Потребителското разрешение не е променено IDMissing: ID липсва - NotActive: Потребителското разрешение не е активно - NotInactive: Предоставянето на потребител не е деактивирано NoPermissionForProject: Потребителят няма разрешения за този проект RoleKeyNotFound: Ролята не е намерена Member: diff --git a/internal/static/i18n/cs.yaml b/internal/static/i18n/cs.yaml index 01ceeb0896..d7fe164e38 100644 --- a/internal/static/i18n/cs.yaml +++ b/internal/static/i18n/cs.yaml @@ -115,6 +115,7 @@ Errors: AlreadyVerified: Telefon již ověřen Empty: Telefon je prázdný NotChanged: Telefon nezměněn + VerifyingRemovalIsNotSupported: Ověření odstranění telefonu není podporováno Address: NotFound: Adresa nenalezena NotChanged: Adresa nezměněna @@ -193,7 +194,7 @@ Errors: AlreadyExists: Instance již existuje NotChanged: Instance nezměněna Org: - AlreadyExists: Název organizace je již obsazen + AlreadyExists: Името или идентификационният номер на организацията вече е зает Invalid: Organizace je neplatná AlreadyDeactivated: Organizace je již deaktivována AlreadyActive: Organizace je již aktivní @@ -436,8 +437,6 @@ Errors: Invalid: Uživatelský grant je neplatný NotChanged: Uživatelský grant nebyl změněn IDMissing: Chybí Id - NotActive: Uživatelský grant není aktivní - NotInactive: Uživatelský grant není deaktivován NoPermissionForProject: Uživatel nemá na tomto projektu žádná oprávnění RoleKeyNotFound: Role nenalezena Member: diff --git a/internal/static/i18n/de.yaml b/internal/static/i18n/de.yaml index 32be7c15a5..1f18fe5f27 100644 --- a/internal/static/i18n/de.yaml +++ b/internal/static/i18n/de.yaml @@ -115,6 +115,7 @@ Errors: AlreadyVerified: Telefonnummer bereits verifiziert Empty: Telefonnummer ist leer NotChanged: Telefonnummer wurde nicht geändert + VerifyingRemovalIsNotSupported: Verifizieren der Telefonnummer Entfernung wird nicht unterstützt Address: NotFound: Adresse nicht gefunden NotChanged: Adresse wurde nicht geändert @@ -193,7 +194,7 @@ Errors: AlreadyExists: Instanz exisitiert bereits NotChanged: Instanz wurde nicht verändert Org: - AlreadyExists: Organisationsname existiert bereits + AlreadyExists: Der Name oder die ID der Organisation ist bereits vorhanden Invalid: Organisation ist ungültig AlreadyDeactivated: Organisation ist bereits deaktiviert AlreadyActive: Organisation ist bereits aktiv @@ -436,8 +437,6 @@ Errors: Invalid: Benutzer Berechtigung ist ungültig NotChanged: Benutzer Berechtigung wurde nicht verändert IDMissing: ID fehlt - NotActive: Benutzer Berechtigung ist nicht aktiv - NotInactive: Benutzer Berechtigung ist nicht deaktiviert NoPermissionForProject: Benutzer hat keine Rechte auf diesem Projekt RoleKeyNotFound: Rolle konnte nicht gefunden werden Member: diff --git a/internal/static/i18n/en.yaml b/internal/static/i18n/en.yaml index 2f1adf83f5..fcf566e788 100644 --- a/internal/static/i18n/en.yaml +++ b/internal/static/i18n/en.yaml @@ -116,6 +116,7 @@ Errors: AlreadyVerified: Phone already verified Empty: Phone is empty NotChanged: Phone not changed + VerifyingRemovalIsNotSupported: Verifying phone removal is not supported Address: NotFound: Address not found NotChanged: Address not changed @@ -194,7 +195,7 @@ Errors: AlreadyExists: Instance already exists NotChanged: Instance not changed Org: - AlreadyExists: Organisation's name already taken + AlreadyExists: Organisation's name or id already taken Invalid: Organisation is invalid AlreadyDeactivated: Organisation is already deactivated AlreadyActive: Organisation is already active @@ -437,8 +438,6 @@ Errors: Invalid: User grant is invalid NotChanged: User grant has not been changed IDMissing: Id missing - NotActive: User grant is not active - NotInactive: User grant is not deactivated NoPermissionForProject: User has no permissions on this project RoleKeyNotFound: Role not found Member: diff --git a/internal/static/i18n/es.yaml b/internal/static/i18n/es.yaml index 20a38b3bb9..daffbd452a 100644 --- a/internal/static/i18n/es.yaml +++ b/internal/static/i18n/es.yaml @@ -115,6 +115,7 @@ Errors: AlreadyVerified: El teléfono ya se verificó Empty: El teléfono está vacío NotChanged: El teléfono no ha cambiado + VerifyingRemovalIsNotSupported: La verificación de eliminación no está soportada Address: NotFound: Dirección no encontrada NotChanged: La dirección no ha cambiado @@ -193,7 +194,7 @@ Errors: AlreadyExists: La instancia ya existe NotChanged: La instancia no ha cambiado Org: - AlreadyExists: El nombre de la organización ya está cogido + AlreadyExists: El nombre o id de la organización ya está tomado Invalid: El nombre de la organización no es válido AlreadyDeactivated: La organización ya está desactivada AlreadyActive: La organización ya está activada @@ -436,8 +437,6 @@ Errors: Invalid: La concesión de usuario no es válida NotChanged: La concesión de usuario no ha cambiado IDMissing: Falta Id - NotActive: La concesión de usuario no está activa - NotInactive: La concesión de usuario no está inactiva NoPermissionForProject: El usuario no tiene permisos en este proyecto RoleKeyNotFound: Rol no encontrado Member: diff --git a/internal/static/i18n/fr.yaml b/internal/static/i18n/fr.yaml index 760d8f186b..300adcd452 100644 --- a/internal/static/i18n/fr.yaml +++ b/internal/static/i18n/fr.yaml @@ -115,6 +115,7 @@ Errors: AlreadyVerified: Téléphone déjà vérifié Empty: Téléphone est vide NotChanged: Téléphone n'a pas changé + VerifyingRemovalIsNotSupported: La vérification de la suppression n'est pas prise en charge Address: NotFound: Adresse non trouvée NotChanged: L'adresse n'a pas changé @@ -193,7 +194,7 @@ Errors: AlreadyExists: L'instance existe déjà NotChanged: L'instance n'a pas changé Org: - AlreadyExists: Le nom de l'organisation est déjà pris + AlreadyExists: Le nom de l'organisation ou l'identifiant est déjà pris Invalid: L'organisation n'est pas valide AlreadyDeactivated: L'organisation est déjà désactivée AlreadyActive: L'organisation est déjà active @@ -436,8 +437,6 @@ Errors: Invalid: La subvention d'utilisateur n'est pas valide NotChanged: L'autorisation de l'utilisateur n'a pas été modifiée. IDMissing: Id manquant - NotActive: La subvention de l'utilisateur n'est pas active - NotInactive: La subvention à l'utilisateur n'est pas désactivée NoPermissionForProject: L'utilisateur n'a aucune autorisation pour ce projet RoleKeyNotFound: Rôle non trouvé Member: diff --git a/internal/static/i18n/hu.yaml b/internal/static/i18n/hu.yaml index 5becd6e606..7aa276c140 100644 --- a/internal/static/i18n/hu.yaml +++ b/internal/static/i18n/hu.yaml @@ -115,6 +115,7 @@ Errors: AlreadyVerified: Telefon már ellenőrizve Empty: A telefon mező üres NotChanged: Telefon nem lett megváltoztatva + VerifyingRemovalIsNotSupported: A telefon eltávolításának ellenőrzése nem támogatott Address: NotFound: Cím nem található NotChanged: Cím nem lett megváltoztatva @@ -193,7 +194,7 @@ Errors: AlreadyExists: Az instance már létezik NotChanged: Az instance nem változott Org: - AlreadyExists: A szervezet neve már foglalt + AlreadyExists: A szervezet neve vagy azonosítója már foglalt Invalid: A szervezet érvénytelen AlreadyDeactivated: A szervezet már deaktiválva van AlreadyActive: A szervezet már aktív @@ -435,8 +436,6 @@ Errors: Invalid: A felhasználói jogosultság érvénytelen NotChanged: A felhasználói jogosultság nem lett módosítva IDMissing: Hiányzó azonosító - NotActive: A felhasználói jogosultság nem aktív - NotInactive: A felhasználói jogosultság nincs kikapcsolva NoPermissionForProject: A felhasználónak nincs jogosultsága ebben a projektben RoleKeyNotFound: Szerepkör nem található Member: diff --git a/internal/static/i18n/id.yaml b/internal/static/i18n/id.yaml index 0108d7618b..8ea9a1eb8b 100644 --- a/internal/static/i18n/id.yaml +++ b/internal/static/i18n/id.yaml @@ -115,6 +115,7 @@ Errors: AlreadyVerified: Telepon sudah diverifikasi Empty: Telepon kosong NotChanged: Telepon tidak berubah + VerifyingRemovalIsNotSupported: Verifikasi penghapusan tidak didukung Address: NotFound: Alamat tidak ditemukan NotChanged: Alamat tidak berubah @@ -193,7 +194,7 @@ Errors: AlreadyExists: Contoh sudah ada NotChanged: Contoh tidak berubah Org: - AlreadyExists: Nama organisasi sudah dipakai + AlreadyExists: Nama atau ID organisasi sudah digunakan Invalid: Organisasi tidak valid AlreadyDeactivated: Organisasi sudah dinonaktifkan AlreadyActive: Organisasi sudah aktif @@ -435,8 +436,6 @@ Errors: Invalid: Hibah pengguna tidak valid NotChanged: Hibah pengguna belum diubah IDMissing: Aku hilang - NotActive: Hibah pengguna tidak aktif - NotInactive: Hibah pengguna tidak dinonaktifkan NoPermissionForProject: Pengguna tidak memiliki izin pada proyek ini RoleKeyNotFound: Peran tidak ditemukan Member: diff --git a/internal/static/i18n/it.yaml b/internal/static/i18n/it.yaml index b005a8f945..42847f9885 100644 --- a/internal/static/i18n/it.yaml +++ b/internal/static/i18n/it.yaml @@ -115,6 +115,7 @@ Errors: AlreadyVerified: Telefono già verificato Empty: Il telefono è vuoto NotChanged: Telefono non cambiato + VerifyingRemovalIsNotSupported: La rimozione della verifica non è supportata Address: NotFound: Indirizzo non trovato NotChanged: Indirizzo non cambiato @@ -193,7 +194,7 @@ Errors: AlreadyExists: L'istanza esiste già NotChanged: Istanza non modificata Org: - AlreadyExists: Nome dell'organizzazione già preso + AlreadyExists: Nome o ID dell'organizzazione già utilizzato Invalid: L'organizzazione non è valida AlreadyDeactivated: L'organizzazione è già disattivata AlreadyActive: L'organizzazione è già attiva @@ -436,8 +437,6 @@ Errors: Invalid: User Grant non è valido NotChanged: User Grant non è stata cambiato IDMissing: ID mancante - NotActive: User Grant non è attivo - NotInactive: User Grant non è disattivato NoPermissionForProject: L'utente non ha permessi su questo progetto RoleKeyNotFound: Ruolo non trovato Member: diff --git a/internal/static/i18n/ja.yaml b/internal/static/i18n/ja.yaml index 6b87054286..3195782d67 100644 --- a/internal/static/i18n/ja.yaml +++ b/internal/static/i18n/ja.yaml @@ -116,6 +116,7 @@ Errors: AlreadyVerified: 電話番号はすでに認証済みです Empty: 電話番号が空です NotChanged: 電話番号が変更されていません + VerifyingRemovalIsNotSupported: 電話番号の削除を検証することはできません Address: NotFound: 住所が見つかりません NotChanged: 住所は変更されていません @@ -194,7 +195,7 @@ Errors: AlreadyExists: すでに存在するインスタンス NotChanged: インスタンスは変更されていません Org: - AlreadyExists: 組織の名前はすでに使用されています + AlreadyExists: 組織名またはIDはすでに使用されています Invalid: 無効な組織です AlreadyDeactivated: 組織はすでに非アクティブです AlreadyActive: 組織はすでにアクティブです @@ -437,8 +438,6 @@ Errors: Invalid: 無効なユーザーグラントです NotChanged: ユーザーグラントは変更されていません IDMissing: IDがありません - NotActive: ユーザーグラントはアクティブではありません - NotInactive: ユーザーグラントは非アクティブではありません NoPermissionForProject: ユーザーにはこのプロジェクトに許可がありません RoleKeyNotFound: ロールが見つかりません Member: diff --git a/internal/static/i18n/ko.yaml b/internal/static/i18n/ko.yaml index d83af62235..729494c1f9 100644 --- a/internal/static/i18n/ko.yaml +++ b/internal/static/i18n/ko.yaml @@ -116,6 +116,7 @@ Errors: AlreadyVerified: 전화번호가 이미 인증되었습니다 Empty: 전화번호가 비어 있습니다 NotChanged: 전화번호가 변경되지 않았습니다 + VerifyingRemovalIsNotSupported: 전화번호 제거를 확인하는 것은 지원되지 않습니다 Address: NotFound: 주소를 찾을 수 없습니다 NotChanged: 주소가 변경되지 않았습니다 @@ -194,7 +195,7 @@ Errors: AlreadyExists: 인스턴스가 이미 존재합니다 NotChanged: 인스턴스가 변경되지 않았습니다 Org: - AlreadyExists: 조직 이름이 이미 사용 중입니다 + AlreadyExists: 조직 이름 또는 ID가 이미 사용 중입니다 Invalid: 조직이 유효하지 않습니다 AlreadyDeactivated: 조직이 이미 비활성화되었습니다 AlreadyActive: 조직이 이미 활성화되었습니다 @@ -436,8 +437,6 @@ Errors: Invalid: 사용자 권한이 유효하지 않습니다 NotChanged: 사용자 권한이 변경되지 않았습니다 IDMissing: ID가 누락되었습니다 - NotActive: 사용자 권한이 활성 상태가 아닙니다 - NotInactive: 사용자 권한이 비활성 상태가 아닙니다 NoPermissionForProject: 사용자가 이 프로젝트에 대한 권한이 없습니다 RoleKeyNotFound: 역할을 찾을 수 없습니다 Member: diff --git a/internal/static/i18n/mk.yaml b/internal/static/i18n/mk.yaml index 9fd71430cd..78ce5a7df7 100644 --- a/internal/static/i18n/mk.yaml +++ b/internal/static/i18n/mk.yaml @@ -115,6 +115,7 @@ Errors: AlreadyVerified: Телефонскиот број веќе е верифициран Empty: Телефонскиот број е празен NotChanged: Телефонскиот број не е променет + VerifyingRemovalIsNotSupported: Отстранувањето на верификацијата не е поддржано Address: NotFound: Адресата не е пронајдена NotChanged: Адресата не е променета @@ -192,7 +193,7 @@ Errors: AlreadyExists: Инстанцата веќе постои NotChanged: Инстанцата не е променета Org: - AlreadyExists: Името на организацијата е веќе зафатено + AlreadyExists: Името или ID-то на организацијата е веќе зафатено Invalid: Организацијата е невалидна AlreadyDeactivated: Организацијата е веќе деактивирана AlreadyActive: Организацијата е веќе активна @@ -435,8 +436,6 @@ Errors: Invalid: Овластувањето на корисникот е невалидно NotChanged: Овластувањето на корисникот не е променето IDMissing: ID недостасува - NotActive: Овластувањето на корисникот не е активно - NotInactive: Овластувањето на корисникот не е неактивно NoPermissionForProject: Корисникот нема овластувања за овој проект RoleKeyNotFound: Улогата не е пронајдена Member: diff --git a/internal/static/i18n/nl.yaml b/internal/static/i18n/nl.yaml index 9bc7b46a96..c1ad061787 100644 --- a/internal/static/i18n/nl.yaml +++ b/internal/static/i18n/nl.yaml @@ -115,6 +115,7 @@ Errors: AlreadyVerified: Telefoon is al geverifieerd Empty: Telefoon is leeg NotChanged: Telefoon niet veranderd + VerifyingRemovalIsNotSupported: Verwijderen van verificatie is niet ondersteund Address: NotFound: Adres niet gevonden NotChanged: Adres niet veranderd @@ -193,7 +194,7 @@ Errors: AlreadyExists: Instantie bestaat al NotChanged: Instantie is niet veranderd Org: - AlreadyExists: Organisatienaam is al in gebruik + AlreadyExists: Organisatienaam of -id is al in gebruik Invalid: Organisatie is ongeldig AlreadyDeactivated: Organisatie is al gedeactiveerd AlreadyActive: Organisatie is al actief @@ -436,8 +437,6 @@ Errors: Invalid: Gebruikerstoekenning is ongeldig NotChanged: Gebruikerstoekenning is niet veranderd IDMissing: ID ontbreekt - NotActive: Gebruikerstoekenning is niet actief - NotInactive: Gebruikerstoekenning is niet gedeactiveerd NoPermissionForProject: Gebruiker heeft geen rechten op dit project RoleKeyNotFound: Rol niet gevonden Member: diff --git a/internal/static/i18n/pl.yaml b/internal/static/i18n/pl.yaml index 8b8b43193a..0a0facdfa1 100644 --- a/internal/static/i18n/pl.yaml +++ b/internal/static/i18n/pl.yaml @@ -115,6 +115,7 @@ Errors: AlreadyVerified: Numer telefonu już zweryfikowany Empty: Numer telefonu jest pusty NotChanged: Numer telefonu nie zmieniony + VerifyingRemovalIsNotSupported: Usunięcie weryfikacji nie jest obsługiwane Address: NotFound: Adres nie znaleziony NotChanged: Adres nie zmieniony @@ -193,7 +194,7 @@ Errors: AlreadyExists: Instancja już istnieje NotChanged: Instancja nie zmieniona Org: - AlreadyExists: Nazwa organizacji jest już zajęta + AlreadyExists: Nazwa lub identyfikator organizacji jest już zajęty Invalid: Organizacja jest nieprawidłowa AlreadyDeactivated: Organizacja jest już deaktywowana AlreadyActive: Organizacja jest już aktywna @@ -436,8 +437,6 @@ Errors: Invalid: Uprawnienie użytkownika jest nieprawidłowe NotChanged: Uprawnienie użytkownika nie zostało zmienione IDMissing: Brak ID - NotActive: Uprawnienie użytkownika nie jest aktywne - NotInactive: Uprawnienie użytkownika nie jest dezaktywowane NoPermissionForProject: Użytkownik nie ma uprawnień do tego projektu RoleKeyNotFound: Rola nie znaleziona Member: diff --git a/internal/static/i18n/pt.yaml b/internal/static/i18n/pt.yaml index 64afdbf785..e2caf8059b 100644 --- a/internal/static/i18n/pt.yaml +++ b/internal/static/i18n/pt.yaml @@ -115,6 +115,7 @@ Errors: AlreadyVerified: O telefone já foi verificado Empty: O telefone está vazio NotChanged: Telefone não alterado + VerifyingRemovalIsNotSupported: Remoção de verificação não suportada Address: NotFound: Endereço não encontrado NotChanged: Endereço não alterado @@ -192,7 +193,7 @@ Errors: AlreadyExists: Instância já existe NotChanged: Instância não alterada Org: - AlreadyExists: Nome da organização já está em uso + AlreadyExists: O nome ou ID da organização já está em uso Invalid: Organização é inválida AlreadyDeactivated: Organização já está desativada AlreadyActive: Organização já está ativa @@ -435,8 +436,6 @@ Errors: Invalid: A concessão de usuário é inválida NotChanged: A concessão de usuário não foi alterada IDMissing: ID faltando - NotActive: A concessão de usuário não está ativa - NotInactive: A concessão de usuário não está desativada NoPermissionForProject: O usuário não possui permissões neste projeto RoleKeyNotFound: Função não encontrada Member: diff --git a/internal/static/i18n/ro.yaml b/internal/static/i18n/ro.yaml index 9010e57032..fede8eb85a 100644 --- a/internal/static/i18n/ro.yaml +++ b/internal/static/i18n/ro.yaml @@ -116,6 +116,7 @@ Errors: AlreadyVerified: Numărul de telefon este deja verificat Empty: Numărul de telefon este gol NotChanged: Numărul de telefon nu a fost schimbat + VerifyingRemovalIsNotSupported: Verificarea eliminării nu este acceptată Address: NotFound: Adresa nu a fost găsită NotChanged: Adresa nu a fost schimbată @@ -194,7 +195,7 @@ Errors: AlreadyExists: Instanța există deja NotChanged: Instanța nu a fost schimbată Org: - AlreadyExists: Numele organizației este deja luat + AlreadyExists: Numele sau ID-ul organizației este deja utilizat Invalid: Organizația este invalidă AlreadyDeactivated: Organizația este deja dezactivată AlreadyActive: Organizația este deja activă @@ -436,8 +437,6 @@ Errors: Invalid: Acordarea utilizatorului este invalidă NotChanged: Acordarea utilizatorului nu a fost schimbată IDMissing: Id lipsă - NotActive: Acordarea utilizatorului nu este activă - NotInactive: Acordarea utilizatorului nu este dezactivată NoPermissionForProject: Utilizatorul nu are permisiuni pentru acest proiect RoleKeyNotFound: Rolul nu a fost găsit Member: diff --git a/internal/static/i18n/ru.yaml b/internal/static/i18n/ru.yaml index cf90c12150..c853c871af 100644 --- a/internal/static/i18n/ru.yaml +++ b/internal/static/i18n/ru.yaml @@ -116,6 +116,7 @@ Errors: AlreadyVerified: Телефон уже подтверждён Empty: Телефон пуст NotChanged: Телефон не менялся + VerifyingRemovalIsNotSupported: Удаление телефона не поддерживается Address: NotFound: Адрес не найден NotChanged: Адрес не изменён @@ -193,7 +194,7 @@ Errors: AlreadyExists: Экземпляр уже существует NotChanged: Экземпляр не изменён Org: - AlreadyExists: Название организации уже занято + AlreadyExists: Название организации или идентификатор уже занят Invalid: Организация недействительна AlreadyDeactivated: Организация уже деактивирована AlreadyActive: Организация уже активна @@ -430,8 +431,6 @@ Errors: Invalid: Допуск пользователя недействителен NotChanged: Допуск пользователя не был изменён IDMissing: ID отсутствует - NotActive: Допуск пользователя неактивен - NotInactive: Допуск пользователя не деактивирован NoPermissionForProject: Пользователь не имеет прав доступа к данному проекту RoleKeyNotFound: Роль не найдена Member: diff --git a/internal/static/i18n/sv.yaml b/internal/static/i18n/sv.yaml index ed4b863886..daf8da5cf2 100644 --- a/internal/static/i18n/sv.yaml +++ b/internal/static/i18n/sv.yaml @@ -115,6 +115,7 @@ Errors: AlreadyVerified: Mobilnr redan verifierad Empty: Mobilnr är tom NotChanged: Mobilnr ändrades inte + VerifyingRemovalIsNotSupported: Verifiering av borttagning stöds inte Address: NotFound: Adress hittades inte NotChanged: Adress ändrades inte @@ -193,7 +194,7 @@ Errors: AlreadyExists: Instans finns redan NotChanged: Instans ändrades inte Org: - AlreadyExists: Organisationens namn är redan taget + AlreadyExists: Organisationens namn eller ID är redan upptaget Invalid: Organisationen är ogiltigt AlreadyDeactivated: Organisation är redan avaktiverad AlreadyActive: Organisationen är redan aktiv @@ -435,8 +436,6 @@ Errors: Invalid: Användarbeviljandet är ogiltigt NotChanged: Användarbeviljandet har inte ändrats IDMissing: Id saknas - NotActive: Användarbeviljandet är inte aktivt - NotInactive: Användarbeviljandet är inte inaktivt NoPermissionForProject: Användaren har inga behörigheter i detta projekt RoleKeyNotFound: Rollen hittades inte Member: diff --git a/internal/static/i18n/tr.yaml b/internal/static/i18n/tr.yaml new file mode 100644 index 0000000000..80a7c48eb2 --- /dev/null +++ b/internal/static/i18n/tr.yaml @@ -0,0 +1,1419 @@ +Errors: + Internal: Dahili bir hata oluştu + NoChangesFound: Değişiklik bulunamadı + OriginNotAllowed: Bu "Origin" izin verilmiyor + IDMissing: ID eksik + ResourceOwnerMissing: Kaynak Sahibi Organizasyonu eksik + RemoveFailed: Kaldırılamadı + ProjectionName: + Invalid: Geçersiz projeksiyon adı + Assets: + EmptyKey: Varlık anahtarı boş + Store: + NotInitialized: Varlık depolaması başlatılmadı + NotConfigured: Varlık depolaması yapılandırılmadı + Bucket: + Internal: Bucket oluşturma sırasında dahili hata + AlreadyExists: Bucket zaten mevcut + CreateFailed: Bucket oluşturulamadı + ListFailed: Bucket'lar okunamadı + RemoveFailed: Bucket silinemedi + SetPublicFailed: Bucket herkese açık yapılamadı + Object: + PutFailed: Nesne oluşturulamadı + GetFailed: Nesne okunamadı + NotFound: Nesne bulunamadı + PresignedTokenFailed: İmzalı token oluşturulamadı + ListFailed: Nesne listesi okunamadı + RemoveFailed: Nesne kaldırılamadı + Limit: + ExceedsDefault: Limit varsayılan limiti aşıyor + Limits: + NotFound: Limitler bulunamadı + NoneSpecified: Limit belirtilmedi + Instance: + Blocked: Instance engellenmiş + Restrictions: + NoneSpecified: Kısıtlama belirtilmedi + DefaultLanguageMustBeAllowed: Varsayılan dil izin verilmeli + Language: + NotParsed: Dil ayrıştırılamadı + NotSupported: Dil desteklenmiyor + NotAllowed: Dil izin verilmiyor + Undefined: Dil tanımlanmamış + Duplicate: Dillerde tekrarlar var + OIDCSettings: + NotFound: OIDC Yapılandırması bulunamadı + AlreadyExists: OIDC yapılandırması zaten mevcut + SecretGenerator: + AlreadyExists: Gizli anahtar üreticisi zaten mevcut + TypeMissing: Gizli anahtar üreticisi türü eksik + NotFound: Gizli anahtar üreticisi bulunamadı + SMSConfig: + NotFound: SMS yapılandırması bulunamadı + AlreadyActive: SMS yapılandırması zaten aktif + AlreadyDeactivated: SMS yapılandırması zaten devre dışı + NotExternalVerification: SMS yapılandırması kod doğrulamayı desteklemiyor + SMTP: + NotEmailMessage: mesaj EmailMessage değil + RequiredAttributes: konu, alıcılar ve içerik ayarlanmalı ancak bazıları veya tümü boş + CouldNotSplit: smtp'ye bağlanmak için host ve port ayrıştırılamadı + CouldNotDial: SMTP sunucusuna bağlanılamadı, port ve güvenlik duvarı sorunlarını kontrol edin... + CouldNotDialTLS: TLS kullanarak SMTP sunucusuna bağlanılamadı, port ve güvenlik duvarı sorunlarını kontrol edin... + CouldNotCreateClient: smtp istemcisi oluşturulamadı + CouldNotStartTLS: tls başlatılamadı + CouldNotAuth: smtp kimlik doğrulaması eklenemedi, kullanıcı adı ve şifrenizin doğru olup olmadığını kontrol edin, doğruysa sağlayıcınız ZITADEL tarafından desteklenmeyen bir kimlik doğrulama yöntemi gerektirebilir + CouldNotSetSender: gönderen ayarlanamadı + CouldNotSetRecipient: alıcı ayarlanamadı + SMTPConfig: + TestPassword: Test için şifre bulunamadı + NotFound: SMTP yapılandırması bulunamadı + AlreadyExists: SMTP yapılandırması zaten mevcut + AlreadyDeactivated: SMTP yapılandırması zaten devre dışı + SenderAdressNotCustomDomain: Gönderen adresi instance üzerinde özel domain olarak yapılandırılmalı. + TestEmailNotFound: Test için e-posta adresi bulunamadı + Notification: + NoDomain: Mesaj için Domain bulunamadı + User: + NotFound: Kullanıcı bulunamadı + AlreadyExists: Kullanıcı zaten mevcut + NotFoundOnOrg: Kullanıcı seçilen organizasyonda bulunamadı + NotAllowedOrg: Kullanıcı gerekli organizasyonun üyesi değil + UserIDMissing: Kullanıcı ID eksik + UserIDWrong: "İstek kullanıcısı, kimlik doğrulaması yapılan kullanıcıya eşit değil" + DomainPolicyNil: Organizasyon Politikası boş + EmailAsUsernameNotAllowed: E-posta kullanıcı adı olarak izin verilmiyor + Invalid: Kullanıcı verisi geçersiz + DomainNotAllowedAsUsername: Domain zaten rezerve edilmiş ve kullanılamaz + AlreadyInactive: Kullanıcı zaten pasif + NotInactive: Kullanıcı pasif değil + CantDeactivateInitial: Başlangıç durumundaki kullanıcı sadece silinebilir, devre dışı bırakılamaz + ShouldBeActiveOrInitial: Kullanıcı aktif veya başlangıç durumunda değil + AlreadyInitialised: Kullanıcı zaten başlatılmış + NotInitialised: Kullanıcı henüz başlatılmamış + NotLocked: Kullanıcı kilitli değil + NoChanges: Değişiklik bulunamadı + InitCodeNotFound: Başlatma Kodu bulunamadı + UsernameNotChanged: Kullanıcı adı değişmedi + InvalidURLTemplate: URL Şablonu geçersiz + Profile: + NotFound: Profil bulunamadı + NotChanged: Profil değişmedi + Empty: Profil boş + FirstNameEmpty: Profildeki ad boş + LastNameEmpty: Profildeki soyad boş + IDMissing: Profil ID eksik + Email: + NotFound: E-posta bulunamadı + Invalid: E-posta geçersiz + AlreadyVerified: E-posta zaten doğrulanmış + NotChanged: E-posta değişmedi + Empty: E-posta boş + IDMissing: E-posta ID eksik + Phone: + NotFound: Telefon bulunamadı + Invalid: Telefon geçersiz + AlreadyVerified: Telefon zaten doğrulanmış + Empty: Telefon boş + NotChanged: Telefon değişmedi + VerifyingRemovalIsNotSupported: Telefon kaldırma doğrulaması desteklenmiyor + Address: + NotFound: Adres bulunamadı + NotChanged: Adres değişmedi + Machine: + Key: + NotFound: Makine anahtarı bulunamadı + AlreadyExisting: Makine anahtarı zaten mevcut + Invalid: Genel anahtar, PEM kodlamalı PKIX formatında geçerli bir RSA genel anahtarı değil + Secret: + NotExisting: Gizli anahtar mevcut değil + Invalid: Gizli anahtar geçersiz + CouldNotGenerate: Gizli anahtar oluşturulamadı + PAT: + NotFound: Kişisel Erişim Token'ı bulunamadı + NotHuman: Kullanıcı kişisel olmalı + NotMachine: Kullanıcı teknik olmalı + WrongType: Bu kullanıcı türü için izin verilmiyor + NotAllowedToLink: Kullanıcının harici giriş sağlayıcısı ile bağlantı kurmasına izin verilmiyor + Username: + AlreadyExists: Kullanıcı adı zaten alınmış + Reserved: Kullanıcı adı zaten alınmış + Empty: Kullanıcı adı boş + Code: + Empty: Kod boş + NotFound: Kod bulunamadı + Expired: Kodun süresi dolmuş + GeneratorAlgNotSupported: Desteklenmeyen üretici algoritması + Invalid: Kod geçersiz + Password: + NotFound: Şifre bulunamadı + Empty: Şifre boş + Invalid: Şifre geçersiz + NotSet: Kullanıcı şifre ayarlamamış + NotChanged: Yeni şifre mevcut şifrenizle aynı olamaz + NotSupported: Şifre hash kodlaması desteklenmiyor. Kontrol edin https://zitadel.com/docs/concepts/architecture/secrets#hashed-secrets + PasswordComplexityPolicy: + NotFound: Şifre politikası bulunamadı + MinLength: Şifre çok kısa + MinLengthNotAllowed: Verilen minimum uzunluk izin verilmiyor + HasLower: Şifre küçük harf içermeli + HasUpper: Şifre büyük harf içermeli + HasNumber: Şifre sayı içermeli + HasSymbol: Şifre sembol içermeli + ExternalIDP: + Invalid: Harici IDP geçersiz + IDPConfigNotExisting: IDP sağlayıcısı bu organizasyon için geçersiz + NotAllowed: Harici IDP izin verilmiyor + MinimumExternalIDPNeeded: En az bir IDP eklenmelidir + AlreadyExists: Harici IDP zaten alınmış + NotFound: Harici IDP bulunamadı + LoginFailed: Harici IDP'de giriş başarısız + MFA: + OTP: + AlreadyReady: Çok faktörlü OTP (Tek Kullanımlık Şifre) zaten kurulmuş + NotExisting: Çok faktörlü OTP (Tek Kullanımlık Şifre) mevcut değil + NotReady: Çok faktörlü OTP (Tek Kullanımlık Şifre) hazır değil + InvalidCode: Geçersiz kod + U2F: + NotExisting: U2F mevcut değil + Passwordless: + NotExisting: Şifresiz giriş mevcut değil + WebAuthN: + NotFound: WebAuthN Token bulunamadı + BeginRegisterFailed: WebAuthN kayıt başlatma başarısız + MarshalError: Verileri sıralama hatası + ErrorOnParseCredential: Kimlik bilgilerini ayrıştırma hatası + CreateCredentialFailed: Kimlik bilgileri oluşturma hatası + BeginLoginFailed: WebAuthN giriş başlatma başarısız + ValidateLoginFailed: Giriş kimlik bilgilerini doğrulama hatası + CloneWarning: Kimlik bilgileri klonlanmış olabilir + RefreshToken: + Invalid: Yenileme Token'ı geçersiz + NotFound: Yenileme Token'ı bulunamadı + Instance: + NotFound: Instance bulunamadı + AlreadyExists: Instance zaten mevcut + NotChanged: Instance değişmedi + Org: + AlreadyExists: Organizasyon adı zaten alınmış + Invalid: Organizasyon geçersiz + AlreadyDeactivated: Organizasyon zaten devre dışı + AlreadyActive: Organizasyon zaten aktif + Empty: Organizasyon boş + NotFound: Organizasyon bulunamadı + NotChanged: Organizasyon değişmedi + DefaultOrgNotDeletable: Varsayılan Organizasyon silinmemeli + ZitadelOrgNotDeletable: ZITADEL projesi olan organizasyon silinmemeli + InvalidDomain: Geçersiz domain + DomainMissing: Domain eksik + DomainNotOnOrg: Domain organizasyonda mevcut değil + DomainNotVerified: Domain doğrulanmamış + DomainAlreadyVerified: Domain zaten doğrulanmış + DomainVerificationTypeInvalid: Domain doğrulama türü geçersiz + DomainVerificationMissing: Domain doğrulaması henüz başlatılmamış + DomainVerificationFailed: Domain doğrulaması başarısız + DomainVerificationTXTNotFound: Domain'iniz için _zitadel-challenge TXT kaydı bulunamadı. DNS sunucunuza eklediğinizden emin olun veya yeni kaydın yayılmasını bekleyin + DomainVerificationTXTNoMatch: Domain'iniz için _zitadel-challenge TXT kaydı bulundu ancak doğru token metnini içermiyor. DNS sunucunuza doğru token'ı eklediğinizden emin olun veya yeni kaydın yayılmasını bekleyin + DomainVerificationHTTPNotFound: Challenge içeren dosya beklenen URL'de bulunamadı. Dosyayı okuma izinleriyle doğru yere yüklediğinizden emin olun + DomainVerificationHTTPNoMatch: Challenge içeren dosya beklenen URL'de bulundu ancak doğru token metnini içermiyor. İçeriğini kontrol edin + DomainVerificationTimeout: DNS sunucusunu sorgularken zaman aşımı oluştu + PrimaryDomainNotDeletable: Birincil domain silinmemeli + DomainNotFound: Domain bulunamadı + MemberIDMissing: Üye ID eksik + MemberNotFound: Organizasyon üyesi bulunamadı + InvalidMember: Organizasyon üyesi geçersiz + UserIDMissing: Kullanıcı ID eksik + PolicyAlreadyExists: Politika zaten mevcut + PolicyNotExisting: Politika mevcut değil + IdpInvalid: IDP yapılandırması geçersiz + IdpNotExisting: IDP yapılandırması mevcut değil + OIDCConfigInvalid: OIDC IDP yapılandırması geçersiz + IdpIsNotOIDC: IDP yapılandırması oidc türünde değil + Domain: + AlreadyExists: Domain zaten mevcut + InvalidCharacter: Domain için sadece alfanümerik karakterler, . ve - kullanılabilir + EmptyString: Geçersiz sayısal olmayan ve alfabetik olmayan karakterler boş alanlarla değiştirildi ve sonuçta domain boş bir dize oldu + IDP: + InvalidSearchQuery: Geçersiz arama sorgusu + ClientIDMissing: ClientID eksik + TeamIDMissing: TeamID eksik + KeyIDMissing: KeyID eksik + PrivateKeyMissing: Özel Anahtar eksik + LoginPolicy: + NotFound: Giriş Politikası bulunamadı + Invalid: Giriş Politikası geçersiz + RedirectURIInvalid: Varsayılan Yönlendirme URI geçersiz + NotExisting: Giriş Politikası mevcut değil + AlreadyExists: Giriş Politikası zaten mevcut + IdpProviderAlreadyExisting: Kimlik Sağlayıcısı zaten mevcut + IdpProviderNotExisting: Kimlik Sağlayıcısı mevcut değil + RegistrationNotAllowed: Kayıt izin verilmiyor + UsernamePasswordNotAllowed: Kullanıcı Adı / Şifre ile giriş izin verilmiyor + MFA: + AlreadyExists: Çok faktörlü kimlik doğrulama zaten mevcut + NotExisting: Çok faktörlü kimlik doğrulama mevcut değil + Unspecified: Çok faktörlü kimlik doğrulama geçersiz + MailTemplate: + NotFound: Varsayılan E-posta Şablonu bulunamadı + NotChanged: Varsayılan E-posta Şablonu değişmedi + AlreadyExists: Varsayılan E-posta Şablonu zaten mevcut + Invalid: Varsayılan E-posta Şablonu geçersiz + CustomMessageText: + NotFound: Varsayılan Mesaj Metni bulunamadı + NotChanged: Varsayılan Mesaj Metni değişmedi + AlreadyExists: Varsayılan Mesaj Metni zaten mevcut + Invalid: Varsayılan Mesaj Metni geçersiz + PasswordComplexityPolicy: + NotFound: Şifre Karmaşıklık Politikası bulunamadı + Empty: Şifre Karmaşıklık Politikası boş + NotExisting: Şifre Karmaşıklık Politikası mevcut değil + AlreadyExists: Şifre Karmaşıklık Politikası zaten mevcut + PasswordLockoutPolicy: + NotFound: Şifre Kilitleme Politikası bulunamadı + Empty: Şifre Kilitleme Politikası boş + NotExisting: Şifre Kilitleme Politikası mevcut değil + AlreadyExists: Şifre Kilitleme Politikası zaten mevcut + PasswordAgePolicy: + NotFound: Şifre Yaş Politikası bulunamadı + Empty: Şifre Yaş Politikası boş + NotExisting: Şifre Yaş Politikası mevcut değil + AlreadyExists: Şifre Yaş Politikası zaten mevcut + OrgIAMPolicy: + Empty: Org IAM Politikası boş + NotExisting: Org IAM Politikası mevcut değil + AlreadyExists: Org IAM Politikası zaten mevcut + NotificationPolicy: + NotFound: Bildirim Politikası bulunamadı + NotChanged: Bildirim Politikası değişmedi + AlreadyExists: Bildirim Politikası zaten mevcut + LabelPolicy: + NotFound: Özel Etiket Politikası bulunamadı + NotChanged: Özel Etiket Politikası değişmedi + Project: + ProjectIDMissing: Proje Id eksik + AlreadyExists: Proje organizasyonda zaten mevcut + OrgNotExisting: Organizasyon mevcut değil + UserNotExisting: Kullanıcı mevcut değil + CouldNotGenerateClientSecret: İstemci gizli anahtarı oluşturulamadı + Invalid: Proje geçersiz + NotActive: Proje aktif değil + NotInactive: Proje devre dışı değil + NotFound: Proje bulunamadı + UserIDMissing: Kullanıcı ID eksik + Member: + NotFound: Proje üyesi bulunamadı + Invalid: Proje üyesi geçersiz + AlreadyExists: Proje üyesi zaten mevcut + NotExisting: Proje üyesi mevcut değil + MinimumOneRoleNeeded: En az bir rol eklenmelidir + Role: + AlreadyExists: Rol zaten mevcut + Invalid: Rol geçersiz + NotExisting: Rol mevcut değil + IDMissing: ID eksik + App: + AlreadyExists: Uygulama zaten mevcut + NotFound: Uygulama bulunamadı + Invalid: Uygulama geçersiz + NotExisting: Uygulama mevcut değil + NotActive: Uygulama aktif değil + NotInactive: Uygulama pasif değil + OIDCConfigInvalid: OIDC yapılandırması geçersiz + APIConfigInvalid: API yapılandırması geçersiz + SAMLConfigInvalid: SAML yapılandırması geçersiz + IsNotOIDC: Uygulama OIDC türünde değil + IsNotAPI: Uygulama API türünde değil + IsNotSAML: Uygulama SAML türünde değil + SAMLMetadataMissing: SAML metadata eksik + SAMLMetadataFormat: SAML Metadata format hatası + SAMLEntityIDAlreadyExisting: SAML EntityID zaten mevcut + OIDCAuthMethodNoSecret: Seçilen OIDC Kimlik Doğrulama Yöntemi gizli anahtar gerektirmiyor + APIAuthMethodNoSecret: Seçilen API Kimlik Doğrulama Yöntemi gizli anahtar gerektirmiyor + AuthMethodNoPrivateKeyJWT: Seçilen Kimlik Doğrulama Yöntemi anahtar gerektirmiyor + ClientSecretInvalid: İstemci Gizli Anahtarı geçersiz + Key: + AlreadyExisting: Uygulama anahtarı zaten mevcut + NotFound: Uygulama anahtarı bulunamadı + RequiredFieldsMissing: Bazı gerekli alanlar eksik + Grant: + AlreadyExists: Proje yetkisi zaten mevcut + NotFound: Yetki bulunamadı + Invalid: Proje yetkisi geçersiz + NotExisting: Proje yetkisi mevcut değil + HasNotExistingRole: Projede bulunmayan bir rol var + NotActive: Proje yetkisi aktif değil + NotInactive: Proje yetkisi pasif değil + IAM: + NotFound: Instance bulunamadı. Domain'in doğru olduğundan emin olun. Kontrol edin https://zitadel.com/docs/apis/introduction#domains + Member: + RolesNotChanged: Roller değişmedi + MemberInvalid: Üye geçersiz + MemberAlreadyExisting: Üye zaten mevcut + MemberNotExisting: Üye mevcut değil + IDMissing: Id eksik + IAMProjectIDMissing: IAM proje id eksik + IamProjectAlreadySet: IAM proje id zaten ayarlanmış + IdpInvalid: IDP yapılandırması geçersiz + IdpNotExisting: IDP yapılandırması mevcut değil + OIDCConfigInvalid: OIDC IDP yapılandırması geçersiz + IdpIsNotOIDC: IDP yapılandırması oidc türünde değil + LoginPolicyInvalid: Giriş Politikası geçersiz + LoginPolicyNotExisting: Giriş Politikası mevcut değil + IdpProviderInvalid: Kimlik Sağlayıcısı geçersiz + LoginPolicy: + NotFound: Varsayılan Giriş Politikası bulunamadı + NotChanged: Varsayılan Giriş Politikası değişmedi + NotExisting: Varsayılan Giriş Politikası mevcut değil + AlreadyExists: Varsayılan Giriş Politikası zaten mevcut + RedirectURIInvalid: Varsayılan Yönlendirme URI geçersiz + MFA: + AlreadyExists: Çok faktörlü kimlik doğrulama zaten mevcut + NotExisting: Çok faktörlü kimlik doğrulama mevcut değil + Unspecified: Çok faktörlü kimlik doğrulama geçersiz + IDP: + AlreadyExists: Kimlik sağlayıcısı zaten mevcut + NotExisting: Kimlik sağlayıcısı mevcut değil + Invalid: Kimlik Sağlayıcısı geçersiz + IDPConfig: + AlreadyExists: Kimlik Sağlayıcısı Yapılandırması zaten mevcut + NotInactive: Kimlik Sağlayıcısı Yapılandırması pasif değil + NotActive: Kimlik Sağlayıcısı Yapılandırması aktif değil + LabelPolicy: + NotFound: Varsayılan Özel Etiket Politikası bulunamadı + NotChanged: Varsayılan Özel Etiket Politikası değişmedi + MailTemplate: + NotFound: Varsayılan E-posta Şablonu bulunamadı + NotChanged: Varsayılan E-posta Şablonu değişmedi + AlreadyExists: Varsayılan E-posta Şablonu zaten mevcut + Invalid: Varsayılan E-posta Şablonu geçersiz + CustomMessageText: + NotFound: Varsayılan Mesaj Metni bulunamadı + NotChanged: Varsayılan Mesaj Metni değişmedi + AlreadyExists: Varsayılan Mesaj Metni zaten mevcut + Invalid: Varsayılan Mesaj Metni geçersiz + PasswordComplexityPolicy: + NotFound: Varsayılan Şifre Karmaşıklık Politikası bulunamadı + NotExisting: Varsayılan Şifre Karmaşıklık Politikası mevcut değil + AlreadyExists: Varsayılan Şifre Karmaşıklık Politikası zaten mevcut + Empty: Varsayılan Şifre Karmaşıklık Politikası boş + NotChanged: Varsayılan Şifre Karmaşıklık Politikası değişmedi + PasswordAgePolicy: + NotFound: Varsayılan Şifre Yaş Politikası bulunamadı + NotExisting: Varsayılan Şifre Yaş Politikası mevcut değil + AlreadyExists: Varsayılan Şifre Yaş Politikası zaten mevcut + Empty: Varsayılan Şifre Yaş Politikası boş + NotChanged: Varsayılan Şifre Yaş Politikası değişmedi + PasswordLockoutPolicy: + NotFound: Varsayılan Şifre Kilitleme Politikası bulunamadı + NotExisting: Varsayılan Şifre Kilitleme Politikası mevcut değil + AlreadyExists: Varsayılan Şifre Kilitleme Politikası zaten mevcut + Empty: Varsayılan Şifre Kilitleme Politikası boş + NotChanged: Varsayılan Şifre Kilitleme Politikası değişmedi + DomainPolicy: + NotFound: Org IAM Politikası bulunamadı + Empty: Org IAM Politikası boş + NotExisting: Org IAM Politikası mevcut değil + AlreadyExists: Org IAM Politikası zaten mevcut + NotChanged: Org IAM Politikası değişmedi + NotificationPolicy: + NotFound: Varsayılan Bildirim Politikası bulunamadı + NotChanged: Varsayılan Bildirim Politikası değişmedi + AlreadyExists: Varsayılan Bildirim Politikası zaten mevcut + Policy: + AlreadyExists: Politika zaten mevcut + Label: + Invalid: + PrimaryColor: Birincil renk geçerli bir Hex renk değeri değil + BackgroundColor: Arka plan rengi geçerli bir Hex renk değeri değil + WarnColor: Uyarı rengi geçerli bir Hex renk değeri değil + FontColor: Yazı tipi rengi geçerli bir Hex renk değeri değil + PrimaryColorDark: Birincil renk (karanlık mod) geçerli bir Hex renk değeri değil + BackgroundColorDark: Arka plan rengi (karanlık mod) geçerli bir Hex renk değeri değil + WarnColorDark: Uyarı rengi (karanlık mod) geçerli bir Hex renk değeri değil + FontColorDark: Yazı tipi rengi (karanlık mod) geçerli bir Hex renk değeri değil + UserGrant: + AlreadyExists: Kullanıcı yetkisi zaten mevcut + NotFound: Kullanıcı yetkisi bulunamadı + Invalid: Kullanıcı yetkisi geçersiz + NotChanged: Kullanıcı yetkisi değişmedi + IDMissing: Id eksik + NoPermissionForProject: Kullanıcının bu proje üzerinde izni yok + RoleKeyNotFound: Rol bulunamadı + Member: + AlreadyExists: Üye zaten mevcut + IDPConfig: + AlreadyExists: Bu isimde IDP Yapılandırması zaten mevcut + NotExisting: Kimlik Sağlayıcısı Yapılandırması mevcut değil + Changes: + NotFound: Geçmiş bulunamadı + AuditRetention: Geçmiş Denetim Günlüğü Saklama süresinin dışında + Token: + NotFound: Token bulunamadı + Invalid: Token geçersiz + UserSession: + NotFound: KullanıcıOturumu bulunamadı + Key: + NotFound: Anahtar bulunamadı + ExpireBeforeNow: Son kullanma tarihi geçmişte + Login: + LoginPolicy: + MFA: + ForceAndNotConfigured: Çok faktörlü kimlik doğrulama gerekli olarak yapılandırılmış, ancak olası hiçbir sağlayıcı yapılandırılmamış. Lütfen sistem yöneticinizle iletişime geçin. + Step: + Started: + AlreadyExists: Başlatılan adım zaten mevcut + Done: + AlreadyExists: Tamamlanan adım zaten mevcut + CustomText: + AlreadyExists: Özel metin zaten mevcut + Invalid: Özel metin geçersiz + NotFound: Özel metin bulunamadı + TranslationFile: + ReadError: Çeviri dosyasını okuma hatası + MergeError: Çeviri dosyası özel çevirilerle birleştirilemedi + NotFound: Çeviri dosyası mevcut değil + Metadata: + NotFound: Metadata bulunamadı + NoData: Metadata listesi boş + Invalid: Metadata geçersiz + KeyNotExisting: Bir veya daha fazla anahtar mevcut değil + Action: + Invalid: Eylem geçersiz + NotFound: Eylem bulunamadı + NotActive: Eylem aktif değil + NotInactive: Eylem pasif değil + MaxAllowed: Ek aktif Eylem izin verilmiyor + NotEnabled: '"Action" özelliği etkin değil' + Flow: + FlowTypeMissing: FlowType eksik + Empty: Akış zaten boş + WrongTriggerType: TriggerType geçersiz + NoChanges: Değişiklik yok + ActionIDsNotExist: ActionID'ler mevcut değil + Query: + CloseRows: SQL İfadesi tamamlanamadı + SQLStatement: SQL İfadesi oluşturulamadı + InvalidRequest: İstek geçersiz + TooManyNestingLevels: Çok fazla sorgu iç içe geçme seviyesi (Maksimum 20) + LimitExceeded: Limit aşıldı + Quota: + AlreadyExists: Bu birim için kota zaten mevcut + NotFound: Bu birim için kota bulunamadı + Invalid: + CallURL: Kota çağrı URL'si geçersiz + Percent: Kota yüzdesi 1'den düşük + Unimplemented: Bu birim için kotalar uygulanmamış + Amount: Kota miktarı 1'den düşük + ResetInterval: Kota sıfırlama aralığı bir dakikadan kısa + Noop: Bildirimsiz sınırsız kotanın etkisi yok + Access: + Exhausted: Kimlik doğrulamalı istekler için kota tükendi + Execution: + Exhausted: Yürütme saniyeleri için kota tükendi + LogStore: + Access: + StorageFailed: Erişim günlüğünü veritabanına kaydetme başarısız + ScanFailed: Kimlik doğrulamalı istekler için kullanım sorgulama başarısız + Execution: + StorageFailed: Eylem yürütme günlüğünü veritabanına kaydetme başarısız + ScanFailed: Eylem yürütme saniyeleri için kullanım sorgulama başarısız + Session: + NotExisting: Oturum mevcut değil + Terminated: Oturum zaten sonlandırılmış + Expired: Oturumun süresi dolmuş + PositiveLifetime: Oturum ömrü 0'dan az olmamalı + Token: + Invalid: Oturum Token'ı geçersiz + WebAuthN: + NoChallenge: WebAuthN challenge'ı olmayan oturum + Intent: + IDPMissing: İstekte IDP ID eksik + IDPInvalid: İstek için IDP geçersiz + ResponseInvalid: IDP yanıtı geçersiz + MissingSingleMappingAttribute: IDP yanıtı eşleme özniteliğini içermiyor veya birden fazla değeri var + SuccessURLMissing: İstekte Başarı URL'si eksik + FailureURLMissing: İstekte Hata URL'si eksik + StateMissing: İstekte State parametresi eksik + NotStarted: Niyet başlatılmamış veya zaten sonlandırılmış + NotSucceeded: Niyet başarılı olmamış + Expired: Niyetin süresi dolmuş + TokenCreationFailed: Token oluşturma başarısız + InvalidToken: Niyet Token'ı geçersiz + OtherUser: Niyet başka bir kullanıcı için + AuthRequest: + AlreadyExists: Kimlik Doğrulama İsteği zaten mevcut + NotExisting: Kimlik Doğrulama İsteği mevcut değil + WrongLoginClient: Kimlik Doğrulama İsteği başka giriş istemcisi tarafından oluşturulmuş + AlreadyHandled: Kimlik Doğrulama İsteği zaten işlenmiş + OIDCSession: + RefreshTokenInvalid: Yenileme Token'ı geçersiz + Token: + Invalid: Token geçersiz + Expired: Tokenın süresi dolmuş + InvalidClient: Token bu istemci için verilmemiş + SAMLRequest: + AlreadyExists: SAMLRequest zaten mevcut + NotExisting: SAMLRequest mevcut değil + WrongLoginClient: SAMLRequest başka giriş istemcisi tarafından oluşturulmuş + AlreadyHandled: SAMLRequest zaten işlenmiş + SAMLSession: + InvalidClient: SAMLResponse bu istemci için verilmemiş + DeviceAuth: + NotFound: Cihaz Yetkilendirme İsteği mevcut değil + AlreadyHandled: Cihaz Yetkilendirme İsteği zaten işlenmiş + Feature: + NotExisting: Özellik mevcut değil + TypeNotSupported: Özellik türü desteklenmiyor + InvalidValue: Bu özellik için geçersiz değer + Target: + Invalid: Hedef geçersiz + NoTimeout: Hedefin zaman aşımı yok + InvalidURL: Hedefin geçersiz URL'si var + NotFound: Hedef bulunamadı + Execution: + ConditionInvalid: Yürütme koşulu geçersiz + Invalid: Yürütme geçersiz + NotFound: Yürütme bulunamadı + IncludeNotFound: Include bulunamadı + NoTargets: Hedef tanımlanmamış + Failed: Yürütme başarısız + ResponseIsNotValidJSON: Yanıt geçerli JSON değil + UserSchema: + NotEnabled: '"User Schema" özelliği etkin değil' + Type: + Missing: Kullanıcı Şema Türü eksik + AlreadyExists: Kullanıcı Şema Türü zaten mevcut + Authenticator: + Invalid: Geçersiz kimlik doğrulayıcı türü + NotActive: Kullanıcı Şeması aktif değil + NotInactive: Kullanıcı Şeması pasif değil + NotExists: Kullanıcı Şeması mevcut değil + ID: + Missing: Kullanıcı Şema ID eksik + Invalid: Kullanıcı Şeması geçersiz + Data: + Invalid: Kullanıcı Şeması için veri geçersiz + TokenExchange: + FeatureDisabled: Token Exchange özelliği instance'ınız için devre dışı. https://zitadel.com/docs/apis/resources/feature_service_v2/feature-service-set-instance-features + Token: + Missing: Token eksik + Invalid: Token geçersiz + TypeMissing: Token türü eksik + TypeNotAllowed: Token türü izin verilmiyor + TypeNotSupported: Token türü desteklenmiyor + NotForAPI: API için taklit token'larına izin verilmiyor + Impersonation: + PolicyDisabled: Instance güvenlik politikasında taklit devre dışı + WebKey: + ActiveDelete: Aktif web anahtarı silinemez + Config: Geçersiz web anahtarı yapılandırması + Duplicate: Web anahtarı ID benzersiz değil + FeatureDisabled: Web anahtarı özelliği devre dışı + NoActive: Aktif web anahtarı bulunamadı + NotFound: Web anahtarı bulunamadı + +AggregateTypes: + action: Eylem + instance: Instance + key_pair: Anahtar Çifti + org: Organizasyon + project: Proje + user: Kullanıcı + usergrant: Kullanıcı yetkisi + quota: Kota + feature: Özellik + target: Hedef + execution: Yürütme + user_schema: Kullanıcı Şeması + auth_request: Kimlik Doğrulama İsteği + device_auth: Cihaz Kimlik Doğrulaması + idpintent: IDP Niyeti + limits: Limitler + milestone: Kilometre Taşı + oidc_session: OIDC Oturumu + restrictions: Kısıtlamalar + system: Sistem + session: Oturum + web_key: Web Anahtarı + saml_request: SAML İsteği + saml_session: SAML Oturumu + +EventTypes: + execution: + set: Yürütme ayarlandı + removed: Yürütme silindi + target: + added: Hedef oluşturuldu + changed: Hedef değiştirildi + removed: Hedef silindi + user: + added: Kullanıcı eklendi + selfregistered: Kullanıcı kendini kaydetti + initialization: + code: + added: Başlatma kodu oluşturuldu + sent: Başlatma kodu gönderildi + check: + succeeded: Başlatma kontrolü başarılı + failed: Başlatma kontrolü başarısız + token: + added: Erişim Token'ı oluşturuldu + v2.added: Erişim Token'ı oluşturuldu + removed: Erişim Token'ı kaldırıldı + impersonated: Kullanıcı taklit edildi + username: + reserved: Kullanıcı adı rezerve edildi + released: Kullanıcı adı serbest bırakıldı + changed: Kullanıcı adı değiştirildi + email: + reserved: E-posta adresi rezerve edildi + released: E-posta adresi serbest bırakıldı + changed: E-posta adresi değiştirildi + verified: E-posta adresi doğrulandı + verification: + failed: E-posta adresi doğrulaması başarısız + code: + added: E-posta adresi doğrulama kodu oluşturuldu + sent: E-posta adresi doğrulama kodu gönderildi + machine: + added: Teknik kullanıcı eklendi + changed: Teknik kullanıcı değiştirildi + key: + added: Anahtar eklendi + removed: Anahtar kaldırıldı + secret: + set: Gizli anahtar ayarlandı + updated: Gizli anahtar hash'i güncellendi + removed: Gizli anahtar kaldırıldı + check: + succeeded: Gizli anahtar kontrolü başarılı + failed: Gizli anahtar kontrolü başarısız + human: + added: Kişi eklendi + selfregistered: Kişi kendini kaydetti + avatar: + added: Avatar eklendi + removed: Avatar kaldırıldı + initialization: + code: + added: Başlatma kodu oluşturuldu + sent: Başlatma kodu gönderildi + check: + succeeded: Başlatma kontrolü başarılı + failed: Başlatma kontrolü başarısız + invite: + code: + added: Davet kodu oluşturuldu + sent: Davet kodu gönderildi + check: + succeeded: Davet kontrolü başarılı + failed: Davet kontrolü başarısız + username: + reserved: Kullanıcı adı rezerve edildi + released: Kullanıcı adı serbest bırakıldı + email: + changed: E-posta adresi değiştirildi + verified: E-posta adresi doğrulandı + verification: + failed: E-posta adresi doğrulaması başarısız + code: + added: E-posta adresi doğrulama kodu oluşturuldu + sent: E-posta adresi doğrulama kodu gönderildi + password: + changed: Şifre değiştirildi + code: + added: Şifre kodu oluşturuldu + sent: Şifre kodu gönderildi + check: + succeeded: Şifre kontrolü başarılı + failed: Şifre kontrolü başarısız + change: + sent: Şifre değişikliği gönderildi + hash: + updated: Şifre hash'i güncellendi + externallogin: + check: + succeeded: Harici giriş başarılı + externalidp: + added: Harici IDP eklendi + removed: Harici IDP kaldırıldı + cascade: + removed: Harici IDP basamaklı kaldırıldı + id: + migrated: IDP'nin Harici UserID'si taşındı + phone: + changed: Telefon numarası değiştirildi + verified: Telefon numarası doğrulandı + verification: + failed: Telefon numarası doğrulaması başarısız + code: + added: Telefon numarası kodu oluşturuldu + sent: Telefon numarası kodu gönderildi + removed: Telefon numarası kaldırıldı + profile: + changed: Kullanıcı profili değiştirildi + address: + changed: Kullanıcı adresi değiştirildi + mfa: + otp: + added: Çok faktörlü OTP eklendi + verified: Çok faktörlü OTP doğrulandı + removed: Çok faktörlü OTP kaldırıldı + check: + succeeded: Çok faktörlü OTP kontrolü başarılı + failed: Çok faktörlü OTP kontrolü başarısız + sms: + added: Çok faktörlü OTP SMS eklendi + removed: Çok faktörlü OTP SMS kaldırıldı + code: + added: Çok faktörlü OTP SMS kodu eklendi + sent: Çok faktörlü OTP SMS kodu gönderildi + check: + succeeded: Çok faktörlü OTP SMS kontrolü başarılı + failed: Çok faktörlü OTP SMS kontrolü başarısız + email: + added: Çok faktörlü OTP E-posta eklendi + removed: Çok faktörlü OTP E-posta kaldırıldı + code: + added: Çok faktörlü OTP E-posta kodu eklendi + sent: Çok faktörlü OTP E-posta kodu gönderildi + check: + succeeded: Çok faktörlü OTP E-posta kontrolü başarılı + failed: Çok faktörlü OTP E-posta kontrolü başarısız + u2f: + token: + added: Çok faktörlü U2F Token eklendi + verified: Çok faktörlü U2F Token doğrulandı + removed: Çok faktörlü U2F Token kaldırıldı + begin: + login: Çok faktörlü U2F kontrolü başlatıldı + check: + succeeded: Çok faktörlü U2F kontrolü başarılı + failed: Çok faktörlü U2F kontrolü başarısız + signcount: + changed: Çok faktörlü U2F Token'ın sağlama toplamı değiştirildi + init: + skipped: Çok faktörlü kimlik doğrulama başlatması atlandı + passwordless: + token: + added: Şifresiz Giriş için Token eklendi + verified: Şifresiz Giriş için Token doğrulandı + removed: Şifresiz Giriş için Token kaldırıldı + begin: + login: Şifresiz Giriş kontrolü başlatıldı + check: + succeeded: Şifresiz Giriş kontrolü başarılı + failed: Şifresiz Giriş kontrolü başarısız + signcount: + changed: Şifresiz Giriş Token'ının sağlama toplamı değiştirildi + initialization: + code: + added: Şifresiz başlatma kodu eklendi + sent: Şifresiz başlatma kodu gönderildi + requested: Şifresiz başlatma kodu istendi + check: + succeeded: Şifresiz başlatma kodu başarıyla kontrol edildi + failed: Şifresiz başlatma kodu kontrolü başarısız + signed: + out: Kullanıcı çıkış yaptı + refresh: + token: + added: Yenileme Token'ı oluşturuldu + renewed: Yenileme Token'ı yenilendi + removed: Yenileme Token'ı kaldırıldı + locked: Kullanıcı kilitlendi + unlocked: Kullanıcının kilidi açıldı + deactivated: Kullanıcı devre dışı bırakıldı + reactivated: Kullanıcı yeniden etkinleştirildi + removed: Kullanıcı kaldırıldı + password: + changed: Şifre değiştirildi + code: + added: Şifre kodu oluşturuldu + sent: Şifre kodu gönderildi + check: + succeeded: Şifre kontrolü başarılı + failed: Şifre kontrolü başarısız + phone: + changed: Telefon numarası değiştirildi + verified: Telefon numarası doğrulandı + verification: + failed: Telefon numarası doğrulaması başarısız + code: + added: Telefon numarası kodu oluşturuldu + sent: Telefon numarası kodu gönderildi + + profile: + changed: Kullanıcı profili değiştirildi + address: + changed: Kullanıcı adresi değiştirildi + mfa: + otp: + added: Çok faktörlü OTP eklendi + verified: Çok faktörlü OTP doğrulandı + removed: Çok faktörlü OTP kaldırıldı + check: + succeeded: Çok faktörlü OTP kontrolü başarılı + failed: Çok faktörlü OTP kontrolü başarısız + init: + skipped: Çok faktörlü OTP başlatması atlandı + init: + skipped: Çok faktörlü kimlik doğrulama başlatması atlandı + signed: + out: Kullanıcı çıkış yaptı + grant: + added: Yetkilendirme eklendi + changed: Yetkilendirme değiştirildi + removed: Yetkilendirme kaldırıldı + deactivated: Yetkilendirme devre dışı bırakıldı + reactivated: Yetkilendirme yeniden etkinleştirildi + reserved: Yetkilendirme rezerve edildi + released: Yetkilendirme serbest bırakıldı + cascade: + removed: Yetkilendirme kaldırıldı + changed: Yetkilendirme değiştirildi + metadata: + set: Kullanıcı metadata'sı ayarlandı + removed: Kullanıcı metadata'sı kaldırıldı + removed.all: Tüm kullanıcı metadata'sı kaldırıldı + domain: + claimed: Domain talep edildi + claimed.sent: Domain talep bildirimmi gönderildi + pat: + added: Kişisel Erişim Token'ı eklendi + removed: Kişisel Erişim Token'ı kaldırıldı + org: + added: Organizasyon eklendi + changed: Organizasyon değiştirildi + deactivated: Organizasyon devre dışı bırakıldı + reactivated: Organizasyon yeniden etkinleştirildi + removed: Organizasyon kaldırıldı + domain: + added: Domain eklendi + verification: + added: Domain doğrulaması eklendi + failed: Domain doğrulaması başarısız + verified: Domain doğrulandı + removed: Domain kaldırıldı + primary: + set: Birincil domain ayarlandı + reserved: Domain rezerve edildi + released: Domain serbest bırakıldı + name: + reserved: Organizasyon adı rezerve edildi + released: Organizasyon adı serbest bırakıldı + member: + added: Organizasyon üyesi eklendi + changed: Organizasyon üyesi değiştirildi + removed: Organizasyon üyesi kaldırıldı + cascade: + removed: Organizasyon üyesi basamaklı kaldırıldı + iam: + policy: + added: Sistem politikası eklendi + changed: Sistem politikası değiştirildi + removed: Sistem politikası kaldırıldı + idp: + config: + added: IDP yapılandırması eklendi + changed: IDP yapılandırması değiştirildi + removed: IDP yapılandırması kaldırıldı + deactivated: IDP yapılandırması devre dışı bırakıldı + reactivated: IDP yapılandırması yeniden etkinleştirildi + oidc: + config: + added: OIDC IDP yapılandırması eklendi + changed: OIDC IDP yapılandırması değiştirildi + saml: + config: + added: SAML IDP yapılandırması eklendi + changed: SAML IDP yapılandırması değiştirildi + jwt: + config: + added: JWT IDP yapılandırması eklendi + changed: JWT IDP yapılandırması değiştirildi + customtext: + set: Özel metin ayarlandı + removed: Özel metin kaldırıldı + template: + removed: Özel metin şablonu kaldırıldı + policy: + login: + added: Giriş Politikası eklendi + changed: Giriş Politikası değiştirildi + removed: Giriş Politikası kaldırıldı + idpprovider: + added: Kimlik Sağlayıcısı Giriş Politikasına eklendi + removed: Kimlik Sağlayıcısı Giriş Politikasından kaldırıldı + cascade: + removed: Kimlik Sağlayıcısı Giriş Politikasından basamaklı kaldırıldı + secondfactor: + added: İkinci faktör Giriş Politikasına eklendi + removed: İkinci faktör Giriş Politikasından kaldırıldı + multifactor: + added: Çok faktörlü Giriş Politikasına eklendi + removed: Çok faktörlü Giriş Politikasından kaldırıldı + password: + complexity: + added: Şifre karmaşıklık politikası eklendi + changed: Şifre karmaşıklık politikası değiştirildi + removed: Şifre karmaşıklık politikası kaldırıldı + age: + added: Şifre yaş politikası eklendi + changed: Şifre yaş politikası değiştirildi + removed: Şifre yaş politikası kaldırıldı + lockout: + added: Şifre kilitleme politikası eklendi + changed: Şifre kilitleme politikası değiştirildi + removed: Şifre kilitleme politikası kaldırıldı + label: + added: Etiket Politikası eklendi + changed: Etiket Politikası değiştirildi + activated: Etiket Politikası etkinleştirildi + removed: Etiket Politikası kaldırıldı + logo: + added: Logo Etiket Politikasına eklendi + removed: Logo Etiket Politikasından kaldırıldı + dark: + added: Logo (karanlık mod) Etiket Politikasına eklendi + removed: Logo (karanlık mod) Etiket Politikasından kaldırıldı + icon: + added: İkon Etiket Politikasına eklendi + removed: İkon Etiket Politikasından kaldırıldı + dark: + added: İkon (karanlık mod) Etiket Politikasına eklendi + removed: İkon (karanlık mod) Etiket Politikasından kaldırıldı + font: + added: Yazı tipi Etiket Politikasına eklendi + removed: Yazı tipi Etiket Politikasından kaldırıldı + assets: + removed: Varlıklar Etiket Politikasından kaldırıldı + privacy: + added: Gizlilik politikası ve Kullanım Şartları eklendi + changed: Gizlilik politikası ve Kullanım Şartları değiştirildi + removed: Gizlilik politikası ve Kullanım Şartları kaldırıldı + domain: + added: Domain politikası eklendi + changed: Domain politikası değiştirildi + removed: Domain politikası kaldırıldı + lockout: + added: Kilitleme politikası eklendi + changed: Kilitleme politikası değiştirildi + removed: Kilitleme politikası kaldırıldı + notification: + added: Bildirim politikası eklendi + changed: Bildirim politikası değiştirildi + removed: Bildirim politikası kaldırıldı + flow: + trigger_actions: + set: Eylem ayarlandı + cascade: + removed: Eylemler basamaklı kaldırıldı + removed: Eylemler kaldırıldı + cleared: Akış temizlendi + mail: + template: + added: E-Posta şablonu eklendi + changed: E-Posta şablonu değiştirildi + removed: E-Posta şablonu kaldırıldı + text: + added: E-Posta metni eklendi + changed: E-Posta metni değiştirildi + removed: E-Posta metni kaldırıldı + metadata: + removed: Metadata kaldırıldı + removed.all: Tüm metadata kaldırıldı + set: Metadata ayarlandı + project: + added: Proje eklendi + changed: Proje değiştirildi + deactivated: Proje devre dışı bırakıldı + reactivated: Proje yeniden etkinleştirildi + removed: Proje kaldırıldı + member: + added: Proje üyesi eklendi + changed: Proje üyesi değiştirildi + removed: Proje üyesi kaldırıldı + cascade: + removed: Proje üyesi basamaklı kaldırıldı + role: + added: Proje rolü eklendi + changed: Proje rolü değiştirildi + removed: Proje rolü kaldırıldı + grant: + added: Yönetim erişimi eklendi + changed: Yönetim erişimi değiştirildi + removed: Yönetim erişimi kaldırıldı + deactivated: Yönetim erişimi devre dışı bırakıldı + reactivated: Yönetim erişimi yeniden etkinleştirildi + cascade: + changed: Yönetim erişimi değiştirildi + member: + added: Yönetim erişimi üyesi eklendi + changed: Yönetim erişimi üyesi değiştirildi + removed: Yönetim erişimi üyesi kaldırıldı + cascade: + removed: Yönetim erişimi basamaklı kaldırıldı + application: + added: Uygulama eklendi + changed: Uygulama değiştirildi + removed: Uygulama kaldırıldı + deactivated: Uygulama devre dışı bırakıldı + reactivated: Uygulama yeniden etkinleştirildi + oidc: + secret: + check: + succeeded: OIDC İstemci Gizli Anahtarı kontrolü başarılı + failed: OIDC İstemci Gizli Anahtarı kontrolü başarısız + key: + added: OIDC Uygulama Anahtarı eklendi + removed: OIDC Uygulama Anahtarı kaldırıldı + api: + secret: + check: + succeeded: API gizli anahtarı kontrolü başarılı + failed: API gizli anahtarı kontrolü başarısız + key: + added: Uygulama anahtarı eklendi + removed: Uygulama anahtarı kaldırıldı + config: + saml: + added: SAML Yapılandırması eklendi + changed: SAML Yapılandırması değiştirildi + oidc: + added: OIDC Yapılandırması eklendi + changed: OIDC Yapılandırması değiştirildi + secret: + changed: OIDC gizli anahtarı değiştirildi + updated: OIDC gizli anahtarı hash'i güncellendi + api: + added: API Yapılandırması eklendi + changed: API Yapılandırması değiştirildi + secret: + changed: API gizli anahtarı değiştirildi + updated: API gizli anahtarı hash'i güncellendi + policy: + password: + complexity: + added: Şifre karmaşıklık politikası eklendi + changed: Şifre karmaşıklık politikası değiştirildi + age: + added: Şifre yaş politikası eklendi + changed: Şifre yaş politikası değiştirildi + lockout: + added: Şifre kilitleme politikası eklendi + changed: Şifre kilitleme politikası değiştirildi + iam: + setup: + started: ZITADEL kurulumu başlatıldı + done: ZITADEL kurulumu tamamlandı + global: + org: + set: Global organizasyon ayarlandı + project: + iam: + set: ZITADEL projesi ayarlandı + member: + added: ZITADEL üyesi eklendi + changed: ZITADEL üyesi değiştirildi + removed: ZITADEL üyesi kaldırıldı + cascade: + removed: ZITADEL üyesi basamaklı kaldırıldı + idp: + config: + added: IDP yapılandırması eklendi + changed: IDP yapılandırması değiştirildi + removed: IDP yapılandırması kaldırıldı + deactivated: IDP yapılandırması devre dışı bırakıldı + reactivated: IDP yapılandırması yeniden etkinleştirildi + oidc: + config: + added: OIDC IDP yapılandırması eklendi + changed: OIDC IDP yapılandırması değiştirildi + saml: + config: + added: SAML IDP yapılandırması eklendi + changed: SAML IDP yapılandırması değiştirildi + jwt: + config: + added: Kimlik sağlayıcısına JWT yapılandırması eklendi + changed: Kimlik sağlayıcısından JWT yapılandırması kaldırıldı + customtext: + set: Metin ayarlandı + removed: Metin kaldırıldı + policy: + login: + added: Varsayılan Giriş Politikası eklendi + changed: Varsayılan Giriş Politikası değiştirildi + idpprovider: + added: Kimlik Sağlayıcısı Varsayılan Giriş Politikasına eklendi + removed: Kimlik Sağlayıcısı Varsayılan Giriş Politikasından kaldırıldı + label: + added: Etiket Politikası eklendi + changed: Etiket Politikası değiştirildi + activated: Etiket Politikası etkinleştirildi + logo: + added: Logo Etiket Politikasına eklendi + removed: Logo Etiket Politikasından kaldırıldı + dark: + added: Logo (karanlık mod) Etiket Politikasına eklendi + removed: Logo (karanlık mod) Etiket Politikasından kaldırıldı + icon: + added: İkon Etiket Politikasına eklendi + removed: İkon Etiket Politikasından kaldırıldı + dark: + added: İkon (karanlık mod) Etiket Politikasına eklendi + removed: İkon (karanlık mod) Etiket Politikasından kaldırıldı + font: + added: Yazı tipi Etiket Politikasına eklendi + removed: Yazı tipi Etiket Politikasından kaldırıldı + assets: + removed: Varlıklar Etiket Politikasından kaldırıldı + default: + language: + set: Varsayılan dil ayarlandı + oidc: + settings: + added: OIDC yapılandırması eklendi + changed: OIDC yapılandırması değiştirildi + removed: OIDC yapılandırması kaldırıldı + secret: + generator: + added: Gizli anahtar oluşturucu eklendi + changed: Gizli anahtar oluşturucu değiştirildi + removed: Gizli anahtar oluşturucu kaldırıldı + smtp: + config: + added: SMTP yapılandırması eklendi + changed: SMTP yapılandırması değiştirildi + activated: SMTP yapılandırması etkinleştirildi + deactivated: SMTP yapılandırması devre dışı bırakıldı + removed: SMTP yapılandırması kaldırıldı + password: + changed: SMTP yapılandırması gizli anahtarı değiştirildi + sms: + config: + twilio: + added: Twilio SMS sağlayıcısı eklendi + changed: Twilio SMS sağlayıcısı değiştirildi + token: + changed: Twilio SMS sağlayıcısı token'ı değiştirildi + removed: Twilio SMS sağlayıcısı kaldırıldı + activated: Twilio SMS sağlayıcısı etkinleştirildi + deactivated: Twilio SMS sağlayıcısı devre dışı bırakıldı + key_pair: + added: Anahtar çifti eklendi + certificate: + added: Sertifika eklendi + action: + added: Eylem eklendi + changed: Eylem değiştirildi + deactivated: Eylem devre dışı bırakıldı + reactivated: Eylem yeniden etkinleştirildi + removed: Eylem kaldırıldı + instance: + added: Örnek eklendi + changed: Örnek değiştirildi + customtext: + removed: Özel metin kaldırıldı + set: Özel metin ayarlandı + template: + removed: Özel metin şablonu kaldırıldı + default: + language: + set: Varsayılan dil ayarlandı + org: + set: Varsayılan organizasyon ayarlandı + domain: + added: Domain eklendi + primary: + set: Birincil domain ayarlandı + removed: Domain kaldırıldı + iam: + console: + set: ZITADEL Konsol uygulaması ayarlandı + project: + set: ZITADEL projesi ayarlandı + mail: + template: + added: E-Posta şablonu eklendi + changed: E-Posta şablonu değiştirildi + text: + added: E-Posta metni eklendi + changed: E-Posta metni değiştirildi + member: + added: Örnek üyesi eklendi + changed: Örnek üyesi değiştirildi + removed: Örnek üyesi kaldırıldı + cascade: + removed: Örnek üyesi basamaklı kaldırıldı + notification: + provider: + debug: + fileadded: Dosya hata ayıklama bildirim sağlayıcısı eklendi + filechanged: Dosya hata ayıklama bildirim sağlayıcısı değiştirildi + fileremoved: Dosya hata ayıklama bildirim sağlayıcısı kaldırıldı + logadded: Log hata ayıklama bildirim sağlayıcısı eklendi + logchanged: Log hata ayıklama bildirim sağlayıcısı değiştirildi + logremoved: Log hata ayıklama bildirim sağlayıcısı kaldırıldı + oidc: + settings: + added: OIDC ayarları eklendi + changed: OIDC ayarları değiştirildi + policy: + domain: + added: Domain politikası eklendi + changed: Domain politikası değiştirildi + label: + activated: Etiket politikası etkinleştirildi + added: Etiket politikası eklendi + assets: + removed: Varlık etiket politikasından kaldırıldı + changed: Etiket politikası değiştirildi + font: + added: Yazı tipi etiket politikasına eklendi + removed: Yazı tipi etiket politikasından kaldırıldı + icon: + added: İkon etiket politikasına eklendi + removed: İkon etiket politikasından kaldırıldı + dark: + added: İkon karanlık etiket politikasına eklendi + removed: İkon karanlık etiket politikasından kaldırıldı + logo: + added: Logo etiket politikasına eklendi + removed: Logo etiket politikasından kaldırıldı + dark: + added: Logo karanlık etiket politikasına eklendi + removed: Logo karanlık etiket politikasından kaldırıldı + lockout: + added: Kilitleme politikası eklendi + changed: Kilitleme politikası değiştirildi + login: + added: Giriş politikası eklendi + changed: Giriş politikası değiştirildi + idpprovider: + added: Kimlik Sağlayıcısı giriş politikasına eklendi + cascade: + removed: Kimlik Sağlayıcısı giriş politikasından basamaklı kaldırıldı + removed: Kimlik Sağlayıcısı giriş politikasından kaldırıldı + multifactor: + added: Çok faktörlü giriş politikasına eklendi + removed: Çok faktörlü giriş politikasından kaldırıldı + secondfactor: + added: İkinci faktör giriş politikasına eklendi + removed: İkinci faktör giriş politikasından kaldırıldı + password: + age: + added: Şifre yaş politikası eklendi + changed: Şifre yaş politikası değiştirildi + complexity: + added: Şifre karmaşıklık politikası eklendi + changed: Şifre karmaşıklık politikası kaldırıldı + privacy: + added: Gizlilik politikası eklendi + changed: Gizlilik politikası değiştirildi + security: + set: Güvenlik politikası ayarlandı + + removed: Örnek kaldırıldı + secret: + generator: + added: Gizli anahtar oluşturucu eklendi + changed: Gizli anahtar oluşturucu değiştirildi + removed: Gizli anahtar oluşturucu kaldırıldı + sms: + configtwilio: + activated: Twilio SMS yapılandırması etkinleştirildi + added: Twilio SMS yapılandırması eklendi + changed: Twilio SMS yapılandırması değiştirildi + deactivated: Twilio SMS yapılandırması devre dışı bırakıldı + removed: Twilio SMS yapılandırması kaldırıldı + token: + changed: Twilio SMS yapılandırmasının token'ı değiştirildi + smtp: + config: + added: SMTP yapılandırması eklendi + changed: SMTP yapılandırması değiştirildi + activated: SMTP yapılandırması etkinleştirildi + deactivated: SMTP yapılandırması devre dışı bırakıldı + password: + changed: SMTP yapılandırmasının şifresi değiştirildi + removed: SMTP yapılandırması kaldırıldı + user_schema: + created: Kullanıcı Şeması oluşturuldu + updated: Kullanıcı Şeması güncellendi + deactivated: Kullanıcı Şeması devre dışı bırakıldı + reactivated: Kullanıcı Şeması yeniden etkinleştirildi + deleted: Kullanıcı Şeması silindi + user: + created: Kullanıcı oluşturuldu + updated: Kullanıcı güncellendi + deleted: Kullanıcı silindi + email: + updated: E-posta adresi değiştirildi + verified: E-posta adresi doğrulandı + verification: + failed: E-posta adresi doğrulaması başarısız + code: + added: E-posta adresi doğrulama kodu oluşturuldu + sent: E-posta adresi doğrulama kodu gönderildi + phone: + updated: Telefon numarası değiştirildi + verified: Telefon numarası doğrulandı + verification: + failed: Telefon numarası doğrulaması başarısız + code: + added: Telefon numarası doğrulama kodu oluşturuldu + sent: Telefon numarası doğrulama kodu gönderildi + + + web_key: + added: Web Anahtarı eklendi + activated: Web Anahtarı etkinleştirildi + deactivated: Web Anahtarı devre dışı bırakıldı + removed: Web Anahtarı kaldırıldı + +Application: + OIDC: + UnsupportedVersion: OIDC sürümünüz desteklenmiyor + V1: + NotCompliant: Yapılandırmanız uyumlu değil ve OIDC 1.0 standardından farklı. + NoRedirectUris: En az bir yönlendirme uri'si kayıtlı olmalı. + NotAllCombinationsAreAllowed: Yapılandırma uyumlu, ancak tüm olası kombinasyonlara izin verilmiyor. + Code: + RedirectUris: + HttpOnlyForWeb: Yetki türü kodu, web uygulama türü için sadece http yönlendirme uri'lerine izin verir. + CustomOnlyForNative: Yetki türü kodu, yerel uygulama türü için sadece özel yönlendirme uri'lerine izin verir (örn appname://) + Implicit: + RedirectUris: + CustomNotAllowed: Örtük yetki türü özel yönlendirme uri'lerine izin vermez + HttpNotAllowed: Örtük yetki türü http yönlendirme uri'lerine izin vermez + HttpLocalhostOnlyForNative: Http://localhost yönlendirme uri'si sadece yerel uygulamalar için izin verilir. + Native: + AuthMethodType: + NotNone: Yerel uygulamalar authmethodtype none olmalı. + RedirectUris: + MustBeHttpLocalhost: Yönlendirme URI'leri kendi protokolünüzle, http://127.0.0.1, http://[::1] veya http://localhost ile başlamalı. + UserAgent: + AuthMethodType: + NotNone: Kullanıcı aracısı uygulaması authmethodtype none olmalı. + GrantType: + Refresh: + NoAuthCode: Yenileme Token'ı sadece Yetkilendirme Kodu ile birlikte izin verilir. + +Action: + Flow: + Type: + Unspecified: Belirtilmemiş + ExternalAuthentication: Harici Kimlik Doğrulama + CustomiseToken: Token Tamamlama + InternalAuthentication: Dahili Kimlik Doğrulama + CustomizeSAMLResponse: SAMLResponse Tamamlama + TriggerType: + Unspecified: Belirtilmemiş + PostAuthentication: Kimlik Doğrulama Sonrası + PreCreation: Oluşturma Öncesi + PostCreation: Oluşturma Sonrası + PreUserinfoCreation: Userinfo oluşturma öncesi + PreAccessTokenCreation: Erişim token'ı oluşturma öncesi + PreSAMLResponseCreation: SAMLResponse oluşturma öncesi diff --git a/internal/static/i18n/zh.yaml b/internal/static/i18n/zh.yaml index 424c6cc645..d4b32571ba 100644 --- a/internal/static/i18n/zh.yaml +++ b/internal/static/i18n/zh.yaml @@ -115,6 +115,7 @@ Errors: AlreadyVerified: 手机号码已经验证 Empty: 电话号码是空的 NotChanged: 电话号码没有改变 + VerifyingRemovalIsNotSupported: 验证手机号码删除不受支持 Address: NotFound: 找不到地址 NotChanged: 地址没有改变 @@ -193,7 +194,7 @@ Errors: AlreadyExists: 实例已经存在 NotChanged: 实例没有改变 Org: - AlreadyExists: 组织名称已被占用 + AlreadyExists: 该组织名称或 ID 已被占用 Invalid: 组织无效 AlreadyDeactivated: 组织已停用 AlreadyActive: 组织已处于启用状态 @@ -436,8 +437,6 @@ Errors: Invalid: 用户授权无效 NotChanged: 用户授权未更改 IDMissing: 没有 ID - NotActive: 用户授权不是启用状态 - NotInactive: 用户授权不是停用状态 NoPermissionForProject: 用户对此项目没有权限 RoleKeyNotFound: 角色不存在 Member: diff --git a/internal/v2/system/event.go b/internal/v2/system/event.go new file mode 100644 index 0000000000..313c0fb293 --- /dev/null +++ b/internal/v2/system/event.go @@ -0,0 +1,44 @@ +package system + +import ( + "context" + + "github.com/zitadel/zitadel/internal/eventstore" +) + +func init() { + eventstore.RegisterFilterEventMapper(AggregateType, IDGeneratedType, eventstore.GenericEventMapper[IDGeneratedEvent]) +} + +const IDGeneratedType = AggregateType + ".id.generated" + +type IDGeneratedEvent struct { + eventstore.BaseEvent `json:"-"` + + ID string `json:"id"` +} + +func (e *IDGeneratedEvent) SetBaseEvent(b *eventstore.BaseEvent) { + e.BaseEvent = *b +} + +func (e *IDGeneratedEvent) Payload() interface{} { + return e +} + +func (e *IDGeneratedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return nil +} + +func NewIDGeneratedEvent( + ctx context.Context, + id string, +) *IDGeneratedEvent { + return &IDGeneratedEvent{ + BaseEvent: *eventstore.NewBaseEventForPush( + ctx, + eventstore.NewAggregate(ctx, AggregateOwner, AggregateType, "v1"), + IDGeneratedType), + ID: id, + } +} diff --git a/load-test/package.json b/load-test/package.json index 73bf8fd449..fd47427f60 100644 --- a/load-test/package.json +++ b/load-test/package.json @@ -28,6 +28,7 @@ "scripts": { "bundle": "webpack", "lint": "prettier --check src/**", - "lint:fix": "prettier --write src" + "lint:fix": "prettier --write src", + "clean": "rm -rf dist .turbo node_modules" } } diff --git a/package.json b/package.json new file mode 100644 index 0000000000..eb0c881b11 --- /dev/null +++ b/package.json @@ -0,0 +1,26 @@ +{ + "packageManager": "pnpm@9.1.2+sha256.19c17528f9ca20bd442e4ca42f00f1b9808a9cb419383cd04ba32ef19322aba7", + "private": true, + "name": "zitadel-monorepo", + "scripts": { + "changeset": "changeset", + "devcontainer": "devcontainer", + "devcontainer:lint-unit": "pnpm devcontainer up --config .devcontainer/turbo-lint-unit/devcontainer.json --workspace-folder . --remove-existing-container", + "devcontainer:integration:login": "pnpm devcontainer up --config .devcontainer/login-integration/devcontainer.json --workspace-folder . --remove-existing-container", + "clean": "turbo run clean", + "clean:all": "pnpm run clean && rm -rf .turbo node_modules", + "generate": "turbo run generate" + }, + "pnpm": { + "overrides": { + "@typescript-eslint/parser": "^8.35.1", + "@zitadel/client": "workspace:*", + "@zitadel/proto": "workspace:*" + } + }, + "devDependencies": { + "@changesets/cli": "^2.29.5", + "@devcontainers/cli": "^0.80.0", + "turbo": "2.5.5" + } +} diff --git a/packages/.changeset/README.md b/packages/.changeset/README.md new file mode 100644 index 0000000000..e5b6d8d6a6 --- /dev/null +++ b/packages/.changeset/README.md @@ -0,0 +1,8 @@ +# Changesets + +Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works +with multi-package repos, or single-package repos to help you version and publish your code. You can +find the full documentation for it [in our repository](https://github.com/changesets/changesets) + +We have a quick list of common questions to get you started engaging with this project in +[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) diff --git a/packages/.changeset/config.json b/packages/.changeset/config.json new file mode 100644 index 0000000000..6551b0ca8a --- /dev/null +++ b/packages/.changeset/config.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@3.0.3/schema.json", + "changelog": "@changesets/cli/changelog", + "commit": false, + "fixed": [], + "linked": [], + "access": "public", + "baseBranch": "main", + "updateInternalDependencies": "patch" +} diff --git a/packages/zitadel-client/.eslintrc.cjs b/packages/zitadel-client/.eslintrc.cjs new file mode 100644 index 0000000000..dc9f0a66c3 --- /dev/null +++ b/packages/zitadel-client/.eslintrc.cjs @@ -0,0 +1,17 @@ +module.exports = { + root: true, + extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"], + parser: "@typescript-eslint/parser", + plugins: ["@typescript-eslint"], + env: { + node: true, + es2022: true, + }, + parserOptions: { + ecmaVersion: 2022, + sourceType: "module", + }, + rules: { + "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }] + } +}; diff --git a/packages/zitadel-client/.gitignore b/packages/zitadel-client/.gitignore new file mode 100644 index 0000000000..8ff894e88c --- /dev/null +++ b/packages/zitadel-client/.gitignore @@ -0,0 +1,4 @@ +src/proto +node_modules +dist +.turbo diff --git a/packages/zitadel-client/CHANGELOG.md b/packages/zitadel-client/CHANGELOG.md new file mode 100644 index 0000000000..f1107bcc5a --- /dev/null +++ b/packages/zitadel-client/CHANGELOG.md @@ -0,0 +1,77 @@ +# @zitadel/client + +## 1.2.0 + +### Minor Changes + +- 62ad388: revert CJS support + +## 1.1.0 + +### Minor Changes + +- 9692297: add CJS and ESM support + +## 1.0.7 + +### Patch Changes + +- Updated dependencies [97b0332] + - @zitadel/proto@1.0.4 + +## 1.0.6 + +### Patch Changes + +- 90fbdd1: use node16/nodenext module resolution +- Updated dependencies [90fbdd1] + - @zitadel/proto@1.0.3 + +## 1.0.5 + +### Patch Changes + +- 4fa22c0: fix export for grpcweb transport + +## 1.0.4 + +### Patch Changes + +- 28dc956: dynamic properties for system token utility + +## 1.0.3 + +### Patch Changes + +- ef1c801: add missing client transport utility + +## 1.0.2 + +### Patch Changes + +- Updated dependencies + - @zitadel/proto@1.0.2 + +## 1.0.1 + +### Patch Changes + +- README updates +- Updated dependencies + - @zitadel/proto@1.0.1 + +## 1.0.0 + +### Major Changes + +- 32e1199: Initial Release + +### Minor Changes + +- f32ab7f: Initial release + +### Patch Changes + +- Updated dependencies [f32ab7f] +- Updated dependencies [32e1199] + - @zitadel/proto@1.0.0 diff --git a/packages/zitadel-client/README.md b/packages/zitadel-client/README.md new file mode 100644 index 0000000000..0a14fe32f5 --- /dev/null +++ b/packages/zitadel-client/README.md @@ -0,0 +1,53 @@ +# ZITADEL Client + +This package exports services and utilities to interact with ZITADEL + +## Installation + +To install the package, use npm or yarn: + +```sh +npm install @zitadel/client +``` + +or + +```sh +yarn add @zitadel/client +``` + +## Usage + +### Importing Services + +You can import and use the services provided by this package to interact with ZITADEL. + +```ts +import { createSettingsServiceClient, makeReqCtx } from "@zitadel/client/v2"; + +// Example usage +const transport = createServerTransport(process.env.ZITADEL_SERVICE_USER_TOKEN!, { baseUrl: process.env.ZITADEL_API_URL! }); + +const settingsService = createSettingsServiceClient(transport); + +settingsService.getBrandingSettings({ ctx: makeReqCtx("orgId") }, {}); +``` + +### Utilities + +This package also provides various utilities to work with ZITADEL + +```ts +import { timestampMs } from "@zitadel/client"; + +// Example usage +console.log(`${timestampMs(session.creationDate)}`); +``` + +## Documentation + +For detailed documentation and API references, please visit the [ZITADEL documentation](https://zitadel.com/docs). + +## Contributing + +Contributions are welcome! Please read the contributing guidelines before getting started. diff --git a/packages/zitadel-client/knip.json b/packages/zitadel-client/knip.json new file mode 100644 index 0000000000..571d5f9dac --- /dev/null +++ b/packages/zitadel-client/knip.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://unpkg.com/knip@5/schema.json", + "ignoreDependencies": [ + "@zitadel/proto" + ] +} \ No newline at end of file diff --git a/packages/zitadel-client/node/package.json b/packages/zitadel-client/node/package.json new file mode 100644 index 0000000000..0245d755cb --- /dev/null +++ b/packages/zitadel-client/node/package.json @@ -0,0 +1,6 @@ +{ + "name": "@zitadel/client-node", + "main": "../dist/node.js", + "types": "../dist/node.d.ts", + "type": "module" +} diff --git a/packages/zitadel-client/package.json b/packages/zitadel-client/package.json new file mode 100644 index 0000000000..cef3a02021 --- /dev/null +++ b/packages/zitadel-client/package.json @@ -0,0 +1,95 @@ +{ + "name": "@zitadel/client", + "version": "1.3.1", + "license": "MIT", + "publishConfig": { + "access": "public" + }, + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.cjs" + }, + "./v1": { + "types": { + "import": "./dist/v1.d.ts", + "require": "./dist/v1.d.cts", + "default": "./dist/v1.d.ts" + }, + "import": "./dist/v1.js", + "require": "./dist/v1.cjs" + }, + "./v2": { + "types": { + "import": "./dist/v2.d.ts", + "require": "./dist/v2.d.cts", + "default": "./dist/v2.d.ts" + }, + "import": "./dist/v2.js", + "require": "./dist/v2.cjs" + }, + "./v3alpha": { + "types": { + "import": "./dist/v3alpha.d.ts", + "require": "./dist/v3alpha.d.cts", + "default": "./dist/v3alpha.d.ts" + }, + "import": "./dist/v3alpha.js", + "require": "./dist/v3alpha.cjs" + }, + "./node": { + "types": { + "import": "./dist/node.d.ts", + "require": "./dist/node.d.cts", + "default": "./dist/node.d.ts" + }, + "import": "./dist/node.js", + "require": "./dist/node.cjs" + }, + "./web": { + "types": { + "import": "./dist/web.d.ts", + "require": "./dist/web.d.cts", + "default": "./dist/web.d.ts" + }, + "import": "./dist/web.js", + "require": "./dist/web.cjs" + } + }, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist/**" + ], + "sideEffects": false, + "scripts": { + "build": "tsup", + "lint": "pnpm run '/^lint:check:.*$/'", + "lint:check:eslint": "eslint 'src/**/*.ts*'", + "lint:check:knip": "knip", + "test:unit": "vitest --run", + "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist" + }, + "dependencies": { + "@bufbuild/protobuf": "^2.2.2", + "@connectrpc/connect": "^2.0.0", + "@connectrpc/connect-node": "^2.0.0", + "@connectrpc/connect-web": "^2.0.0", + "@zitadel/proto": "workspace:*", + "jose": "^5.3.0" + }, + "devDependencies": { + "@bufbuild/protocompile": "^0.0.1", + "@bufbuild/buf": "^1.53.0", + "@types/node": "^24.0.14", + "@typescript-eslint/eslint-plugin": "^8.15.0", + "@typescript-eslint/parser": "^8.15.0", + "eslint": "^8.57.0", + "knip": "^5.61.3", + "tsup": "^8.4.0", + "typescript": "^5.8.3", + "vitest": "^2.0.0" + } +} diff --git a/packages/zitadel-client/src/helpers.ts b/packages/zitadel-client/src/helpers.ts new file mode 100644 index 0000000000..637cadf538 --- /dev/null +++ b/packages/zitadel-client/src/helpers.ts @@ -0,0 +1,11 @@ +import type { DescService } from "@bufbuild/protobuf"; +import { Timestamp, timestampDate } from "@bufbuild/protobuf/wkt"; +import { createClient, Transport } from "@connectrpc/connect"; + +export function createClientFor(service: TService) { + return (transport: Transport) => createClient(service, transport); +} + +export function toDate(timestamp: Timestamp | undefined): Date | undefined { + return timestamp ? timestampDate(timestamp) : undefined; +} diff --git a/packages/zitadel-client/src/index.ts b/packages/zitadel-client/src/index.ts new file mode 100644 index 0000000000..f3f93e593f --- /dev/null +++ b/packages/zitadel-client/src/index.ts @@ -0,0 +1,10 @@ +export { createClientFor, toDate } from "./helpers.js"; +export { NewAuthorizationBearerInterceptor } from "./interceptors.js"; + +// TODO: Move this to `./protobuf.ts` and export it from there +export { create, fromJson, toJson } from "@bufbuild/protobuf"; +export type { JsonObject } from "@bufbuild/protobuf"; +export type { GenService } from "@bufbuild/protobuf/codegenv1"; +export { TimestampSchema, timestampDate, timestampFromDate, timestampFromMs, timestampMs } from "@bufbuild/protobuf/wkt"; +export type { Duration, Timestamp } from "@bufbuild/protobuf/wkt"; +export type { Client, Code, ConnectError } from "@connectrpc/connect"; diff --git a/packages/zitadel-client/src/interceptors.test.ts b/packages/zitadel-client/src/interceptors.test.ts new file mode 100644 index 0000000000..b43368ff19 --- /dev/null +++ b/packages/zitadel-client/src/interceptors.test.ts @@ -0,0 +1,67 @@ +import { Int32Value } from "@bufbuild/protobuf/wkt"; +import { compileService } from "@bufbuild/protocompile"; +import { createRouterTransport, HandlerContext } from "@connectrpc/connect"; +import { describe, expect, test, vitest } from "vitest"; +import { NewAuthorizationBearerInterceptor } from "./interceptors.js"; + +const TestService = compileService(` + syntax = "proto3"; + package handwritten; + service TestService { + rpc Unary(Int32Value) returns (StringValue); + } + message Int32Value { + int32 value = 1; + } + message StringValue { + string value = 1; + } +`); + +describe("NewAuthorizationBearerInterceptor", () => { + const transport = { + interceptors: [NewAuthorizationBearerInterceptor("mytoken")], + }; + + test("injects the authorization token", async () => { + const handler = vitest.fn((request: Int32Value, _context: HandlerContext) => { + return { value: request.value.toString() }; + }); + + const service = createRouterTransport( + ({ rpc }) => { + rpc(TestService.method.unary, handler); + }, + { transport }, + ); + + await service.unary(TestService.method.unary, undefined, undefined, {}, { value: 9001 }); + + expect(handler).toBeCalled(); + expect(handler.mock.calls[0][1].requestHeader.get("Authorization")).toBe("Bearer mytoken"); + }); + + test("do not overwrite the previous authorization token", async () => { + const handler = vitest.fn((request: Int32Value, _context: HandlerContext) => { + return { value: request.value.toString() }; + }); + + const service = createRouterTransport( + ({ rpc }) => { + rpc(TestService.method.unary, handler); + }, + { transport }, + ); + + await service.unary( + TestService.method.unary, + undefined, + undefined, + { Authorization: "Bearer somethingelse" }, + { value: 9001 }, + ); + + expect(handler).toBeCalled(); + expect(handler.mock.calls[0][1].requestHeader.get("Authorization")).toBe("Bearer somethingelse"); + }); +}); diff --git a/packages/zitadel-client/src/interceptors.ts b/packages/zitadel-client/src/interceptors.ts new file mode 100644 index 0000000000..9d719c7d70 --- /dev/null +++ b/packages/zitadel-client/src/interceptors.ts @@ -0,0 +1,16 @@ +import type { Interceptor } from "@connectrpc/connect"; + +/** + * Creates an interceptor that adds an Authorization header with a Bearer token. + * @param token + */ +export function NewAuthorizationBearerInterceptor(token: string): Interceptor { + return (next) => (req) => { + // TODO: I am not what is the intent of checking for the Authorization header + // and setting it if it is not present. + if (!req.header.get("Authorization")) { + req.header.set("Authorization", `Bearer ${token}`); + } + return next(req); + }; +} diff --git a/packages/zitadel-client/src/node.ts b/packages/zitadel-client/src/node.ts new file mode 100644 index 0000000000..0b15310d2c --- /dev/null +++ b/packages/zitadel-client/src/node.ts @@ -0,0 +1,36 @@ +import { createGrpcTransport, GrpcTransportOptions } from "@connectrpc/connect-node"; +import { importPKCS8, SignJWT } from "jose"; +import { NewAuthorizationBearerInterceptor } from "./interceptors.js"; + +/** + * Create a server transport using grpc with the given token and configuration options. + * @param token + * @param opts + */ +export function createServerTransport(token: string, opts: GrpcTransportOptions) { + return createGrpcTransport({ + ...opts, + interceptors: [...(opts.interceptors || []), NewAuthorizationBearerInterceptor(token)], + }); +} + +export async function newSystemToken({ + audience, + subject, + key, + expirationTime, +}: { + audience: string; + subject: string; + key: string; + expirationTime?: number | string | Date; +}) { + return await new SignJWT({}) + .setProtectedHeader({ alg: "RS256" }) + .setIssuedAt() + .setExpirationTime(expirationTime ?? "1h") + .setIssuer(subject) + .setSubject(subject) + .setAudience(audience) + .sign(await importPKCS8(key, "RS256")); +} diff --git a/packages/zitadel-client/src/v1.ts b/packages/zitadel-client/src/v1.ts new file mode 100644 index 0000000000..d04180cf88 --- /dev/null +++ b/packages/zitadel-client/src/v1.ts @@ -0,0 +1,11 @@ +import { createClientFor } from "./helpers.js"; + +import { AdminService } from "@zitadel/proto/zitadel/admin_pb.js"; +import { AuthService } from "@zitadel/proto/zitadel/auth_pb.js"; +import { ManagementService } from "@zitadel/proto/zitadel/management_pb.js"; +import { SystemService } from "@zitadel/proto/zitadel/system_pb.js"; + +export const createAdminServiceClient = createClientFor(AdminService); +export const createAuthServiceClient = createClientFor(AuthService); +export const createManagementServiceClient = createClientFor(ManagementService); +export const createSystemServiceClient = createClientFor(SystemService); diff --git a/packages/zitadel-client/src/v2.ts b/packages/zitadel-client/src/v2.ts new file mode 100644 index 0000000000..49cf901734 --- /dev/null +++ b/packages/zitadel-client/src/v2.ts @@ -0,0 +1,27 @@ +import { create } from "@bufbuild/protobuf"; +import { FeatureService } from "@zitadel/proto/zitadel/feature/v2/feature_service_pb.js"; +import { IdentityProviderService } from "@zitadel/proto/zitadel/idp/v2/idp_service_pb.js"; +import { RequestContextSchema } from "@zitadel/proto/zitadel/object/v2/object_pb.js"; +import { OIDCService } from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb.js"; +import { OrganizationService } from "@zitadel/proto/zitadel/org/v2/org_service_pb.js"; +import { SAMLService } from "@zitadel/proto/zitadel/saml/v2/saml_service_pb.js"; +import { SessionService } from "@zitadel/proto/zitadel/session/v2/session_service_pb.js"; +import { SettingsService } from "@zitadel/proto/zitadel/settings/v2/settings_service_pb.js"; +import { UserService } from "@zitadel/proto/zitadel/user/v2/user_service_pb.js"; + +import { createClientFor } from "./helpers.js"; + +export const createUserServiceClient = createClientFor(UserService); +export const createSettingsServiceClient = createClientFor(SettingsService); +export const createSessionServiceClient = createClientFor(SessionService); +export const createOIDCServiceClient = createClientFor(OIDCService); +export const createSAMLServiceClient = createClientFor(SAMLService); +export const createOrganizationServiceClient = createClientFor(OrganizationService); +export const createFeatureServiceClient = createClientFor(FeatureService); +export const createIdpServiceClient = createClientFor(IdentityProviderService); + +export function makeReqCtx(orgId: string | undefined) { + return create(RequestContextSchema, { + resourceOwner: orgId ? { case: "orgId", value: orgId } : { case: "instance", value: true }, + }); +} diff --git a/packages/zitadel-client/src/v3alpha.ts b/packages/zitadel-client/src/v3alpha.ts new file mode 100644 index 0000000000..a5cc533ade --- /dev/null +++ b/packages/zitadel-client/src/v3alpha.ts @@ -0,0 +1,6 @@ +import { ZITADELUsers } from "@zitadel/proto/zitadel/resources/user/v3alpha/user_service_pb.js"; +import { ZITADELUserSchemas } from "@zitadel/proto/zitadel/resources/userschema/v3alpha/user_schema_service_pb.js"; +import { createClientFor } from "./helpers.js"; + +export const createUserSchemaServiceClient = createClientFor(ZITADELUserSchemas); +export const createUserServiceClient = createClientFor(ZITADELUsers); diff --git a/packages/zitadel-client/src/web.ts b/packages/zitadel-client/src/web.ts new file mode 100644 index 0000000000..26c40da0fe --- /dev/null +++ b/packages/zitadel-client/src/web.ts @@ -0,0 +1,15 @@ +import { GrpcTransportOptions } from "@connectrpc/connect-node"; +import { createGrpcWebTransport } from "@connectrpc/connect-web"; +import { NewAuthorizationBearerInterceptor } from "./interceptors.js"; + +/** + * Create a client transport using grpc web with the given token and configuration options. + * @param token + * @param opts + */ +export function createClientTransport(token: string, opts: GrpcTransportOptions) { + return createGrpcWebTransport({ + ...opts, + interceptors: [...(opts.interceptors || []), NewAuthorizationBearerInterceptor(token)], + }); +} diff --git a/packages/zitadel-client/tsconfig.json b/packages/zitadel-client/tsconfig.json new file mode 100644 index 0000000000..58f445d906 --- /dev/null +++ b/packages/zitadel-client/tsconfig.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "composite": false, + "declaration": true, + "declarationMap": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "inlineSources": false, + "isolatedModules": true, + "moduleResolution": "node16", + "noUnusedLocals": false, + "noUnusedParameters": false, + "preserveWatchOutput": true, + "skipLibCheck": true, + "strict": true, + "lib": ["es2023"], + "module": "node16", + "target": "es2022" + }, + "include": ["./src/**/*"], + "exclude": ["dist", "build", "node_modules"] +} diff --git a/packages/zitadel-client/tsup.config.ts b/packages/zitadel-client/tsup.config.ts new file mode 100644 index 0000000000..3c9eeb8b83 --- /dev/null +++ b/packages/zitadel-client/tsup.config.ts @@ -0,0 +1,13 @@ +import { defineConfig, Options } from "tsup"; + +export default defineConfig((options: Options) => ({ + entry: ["src/index.ts", "src/v1.ts", "src/v2.ts", "src/v3alpha.ts", "src/node.ts", "src/web.ts"], + format: ["esm", "cjs"], + treeshake: false, + splitting: true, + dts: true, + minify: false, + clean: true, + sourcemap: true, + ...options, +})); diff --git a/packages/zitadel-client/turbo.json b/packages/zitadel-client/turbo.json new file mode 100644 index 0000000000..9085c5194e --- /dev/null +++ b/packages/zitadel-client/turbo.json @@ -0,0 +1,9 @@ +{ + "extends": ["//"], + "tasks": { + "build": { + "outputs": ["dist/**"], + "dependsOn": ["@zitadel/proto#generate"] + } + } +} diff --git a/packages/zitadel-client/v1/package.json b/packages/zitadel-client/v1/package.json new file mode 100644 index 0000000000..653354b919 --- /dev/null +++ b/packages/zitadel-client/v1/package.json @@ -0,0 +1,6 @@ +{ + "name": "@zitadel/client-v1", + "main": "../dist/v1.js", + "types": "../dist/v1.d.ts", + "type": "module" +} diff --git a/packages/zitadel-client/v2/package.json b/packages/zitadel-client/v2/package.json new file mode 100644 index 0000000000..6466dd136c --- /dev/null +++ b/packages/zitadel-client/v2/package.json @@ -0,0 +1,6 @@ +{ + "name": "@zitadel/client-v2", + "main": "../dist/v2.js", + "types": "../dist/v2.d.ts", + "type": "module" +} diff --git a/packages/zitadel-client/v3alpha/package.json b/packages/zitadel-client/v3alpha/package.json new file mode 100644 index 0000000000..27bf54f799 --- /dev/null +++ b/packages/zitadel-client/v3alpha/package.json @@ -0,0 +1,6 @@ +{ + "name": "@zitadel/client-v3alpha", + "main": "../dist/v3alpha.js", + "types": "../dist/v3alpha.d.ts", + "type": "module" +} diff --git a/packages/zitadel-client/web/package.json b/packages/zitadel-client/web/package.json new file mode 100644 index 0000000000..fd55cc9c5c --- /dev/null +++ b/packages/zitadel-client/web/package.json @@ -0,0 +1,6 @@ +{ + "name": "@zitadel/client-web", + "main": "../dist/web.js", + "types": "../dist/web.d.ts", + "type": "module" +} diff --git a/packages/zitadel-proto/.gitignore b/packages/zitadel-proto/.gitignore new file mode 100644 index 0000000000..0a725b8969 --- /dev/null +++ b/packages/zitadel-proto/.gitignore @@ -0,0 +1,8 @@ +zitadel +google +protoc-gen-openapiv2 +validate +node_modules +cjs +es +types diff --git a/packages/zitadel-proto/.npmignore b/packages/zitadel-proto/.npmignore new file mode 100644 index 0000000000..f422a6b429 --- /dev/null +++ b/packages/zitadel-proto/.npmignore @@ -0,0 +1,6 @@ +node_modules +.turbo +*.log +.DS_Store +buf.gen.yaml +turbo.json diff --git a/packages/zitadel-proto/CHANGELOG.md b/packages/zitadel-proto/CHANGELOG.md new file mode 100644 index 0000000000..c3964e2b29 --- /dev/null +++ b/packages/zitadel-proto/CHANGELOG.md @@ -0,0 +1,47 @@ +# @zitadel/proto + +## 1.2.0 + +### Minor Changes + +- 62ad388: revert CJS support + +## 1.1.0 + +### Minor Changes + +- 9692297: add CJS and ESM support + +## 1.0.4 + +### Patch Changes + +- 97b0332: bind @zitadel/proto version to zitadel tag + +## 1.0.3 + +### Patch Changes + +- 90fbdd1: use node16/nodenext module resolution + +## 1.0.2 + +### Patch Changes + +- include validate, google and protoc-gen-openapiv2 + +## 1.0.1 + +### Patch Changes + +- README updates + +## 1.0.0 + +### Major Changes + +- 32e1199: Initial Release + +### Minor Changes + +- f32ab7f: Initial release diff --git a/packages/zitadel-proto/README.md b/packages/zitadel-proto/README.md new file mode 100644 index 0000000000..bf8a064c12 --- /dev/null +++ b/packages/zitadel-proto/README.md @@ -0,0 +1,35 @@ +# ZITADEL Proto + +This package provides the Protocol Buffers (proto) definitions used by ZITADEL projects. It includes the proto files and generated code for interacting with ZITADEL's gRPC APIs. + +## Installation + +To install the package, use npm or yarn: + +```sh +npm install @zitadel/proto +``` + +or + +```sh +yarn add @zitadel/proto +``` + +## Usage + +To use the proto definitions in your project, import the generated code: + +```ts +import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb"; + +const org: Organization | null = await getDefaultOrg(); +``` + +## Documentation + +For detailed documentation and API references, please visit the [ZITADEL documentation](https://zitadel.com/docs). + +## Contributing + +Contributions are welcome! Please read the contributing guidelines before getting started. diff --git a/packages/zitadel-proto/buf.gen.yaml b/packages/zitadel-proto/buf.gen.yaml new file mode 100644 index 0000000000..9601096d5b --- /dev/null +++ b/packages/zitadel-proto/buf.gen.yaml @@ -0,0 +1,26 @@ +version: v2 +managed: + enabled: true +plugins: + - remote: buf.build/bufbuild/es:v2.2.0 + out: es + include_imports: true + opt: + - target=js + - json_types=true + - import_extension=js + - remote: buf.build/bufbuild/es:v2.2.0 + out: cjs + include_imports: true + opt: + - target=js + - json_types=true + - import_extension=js + - js_import_style=legacy_commonjs + - remote: buf.build/bufbuild/es:v2.2.0 + out: types + include_imports: true + opt: + - target=dts + - json_types=true + - import_extension=js diff --git a/packages/zitadel-proto/package.json b/packages/zitadel-proto/package.json new file mode 100644 index 0000000000..7119edb8de --- /dev/null +++ b/packages/zitadel-proto/package.json @@ -0,0 +1,90 @@ +{ + "name": "@zitadel/proto", + "version": "1.3.1", + "license": "MIT", + "publishConfig": { + "access": "public" + }, + "type": "module", + "main": "./cjs/index.js", + "types": "./types/index.d.ts", + "files": [ + "cjs/**", + "es/**", + "types/**", + "zitadel/**", + "google/**", + "validate/**", + "protoc-gen-openapiv2/**" + ], + "exports": { + "./zitadel/*": { + "types": "./types/zitadel/*.d.ts", + "import": "./es/zitadel/*.js", + "require": "./cjs/zitadel/*.js" + }, + "./zitadel/*.js": { + "types": "./types/zitadel/*.d.ts", + "import": "./es/zitadel/*.js", + "require": "./cjs/zitadel/*.js" + }, + "./validate/*": { + "types": "./types/validate/*.d.ts", + "import": "./es/validate/*.js", + "require": "./cjs/validate/*.js" + }, + "./validate/*.js": { + "types": "./types/validate/*.d.ts", + "import": "./es/validate/*.js", + "require": "./cjs/validate/*.js" + }, + "./google/*": { + "types": "./types/google/*.d.ts", + "import": "./es/google/*.js", + "require": "./cjs/google/*.js" + }, + "./google/*.js": { + "types": "./types/google/*.d.ts", + "import": "./es/google/*.js", + "require": "./cjs/google/*.js" + }, + "./protoc-gen-openapiv2/*": { + "types": "./types/protoc-gen-openapiv2/*.d.ts", + "import": "./es/protoc-gen-openapiv2/*.js", + "require": "./cjs/protoc-gen-openapiv2/*.js" + }, + "./protoc-gen-openapiv2/*.js": { + "types": "./types/protoc-gen-openapiv2/*.d.ts", + "import": "./es/protoc-gen-openapiv2/*.js", + "require": "./cjs/protoc-gen-openapiv2/*.js" + } + }, + "typesVersions": { + "*": { + "zitadel/*": [ + "./types/zitadel/*" + ], + "validate/*": [ + "./types/validate/*" + ], + "google/*": [ + "./types/google/*" + ], + "protoc-gen-openapiv2/*": [ + "./types/protoc-gen-openapiv2/*" + ] + } + }, + "sideEffects": false, + "scripts": { + "generate": "pnpm exec buf generate ../../proto", + "clean": "rm -rf zitadel .turbo node_modules google protoc-gen-openapiv2 validate cjs types es" + }, + "dependencies": { + "@bufbuild/protobuf": "^2.6.1" + }, + "devDependencies": { + "@bufbuild/buf": "^1.55.1", + "glob": "^11.0.0" + } +} diff --git a/packages/zitadel-proto/turbo.json b/packages/zitadel-proto/turbo.json new file mode 100644 index 0000000000..76e7fc47ad --- /dev/null +++ b/packages/zitadel-proto/turbo.json @@ -0,0 +1,16 @@ +{ + "extends": ["//"], + "tasks": { + "generate": { + "outputs": [ + "zitadel/**", + "google/**", + "validate/**", + "protoc-gen-openapiv2/**", + "cjs/**", + "es/**", + "types/**" + ] + } + } +} diff --git a/pkg/grpc/app/v2beta/application.go b/pkg/grpc/app/v2beta/application.go new file mode 100644 index 0000000000..5cef08db46 --- /dev/null +++ b/pkg/grpc/app/v2beta/application.go @@ -0,0 +1,5 @@ +package app + +type ApplicationConfig = isApplication_Config + +type MetaType = isUpdateSAMLApplicationConfigurationRequest_Metadata diff --git a/pkg/grpc/internal_permission/v2beta/resource.go b/pkg/grpc/internal_permission/v2beta/resource.go new file mode 100644 index 0000000000..a57f60242e --- /dev/null +++ b/pkg/grpc/internal_permission/v2beta/resource.go @@ -0,0 +1,3 @@ +package internal_permission + +type Resource = isAdministrator_Resource diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000000..79a52ce530 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,32829 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +overrides: + '@typescript-eslint/parser': ^8.35.1 + '@zitadel/client': workspace:* + '@zitadel/proto': workspace:* + +importers: + + .: + devDependencies: + '@changesets/cli': + specifier: ^2.29.5 + version: 2.29.5 + '@devcontainers/cli': + specifier: ^0.80.0 + version: 0.80.0 + turbo: + specifier: 2.5.5 + version: 2.5.5 + + apps/login: + dependencies: + '@headlessui/react': + specifier: ^2.1.9 + version: 2.2.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@heroicons/react': + specifier: 2.1.3 + version: 2.1.3(react@19.1.0) + '@radix-ui/react-tooltip': + specifier: ^1.2.7 + version: 1.2.7(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@tailwindcss/forms': + specifier: 0.5.7 + version: 0.5.7(tailwindcss@3.4.14) + '@vercel/analytics': + specifier: ^1.2.2 + version: 1.5.0(next@15.4.0-canary.86(@babel/core@7.28.0)(@playwright/test@1.54.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.2))(react@19.1.0) + '@zitadel/client': + specifier: workspace:* + version: link:../../packages/zitadel-client + '@zitadel/proto': + specifier: workspace:* + version: link:../../packages/zitadel-proto + clsx: + specifier: 1.2.1 + version: 1.2.1 + copy-to-clipboard: + specifier: ^3.3.3 + version: 3.3.3 + deepmerge: + specifier: ^4.3.1 + version: 4.3.1 + lucide-react: + specifier: 0.469.0 + version: 0.469.0(react@19.1.0) + moment: + specifier: ^2.29.4 + version: 2.30.1 + next: + specifier: 15.4.0-canary.86 + version: 15.4.0-canary.86(@babel/core@7.28.0)(@playwright/test@1.54.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.2) + next-intl: + specifier: ^3.25.1 + version: 3.26.5(next@15.4.0-canary.86(@babel/core@7.28.0)(@playwright/test@1.54.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.2))(react@19.1.0) + next-themes: + specifier: ^0.2.1 + version: 0.2.1(next@15.4.0-canary.86(@babel/core@7.28.0)(@playwright/test@1.54.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.2))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + nice-grpc: + specifier: 2.0.1 + version: 2.0.1 + qrcode.react: + specifier: ^3.1.0 + version: 3.2.0(react@19.1.0) + react: + specifier: 19.1.0 + version: 19.1.0 + react-dom: + specifier: 19.1.0 + version: 19.1.0(react@19.1.0) + react-hook-form: + specifier: 7.39.5 + version: 7.39.5(react@19.1.0) + tinycolor2: + specifier: 1.4.2 + version: 1.4.2 + uuid: + specifier: ^11.1.0 + version: 11.1.0 + devDependencies: + '@babel/eslint-parser': + specifier: ^7.23.0 + version: 7.28.0(@babel/core@7.28.0)(eslint@8.57.1) + '@bufbuild/buf': + specifier: ^1.53.0 + version: 1.55.1 + '@faker-js/faker': + specifier: ^9.7.0 + version: 9.9.0 + '@otplib/core': + specifier: ^12.0.0 + version: 12.0.1 + '@otplib/plugin-crypto': + specifier: ^12.0.0 + version: 12.0.1 + '@otplib/plugin-thirty-two': + specifier: ^12.0.0 + version: 12.0.1 + '@playwright/test': + specifier: ^1.52.0 + version: 1.54.1 + '@testing-library/jest-dom': + specifier: ^6.6.3 + version: 6.6.3 + '@testing-library/react': + specifier: ^16.3.0 + version: 16.3.0(@testing-library/dom@10.4.0)(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@types/ms': + specifier: 2.1.0 + version: 2.1.0 + '@types/node': + specifier: ^22.14.1 + version: 22.16.5 + '@types/react': + specifier: 19.1.2 + version: 19.1.2 + '@types/react-dom': + specifier: 19.1.2 + version: 19.1.2(@types/react@19.1.2) + '@types/tinycolor2': + specifier: 1.4.3 + version: 1.4.3 + '@types/uuid': + specifier: ^10.0.0 + version: 10.0.0 + '@typescript-eslint/eslint-plugin': + specifier: ^7.0.0 + version: 7.18.0(@typescript-eslint/parser@8.38.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(typescript@5.8.3) + '@typescript-eslint/parser': + specifier: ^8.35.1 + version: 8.38.0(eslint@8.57.1)(typescript@5.8.3) + '@vercel/git-hooks': + specifier: 1.0.0 + version: 1.0.0 + '@vitejs/plugin-react': + specifier: ^4.4.1 + version: 4.7.0(vite@5.4.19(@types/node@22.16.5)(less@4.1.3)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)) + autoprefixer: + specifier: 10.4.21 + version: 10.4.21(postcss@8.5.3) + concurrently: + specifier: ^9.1.2 + version: 9.2.0 + cypress: + specifier: ^14.5.2 + version: 14.5.2 + dotenv-cli: + specifier: ^8.0.0 + version: 8.0.0 + env-cmd: + specifier: ^10.0.0 + version: 10.1.0 + eslint: + specifier: ^8.57.0 + version: 8.57.1 + eslint-config-next: + specifier: 15.4.0-canary.86 + version: 15.4.0-canary.86(eslint@8.57.1)(typescript@5.8.3) + eslint-config-prettier: + specifier: ^9.1.0 + version: 9.1.2(eslint@8.57.1) + gaxios: + specifier: ^7.1.0 + version: 7.1.1 + grpc-tools: + specifier: 1.13.0 + version: 1.13.0(encoding@0.1.13) + jsdom: + specifier: ^26.1.0 + version: 26.1.0 + lint-staged: + specifier: 15.5.1 + version: 15.5.1 + make-dir-cli: + specifier: 4.0.0 + version: 4.0.0 + nodemon: + specifier: ^3.1.9 + version: 3.1.10 + postcss: + specifier: 8.5.3 + version: 8.5.3 + prettier: + specifier: ^3.2.5 + version: 3.6.2 + prettier-plugin-organize-imports: + specifier: ^3.2.0 + version: 3.2.4(prettier@3.6.2)(typescript@5.8.3) + prettier-plugin-tailwindcss: + specifier: 0.6.11 + version: 0.6.11(prettier-plugin-organize-imports@3.2.4(prettier@3.6.2)(typescript@5.8.3))(prettier@3.6.2) + sass: + specifier: ^1.87.0 + version: 1.89.2 + start-server-and-test: + specifier: ^2.0.11 + version: 2.0.12 + tailwindcss: + specifier: 3.4.14 + version: 3.4.14 + ts-proto: + specifier: ^2.7.0 + version: 2.7.5 + typescript: + specifier: ^5.8.3 + version: 5.8.3 + vite-tsconfig-paths: + specifier: ^5.1.4 + version: 5.1.4(typescript@5.8.3)(vite@5.4.19(@types/node@22.16.5)(less@4.1.3)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)) + vitest: + specifier: ^2.0.0 + version: 2.1.9(@types/node@22.16.5)(jsdom@26.1.0)(less@4.1.3)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1) + + console: + dependencies: + '@angular/animations': + specifier: ^16.2.12 + version: 16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)) + '@angular/cdk': + specifier: ^16.2.14 + version: 16.2.14(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2) + '@angular/common': + specifier: ^16.2.12 + version: 16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2) + '@angular/compiler': + specifier: ^16.2.12 + version: 16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)) + '@angular/core': + specifier: ^16.2.12 + version: 16.2.12(rxjs@7.8.2)(zone.js@0.13.3) + '@angular/forms': + specifier: ^16.2.12 + version: 16.2.12(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(@angular/platform-browser@16.2.12(@angular/animations@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(rxjs@7.8.2) + '@angular/material': + specifier: ^16.2.14 + version: 16.2.14(@angular/animations@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(@angular/cdk@16.2.14(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(@angular/forms@16.2.12(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(@angular/platform-browser@16.2.12(@angular/animations@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(rxjs@7.8.2))(@angular/platform-browser@16.2.12(@angular/animations@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(rxjs@7.8.2) + '@angular/material-moment-adapter': + specifier: ^16.2.14 + version: 16.2.14(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(@angular/material@16.2.14(@angular/animations@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(@angular/cdk@16.2.14(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(@angular/forms@16.2.12(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(@angular/platform-browser@16.2.12(@angular/animations@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(rxjs@7.8.2))(@angular/platform-browser@16.2.12(@angular/animations@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(rxjs@7.8.2))(moment@2.30.1) + '@angular/platform-browser': + specifier: ^16.2.12 + version: 16.2.12(@angular/animations@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)) + '@angular/platform-browser-dynamic': + specifier: ^16.2.12 + version: 16.2.12(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/compiler@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(@angular/platform-browser@16.2.12(@angular/animations@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))) + '@angular/router': + specifier: ^16.2.12 + version: 16.2.12(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(@angular/platform-browser@16.2.12(@angular/animations@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(rxjs@7.8.2) + '@angular/service-worker': + specifier: ^16.2.12 + version: 16.2.12(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)) + '@bufbuild/protobuf': + specifier: ^2.6.1 + version: 2.6.1 + '@connectrpc/connect': + specifier: ^2.0.0 + version: 2.0.2(@bufbuild/protobuf@2.6.1) + '@connectrpc/connect-web': + specifier: ^2.0.0 + version: 2.0.2(@bufbuild/protobuf@2.6.1)(@connectrpc/connect@2.0.2(@bufbuild/protobuf@2.6.1)) + '@ctrl/ngx-codemirror': + specifier: ^6.1.0 + version: 6.1.0(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(@angular/forms@16.2.12(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(@angular/platform-browser@16.2.12(@angular/animations@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(rxjs@7.8.2))(codemirror@5.65.19) + '@fortawesome/angular-fontawesome': + specifier: ^0.13.0 + version: 0.13.0(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(@fortawesome/fontawesome-svg-core@6.7.2) + '@fortawesome/fontawesome-svg-core': + specifier: ^6.7.2 + version: 6.7.2 + '@fortawesome/free-brands-svg-icons': + specifier: ^6.7.2 + version: 6.7.2 + '@ngx-translate/core': + specifier: ^15.0.0 + version: 15.0.0(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2) + '@zitadel/client': + specifier: workspace:* + version: link:../packages/zitadel-client + '@zitadel/proto': + specifier: workspace:* + version: link:../packages/zitadel-proto + angular-oauth2-oidc: + specifier: ^15.0.1 + version: 15.0.1(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)) + angularx-qrcode: + specifier: ^16.0.2 + version: 16.0.2(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)) + buffer: + specifier: ^6.0.3 + version: 6.0.3 + codemirror: + specifier: ^5.65.19 + version: 5.65.19 + file-saver: + specifier: ^2.0.5 + version: 2.0.5 + flag-icons: + specifier: ^7.3.2 + version: 7.5.0 + google-protobuf: + specifier: ^3.21.4 + version: 3.21.4 + grpc-web: + specifier: ^1.5.0 + version: 1.5.0 + i18n-iso-countries: + specifier: ^7.14.0 + version: 7.14.0 + libphonenumber-js: + specifier: ^1.12.6 + version: 1.12.10 + material-design-icons-iconfont: + specifier: ^6.7.0 + version: 6.7.0 + moment: + specifier: ^2.30.1 + version: 2.30.1 + ngx-color: + specifier: ^9.0.0 + version: 9.0.0(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)) + opentype.js: + specifier: ^1.3.4 + version: 1.3.4 + posthog-js: + specifier: ^1.232.7 + version: 1.257.1 + rxjs: + specifier: ^7.8.2 + version: 7.8.2 + tinycolor2: + specifier: ^1.6.0 + version: 1.6.0 + tslib: + specifier: ^2.7.0 + version: 2.8.1 + uuid: + specifier: ^10.0.0 + version: 10.0.0 + zone.js: + specifier: ~0.13.3 + version: 0.13.3 + devDependencies: + '@angular-devkit/build-angular': + specifier: ^16.2.2 + version: 16.2.16(@angular/compiler-cli@16.2.12(@angular/compiler@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(typescript@5.1.6))(@angular/service-worker@16.2.12(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(@swc/core@1.13.1)(@types/node@22.16.5)(html-webpack-plugin@5.6.3(webpack@5.94.0(@swc/core@1.13.1)(esbuild@0.18.17)))(karma@6.4.4)(lightningcss@1.30.1)(tailwindcss@3.4.14)(typescript@5.1.6) + '@angular-eslint/builder': + specifier: 18.3.0 + version: 18.3.0(eslint@8.57.1)(typescript@5.1.6) + '@angular-eslint/eslint-plugin': + specifier: 18.0.0 + version: 18.0.0(eslint@8.57.1)(typescript@5.1.6) + '@angular-eslint/eslint-plugin-template': + specifier: 18.0.0 + version: 18.0.0(eslint@8.57.1)(typescript@5.1.6) + '@angular-eslint/schematics': + specifier: 16.2.0 + version: 16.2.0(@angular/cli@16.2.16(chokidar@3.5.3))(@swc/core@1.13.1)(eslint@8.57.1)(typescript@5.1.6) + '@angular-eslint/template-parser': + specifier: 18.3.0 + version: 18.3.0(eslint@8.57.1)(typescript@5.1.6) + '@angular/cli': + specifier: ^16.2.15 + version: 16.2.16(chokidar@3.5.3) + '@angular/compiler-cli': + specifier: ^16.2.5 + version: 16.2.12(@angular/compiler@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(typescript@5.1.6) + '@angular/language-service': + specifier: ^18.2.4 + version: 18.2.13 + '@bufbuild/buf': + specifier: ^1.55.1 + version: 1.55.1 + '@netlify/framework-info': + specifier: ^9.8.13 + version: 9.9.3 + '@types/file-saver': + specifier: ^2.0.7 + version: 2.0.7 + '@types/google-protobuf': + specifier: ^3.15.3 + version: 3.15.12 + '@types/jasmine': + specifier: ~5.1.4 + version: 5.1.8 + '@types/jasminewd2': + specifier: ~2.0.13 + version: 2.0.13 + '@types/jsonwebtoken': + specifier: ^9.0.6 + version: 9.0.10 + '@types/node': + specifier: ^22.5.5 + version: 22.16.5 + '@types/opentype.js': + specifier: ^1.3.8 + version: 1.3.8 + '@types/qrcode': + specifier: ^1.5.2 + version: 1.5.5 + '@types/uuid': + specifier: ^10.0.0 + version: 10.0.0 + '@typescript-eslint/eslint-plugin': + specifier: ^5.62.0 + version: 5.62.0(@typescript-eslint/parser@8.38.0(eslint@8.57.1)(typescript@5.1.6))(eslint@8.57.1)(typescript@5.1.6) + '@typescript-eslint/parser': + specifier: ^8.35.1 + version: 8.38.0(eslint@8.57.1)(typescript@5.1.6) + eslint: + specifier: ^8.57.1 + version: 8.57.1 + jasmine-core: + specifier: ~5.6.0 + version: 5.6.0 + jasmine-spec-reporter: + specifier: ~7.0.0 + version: 7.0.0 + karma: + specifier: ^6.4.4 + version: 6.4.4 + karma-chrome-launcher: + specifier: ^3.2.0 + version: 3.2.0 + karma-coverage: + specifier: ^2.2.1 + version: 2.2.1 + karma-coverage-istanbul-reporter: + specifier: ^3.0.3 + version: 3.0.3 + karma-jasmine: + specifier: ^5.1.0 + version: 5.1.0(karma@6.4.4) + karma-jasmine-html-reporter: + specifier: ^2.1.0 + version: 2.1.0(jasmine-core@5.6.0)(karma-jasmine@5.1.0(karma@6.4.4))(karma@6.4.4) + prettier: + specifier: ^3.5.3 + version: 3.6.2 + prettier-plugin-organize-imports: + specifier: ^4.1.0 + version: 4.2.0(prettier@3.6.2)(typescript@5.1.6) + typescript: + specifier: '5.1' + version: 5.1.6 + + docs: + dependencies: + '@bufbuild/buf': + specifier: ^1.14.0 + version: 1.55.1 + '@docusaurus/core': + specifier: ^3.8.1 + version: 3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3) + '@docusaurus/faster': + specifier: ^3.8.1 + version: 3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17) + '@docusaurus/preset-classic': + specifier: ^3.8.1 + version: 3.8.1(@algolia/client-search@5.34.0)(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(@types/react@19.1.2)(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@5.8.3) + '@docusaurus/theme-mermaid': + specifier: ^3.8.1 + version: 3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@docusaurus/plugin-content-docs@3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3) + '@docusaurus/theme-search-algolia': + specifier: ^3.8.1 + version: 3.8.1(@algolia/client-search@5.34.0)(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(@types/react@19.1.2)(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@5.8.3) + '@headlessui/react': + specifier: ^1.7.4 + version: 1.7.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@heroicons/react': + specifier: ^2.0.13 + version: 2.1.3(react@18.3.1) + '@inkeep/cxkit-docusaurus': + specifier: ^0.5.89 + version: 0.5.95(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(zod@3.25.76) + '@signalwire/docusaurus-plugin-llms-txt': + specifier: ^1.2.0 + version: 1.2.1(@docusaurus/core@3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3)) + autoprefixer: + specifier: ^10.4.13 + version: 10.4.21(postcss@8.5.3) + clsx: + specifier: ^1.2.1 + version: 1.2.1 + docusaurus-plugin-image-zoom: + specifier: ^3.0.1 + version: 3.0.1(@docusaurus/theme-classic@3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(@types/react@19.1.2)(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3)) + docusaurus-plugin-openapi-docs: + specifier: 4.4.0 + version: 4.4.0(@docusaurus/plugin-content-docs@3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3))(@docusaurus/utils-validation@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@docusaurus/utils@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(encoding@0.1.13)(react@18.3.1) + docusaurus-theme-github-codeblock: + specifier: ^2.0.2 + version: 2.0.2(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + docusaurus-theme-openapi-docs: + specifier: 4.4.0 + version: 4.4.0(@docusaurus/theme-common@3.8.1(@docusaurus/plugin-content-docs@3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@types/react@19.1.2)(docusaurus-plugin-openapi-docs@4.4.0(@docusaurus/plugin-content-docs@3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3))(@docusaurus/utils-validation@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@docusaurus/utils@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(encoding@0.1.13)(react@18.3.1))(docusaurus-plugin-sass@0.2.6(@docusaurus/core@3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(sass@1.89.2)(webpack@5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17))))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(webpack@5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17))) + mdx-mermaid: + specifier: ^2.0.0 + version: 2.0.3(mermaid@11.9.0)(react@18.3.1)(typescript@5.8.3)(unist-util-visit@5.0.0) + postcss: + specifier: ^8.4.31 + version: 8.5.3 + raw-loader: + specifier: ^4.0.2 + version: 4.0.2(webpack@5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17))) + react: + specifier: ^18.2.0 + version: 18.3.1 + react-copy-to-clipboard: + specifier: ^5.1.0 + version: 5.1.0(react@18.3.1) + react-dom: + specifier: ^18.2.0 + version: 18.3.1(react@18.3.1) + react-google-charts: + specifier: ^5.2.1 + version: 5.2.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-player: + specifier: ^2.15.1 + version: 2.16.1(react@18.3.1) + devDependencies: + '@docusaurus/module-type-aliases': + specifier: ^3.8.1 + version: 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/types': + specifier: ^3.8.1 + version: 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + tailwindcss: + specifier: ^3.2.4 + version: 3.4.14 + + e2e: + dependencies: + '@types/pg': + specifier: ^8.11.6 + version: 8.15.4 + cypress-wait-until: + specifier: ^3.0.2 + version: 3.0.2 + jsonwebtoken: + specifier: ^9.0.2 + version: 9.0.2 + mochawesome: + specifier: ^7.1.3 + version: 7.1.3(mocha@11.7.1) + pg: + specifier: ^8.12.0 + version: 8.16.3 + prettier: + specifier: ^3.3.3 + version: 3.6.2 + typescript: + specifier: ^5.5.4 + version: 5.8.3 + uuid: + specifier: ^10.0.0 + version: 10.0.0 + wait-on: + specifier: ^7.2.0 + version: 7.2.0 + devDependencies: + '@types/node': + specifier: ^22.3.0 + version: 22.16.5 + cypress: + specifier: ^14.5.3 + version: 14.5.3 + + packages/zitadel-client: + dependencies: + '@bufbuild/protobuf': + specifier: ^2.2.2 + version: 2.6.1 + '@connectrpc/connect': + specifier: ^2.0.0 + version: 2.0.2(@bufbuild/protobuf@2.6.1) + '@connectrpc/connect-node': + specifier: ^2.0.0 + version: 2.0.2(@bufbuild/protobuf@2.6.1)(@connectrpc/connect@2.0.2(@bufbuild/protobuf@2.6.1)) + '@connectrpc/connect-web': + specifier: ^2.0.0 + version: 2.0.2(@bufbuild/protobuf@2.6.1)(@connectrpc/connect@2.0.2(@bufbuild/protobuf@2.6.1)) + '@zitadel/proto': + specifier: workspace:* + version: link:../zitadel-proto + jose: + specifier: ^5.3.0 + version: 5.10.0 + devDependencies: + '@bufbuild/buf': + specifier: ^1.53.0 + version: 1.55.1 + '@bufbuild/protocompile': + specifier: ^0.0.1 + version: 0.0.1(@bufbuild/buf@1.55.1) + '@types/node': + specifier: ^24.0.14 + version: 24.1.0 + '@typescript-eslint/eslint-plugin': + specifier: ^8.15.0 + version: 8.38.0(@typescript-eslint/parser@8.38.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(typescript@5.8.3) + '@typescript-eslint/parser': + specifier: ^8.35.1 + version: 8.38.0(eslint@8.57.1)(typescript@5.8.3) + eslint: + specifier: ^8.57.0 + version: 8.57.1 + knip: + specifier: ^5.61.3 + version: 5.62.0(@types/node@24.1.0)(typescript@5.8.3) + tsup: + specifier: ^8.4.0 + version: 8.5.0(@swc/core@1.13.1)(jiti@2.4.2)(postcss@8.5.6)(typescript@5.8.3)(yaml@2.8.0) + typescript: + specifier: ^5.8.3 + version: 5.8.3 + vitest: + specifier: ^2.0.0 + version: 2.1.9(@types/node@24.1.0)(jsdom@26.1.0)(less@4.1.3)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1) + + packages/zitadel-proto: + dependencies: + '@bufbuild/protobuf': + specifier: ^2.6.1 + version: 2.6.1 + devDependencies: + '@bufbuild/buf': + specifier: ^1.55.1 + version: 1.55.1 + glob: + specifier: ^11.0.0 + version: 11.0.3 + +packages: + + '@adobe/css-tools@4.4.3': + resolution: {integrity: sha512-VQKMkwriZbaOgVCby1UDY/LDk5fIjhQicCvVPFqfe+69fWaPWydbWJ3wRt59/YzIwda1I81loas3oCoHxnqvdA==} + + '@algolia/autocomplete-core@1.17.9': + resolution: {integrity: sha512-O7BxrpLDPJWWHv/DLA9DRFWs+iY1uOJZkqUwjS5HSZAGcl0hIVCQ97LTLewiZmZ402JYUrun+8NqFP+hCknlbQ==} + + '@algolia/autocomplete-plugin-algolia-insights@1.17.9': + resolution: {integrity: sha512-u1fEHkCbWF92DBeB/KHeMacsjsoI0wFhjZtlCq2ddZbAehshbZST6Hs0Avkc0s+4UyBGbMDnSuXHLuvRWK5iDQ==} + peerDependencies: + search-insights: '>= 1 < 3' + + '@algolia/autocomplete-preset-algolia@1.17.9': + resolution: {integrity: sha512-Na1OuceSJeg8j7ZWn5ssMu/Ax3amtOwk76u4h5J4eK2Nx2KB5qt0Z4cOapCsxot9VcEN11ADV5aUSlQF4RhGjQ==} + peerDependencies: + '@algolia/client-search': '>= 4.9.1 < 6' + algoliasearch: '>= 4.9.1 < 6' + + '@algolia/autocomplete-shared@1.17.9': + resolution: {integrity: sha512-iDf05JDQ7I0b7JEA/9IektxN/80a2MZ1ToohfmNS3rfeuQnIKI3IJlIafD0xu4StbtQTghx9T3Maa97ytkXenQ==} + peerDependencies: + '@algolia/client-search': '>= 4.9.1 < 6' + algoliasearch: '>= 4.9.1 < 6' + + '@algolia/client-abtesting@5.34.0': + resolution: {integrity: sha512-d6ardhDtQsnMpyr/rPrS3YuIE9NYpY4rftkC7Ap9tyuhZ/+V3E/LH+9uEewPguKzVqduApdwJzYq2k+vAXVEbQ==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-analytics@5.34.0': + resolution: {integrity: sha512-WXIByjHNA106JO1Dj6b4viSX/yMN3oIB4qXr2MmyEmNq0MgfuPfPw8ayLRIZPa9Dp27hvM3G8MWJ4RG978HYFw==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-common@5.34.0': + resolution: {integrity: sha512-JeN1XJLZIkkv6yK0KT93CIXXk+cDPUGNg5xeH4fN9ZykYFDWYRyqgaDo+qvg4RXC3WWkdQ+hogQuuCk4Y3Eotw==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-insights@5.34.0': + resolution: {integrity: sha512-gdFlcQa+TWXJUsihHDlreFWniKPFIQ15i5oynCY4m9K3DCex5g5cVj9VG4Hsquxf2t6Y0yv8w6MvVTGDO8oRLw==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-personalization@5.34.0': + resolution: {integrity: sha512-g91NHhIZDkh1IUeNtsUd8V/ZxuBc2ByOfDqhCkoQY3Z/mZszhpn3Czn6AR5pE81fx793vMaiOZvQVB5QttArkQ==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-query-suggestions@5.34.0': + resolution: {integrity: sha512-cvRApDfFrlJ3Vcn37U4Nd/7S6T8cx7FW3mVLJPqkkzixv8DQ/yV+x4VLirxOtGDdq3KohcIbIGWbg1QuyOZRvQ==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-search@5.34.0': + resolution: {integrity: sha512-m9tK4IqJmn+flEPRtuxuHgiHmrKV0su5fuVwVpq8/es4DMjWMgX1a7Lg1PktvO8AbKaTp9kTtBAPnwXpuCwmEg==} + engines: {node: '>= 14.0.0'} + + '@algolia/events@4.0.1': + resolution: {integrity: sha512-FQzvOCgoFXAbf5Y6mYozw2aj5KCJoA3m4heImceldzPSMbdyS4atVjJzXKMsfX3wnZTFYwkkt8/z8UesLHlSBQ==} + + '@algolia/ingestion@1.34.0': + resolution: {integrity: sha512-2rxy4XoeRtIpzxEh5u5UgDC5HY4XbNdjzNgFx1eDrfFkSHpEVjirtLhISMy2N5uSFqYu1uUby5/NC1Soq8J7iw==} + engines: {node: '>= 14.0.0'} + + '@algolia/monitoring@1.34.0': + resolution: {integrity: sha512-OJiDhlJX8ZdWAndc50Z6aUEW/YmnhFK2ul3rahMw5/c9Damh7+oY9SufoK2LimJejy+65Qka06YPG29v2G/vww==} + engines: {node: '>= 14.0.0'} + + '@algolia/recommend@5.34.0': + resolution: {integrity: sha512-fzNQZAdVxu/Gnbavy8KW5gurApwdYcPW6+pjO7Pw8V5drCR3eSqnOxSvp79rhscDX8ezwqMqqK4F3Hsq+KpRzg==} + engines: {node: '>= 14.0.0'} + + '@algolia/requester-browser-xhr@5.34.0': + resolution: {integrity: sha512-gEI0xjzA/xvMpEdYmgQnf6AQKllhgKRtnEWmwDrnct+YPIruEHlx1dd7nRJTy/33MiYcCxkB4khXpNrHuqgp3Q==} + engines: {node: '>= 14.0.0'} + + '@algolia/requester-fetch@5.34.0': + resolution: {integrity: sha512-5SwGOttpbACT4jXzfSJ3mnTcF46SVNSnZ1JjxC3qBa3qKi4U0CJGzuVVy3L798u8dG5H0SZ2MAB5v7180Gnqew==} + engines: {node: '>= 14.0.0'} + + '@algolia/requester-node-http@5.34.0': + resolution: {integrity: sha512-409XlyIyEXrxyGjWxd0q5RASizHSRVUU0AXPCEdqnbcGEzbCgL1n7oYI8YxzE/RqZLha+PNwWCcTVn7EE5tyyQ==} + engines: {node: '>= 14.0.0'} + + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + + '@ampproject/remapping@2.2.1': + resolution: {integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==} + engines: {node: '>=6.0.0'} + + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@angular-devkit/architect@0.1602.16': + resolution: {integrity: sha512-aWEeGU4UlbrSKpcAZsldVNxNXAWEeu9hM2BPk77GftbRC8PBMWpgYyrJWTz2ryn8aSmGKT3T8OyBH4gZA/667w==} + engines: {node: ^16.14.0 || >=18.10.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + + '@angular-devkit/build-angular@16.2.16': + resolution: {integrity: sha512-gEni21kza41xaRnVWP1sMuiWHS/rdoym5FEEGDo9PG60LwRC4lekIgT09GpTlmMu007UEfo0ccQnGroD6+MqWg==} + engines: {node: ^16.14.0 || >=18.10.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + peerDependencies: + '@angular/compiler-cli': ^16.0.0 + '@angular/localize': ^16.0.0 + '@angular/platform-server': ^16.0.0 + '@angular/service-worker': ^16.0.0 + jest: ^29.5.0 + jest-environment-jsdom: ^29.5.0 + karma: ^6.3.0 + ng-packagr: ^16.0.0 + protractor: ^7.0.0 + tailwindcss: ^2.0.0 || ^3.0.0 + typescript: '>=4.9.3 <5.2' + peerDependenciesMeta: + '@angular/localize': + optional: true + '@angular/platform-server': + optional: true + '@angular/service-worker': + optional: true + jest: + optional: true + jest-environment-jsdom: + optional: true + karma: + optional: true + ng-packagr: + optional: true + protractor: + optional: true + tailwindcss: + optional: true + + '@angular-devkit/build-webpack@0.1602.16': + resolution: {integrity: sha512-b99Sj0btI0C2GIfzoyP8epDMIOLqSTqXOxw6klGtBLaGZfM5KAxqFzekXh8cAnHxWCj20WdNhezS1eUTLOkaIA==} + engines: {node: ^16.14.0 || >=18.10.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + peerDependencies: + webpack: ^5.30.0 + webpack-dev-server: ^4.0.0 + + '@angular-devkit/core@16.2.16': + resolution: {integrity: sha512-5xHs9JFmp78sydrOAg0UGErxfMVv5c2f3RXoikS7eBOOXTWEi5pmnOkOvSJ3loQFGVs3Y7i+u02G3VrF5ZxOrA==} + engines: {node: ^16.14.0 || >=18.10.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + peerDependencies: + chokidar: ^3.5.2 + peerDependenciesMeta: + chokidar: + optional: true + + '@angular-devkit/schematics@16.2.16': + resolution: {integrity: sha512-pF6fdtJh6yLmgA7Gs45JIdxPl2MsTAhYcZIMrX1a6ID64dfwtF0MP8fDE6vrWInV1zXbzzf7l7PeKuqVtTSzKg==} + engines: {node: ^16.14.0 || >=18.10.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + + '@angular-eslint/builder@18.3.0': + resolution: {integrity: sha512-httEQyqyBw3+0CRtAa7muFxHrauRfkEfk/jmrh5fn2Eiu+I53hAqFPgrwVi1V6AP/kj2zbAiWhd5xM3pMJdoRQ==} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '*' + + '@angular-eslint/bundled-angular-compiler@16.2.0': + resolution: {integrity: sha512-ct9orDYxkMl2+uvM7UBfgV28Dq57V4dEs+Drh7cD673JIMa6sXbgmd0QEtm8W3cmyK/jcTzmuoufxbH7hOxd6g==} + + '@angular-eslint/bundled-angular-compiler@18.0.0': + resolution: {integrity: sha512-c5XNfpWN6vfMoZpnLLeras7nUIVI10ofJu3W3s1s1NpCjP67kY84SPYRJIND1LemVewMQ+mhnP4xJnqvJxC1tA==} + + '@angular-eslint/bundled-angular-compiler@18.3.0': + resolution: {integrity: sha512-v/59FxUKnMzymVce99gV43huxoqXWMb85aKvzlNvLN+ScDu6ZE4YMiTQNpfapVL2lkxhs0uwB3jH17EYd5TcsA==} + + '@angular-eslint/eslint-plugin-template@16.2.0': + resolution: {integrity: sha512-YFdQ6hHX6NlQj0lfogZwfyKjU8pqkJU+Zsk0ehjlXP8VfKFVmDeQT5/Xr6Df9C8pveC3hvq6Jgd8vo67S9Enxg==} + peerDependencies: + eslint: ^7.20.0 || ^8.0.0 + typescript: '*' + + '@angular-eslint/eslint-plugin-template@18.0.0': + resolution: {integrity: sha512-KN32zW5eutRLumjJNGM77pZ4dpQe/PlffU2fGGVagHSDRrjaEqBmJ/khecUHjz3+VxYLbVWBM2skfb5jC4Lr2g==} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '*' + + '@angular-eslint/eslint-plugin@16.2.0': + resolution: {integrity: sha512-zdiAIox1T+B71HL+A8m+1jWdU34nvPGLhCRw/uZNwHzknsF4tYzNQ9W7T/SC/g/2s1yT2yNosEVNJSGSFvunJg==} + peerDependencies: + eslint: ^7.20.0 || ^8.0.0 + typescript: '*' + + '@angular-eslint/eslint-plugin@18.0.0': + resolution: {integrity: sha512-XhsIR28HiFOg3qbyjr0ZFBvOeFSXowbriFn8pAuiUjYoLJEtNZzPA1Ih/J0Ky5ZXYwcSJbZRQdNR/q1INQEFqA==} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '*' + + '@angular-eslint/schematics@16.2.0': + resolution: {integrity: sha512-2JUVR7hAKx37mgWeDjvyWEMH5uSeeksYuaQT5wwlgIzgrO4BNFuqs6Rgyp2jiYa7BFMX/qHULSa+bSq5J5ceEA==} + peerDependencies: + '@angular/cli': '>= 16.0.0 < 17.0.0' + + '@angular-eslint/template-parser@18.3.0': + resolution: {integrity: sha512-1mUquqcnugI4qsoxcYZKZ6WMi6RPelDcJZg2YqGyuaIuhWmi3ZqJZLErSSpjP60+TbYZu7wM8Kchqa1bwJtEaQ==} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '*' + + '@angular-eslint/utils@16.2.0': + resolution: {integrity: sha512-NxMRwnlIgzmbJQfWkfd9y3Sz0hzjFdK5LH44i+3D5NhpPdZ6SzwHAjMYWoYsmmNQX5tlDXoicYF9Mz9Wz8DJ/A==} + peerDependencies: + eslint: ^7.20.0 || ^8.0.0 + typescript: '*' + + '@angular-eslint/utils@18.0.0': + resolution: {integrity: sha512-ygOlsC5HrknbI8Ah5pa6tGtrpxB0W4UqzZG9Ii7whoWs7OjkBTIbeNy/qaWv1e45MR2/Ytd5BSWK17w0Poyz8w==} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '*' + + '@angular/animations@16.2.12': + resolution: {integrity: sha512-MD0ElviEfAJY8qMOd6/jjSSvtqER2RDAi0lxe6EtUacC1DHCYkaPrKW4vLqY+tmZBg1yf+6n+uS77pXcHHcA3w==} + engines: {node: ^16.14.0 || >=18.10.0} + peerDependencies: + '@angular/core': 16.2.12 + + '@angular/cdk@16.2.14': + resolution: {integrity: sha512-n6PrGdiVeSTEmM/HEiwIyg6YQUUymZrb5afaNLGFRM5YL0Y8OBqd+XhCjb0OfD/AfgCUtedVEPwNqrfW8KzgGw==} + peerDependencies: + '@angular/common': ^16.0.0 || ^17.0.0 + '@angular/core': ^16.0.0 || ^17.0.0 + rxjs: ^6.5.3 || ^7.4.0 + + '@angular/cli@16.2.16': + resolution: {integrity: sha512-aqfNYZ45ndrf36i+7AhQ9R8BCm025j7TtYaUmvvjT4LwiUg6f6KtlZPB/ivBlXmd1g9oXqW4advL0AIi8A/Ozg==} + engines: {node: ^16.14.0 || >=18.10.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + hasBin: true + + '@angular/common@16.2.12': + resolution: {integrity: sha512-B+WY/cT2VgEaz9HfJitBmgdk4I333XG/ybC98CMC4Wz8E49T8yzivmmxXB3OD6qvjcOB6ftuicl6WBqLbZNg2w==} + engines: {node: ^16.14.0 || >=18.10.0} + peerDependencies: + '@angular/core': 16.2.12 + rxjs: ^6.5.3 || ^7.4.0 + + '@angular/compiler-cli@16.2.12': + resolution: {integrity: sha512-pWSrr152562ujh6lsFZR8NfNc5Ljj+zSTQO44DsuB0tZjwEpnRcjJEgzuhGXr+CoiBf+jTSPZKemtSktDk5aaA==} + engines: {node: ^16.14.0 || >=18.10.0} + hasBin: true + peerDependencies: + '@angular/compiler': 16.2.12 + typescript: '>=4.9.3 <5.2' + + '@angular/compiler@16.2.12': + resolution: {integrity: sha512-6SMXUgSVekGM7R6l1Z9rCtUGtlg58GFmgbpMCsGf+VXxP468Njw8rjT2YZkf5aEPxEuRpSHhDYjqz7n14cwCXQ==} + engines: {node: ^16.14.0 || >=18.10.0} + peerDependencies: + '@angular/core': 16.2.12 + peerDependenciesMeta: + '@angular/core': + optional: true + + '@angular/core@16.2.12': + resolution: {integrity: sha512-GLLlDeke/NjroaLYOks0uyzFVo6HyLl7VOm0K1QpLXnYvW63W9Ql/T3yguRZa7tRkOAeFZ3jw+1wnBD4O8MoUA==} + engines: {node: ^16.14.0 || >=18.10.0} + peerDependencies: + rxjs: ^6.5.3 || ^7.4.0 + zone.js: ~0.13.0 + + '@angular/forms@16.2.12': + resolution: {integrity: sha512-1Eao89hlBgLR3v8tU91vccn21BBKL06WWxl7zLpQmG6Hun+2jrThgOE4Pf3os4fkkbH4Apj0tWL2fNIWe/blbw==} + engines: {node: ^16.14.0 || >=18.10.0} + peerDependencies: + '@angular/common': 16.2.12 + '@angular/core': 16.2.12 + '@angular/platform-browser': 16.2.12 + rxjs: ^6.5.3 || ^7.4.0 + + '@angular/language-service@18.2.13': + resolution: {integrity: sha512-4E4VJDrbOAxS69F9C1twQPbR9AjY47Qlz8+lwg5lJOyUJ4GoEThLbXKfadt/vIeYBwMJ7fIsYWXD0Dlmxh4k+w==} + engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0} + + '@angular/material-moment-adapter@16.2.14': + resolution: {integrity: sha512-LagTDXEq8XOVLy8CVswCbmq7v9bb84+VikEEN09tz831U/7PHjDZ3xRgpKtv7hXrh8cTZOg3UPQw5tZk0hwh3Q==} + peerDependencies: + '@angular/core': ^16.0.0 || ^17.0.0 + '@angular/material': 16.2.14 + moment: ^2.18.1 + + '@angular/material@16.2.14': + resolution: {integrity: sha512-zQIxUb23elPfiIvddqkIDYqQhAHa9ZwMblfbv+ug8bxr4D0Dw360jIarxCgMjAcLj7Ccl3GBqZMUnVeM6cjthw==} + peerDependencies: + '@angular/animations': ^16.0.0 || ^17.0.0 + '@angular/cdk': 16.2.14 + '@angular/common': ^16.0.0 || ^17.0.0 + '@angular/core': ^16.0.0 || ^17.0.0 + '@angular/forms': ^16.0.0 || ^17.0.0 + '@angular/platform-browser': ^16.0.0 || ^17.0.0 + rxjs: ^6.5.3 || ^7.4.0 + + '@angular/platform-browser-dynamic@16.2.12': + resolution: {integrity: sha512-ya54jerNgreCVAR278wZavwjrUWImMr2F8yM5n9HBvsMBbFaAQ83anwbOEiHEF2BlR+gJiEBLfpuPRMw20pHqw==} + engines: {node: ^16.14.0 || >=18.10.0} + peerDependencies: + '@angular/common': 16.2.12 + '@angular/compiler': 16.2.12 + '@angular/core': 16.2.12 + '@angular/platform-browser': 16.2.12 + + '@angular/platform-browser@16.2.12': + resolution: {integrity: sha512-NnH7ju1iirmVEsUq432DTm0nZBGQsBrU40M3ZeVHMQ2subnGiyUs3QyzDz8+VWLL/T5xTxWLt9BkDn65vgzlIQ==} + engines: {node: ^16.14.0 || >=18.10.0} + peerDependencies: + '@angular/animations': 16.2.12 + '@angular/common': 16.2.12 + '@angular/core': 16.2.12 + peerDependenciesMeta: + '@angular/animations': + optional: true + + '@angular/router@16.2.12': + resolution: {integrity: sha512-aU6QnYSza005V9P3W6PpkieL56O0IHps96DjqI1RS8yOJUl3THmokqYN4Fm5+HXy4f390FN9i6ftadYQDKeWmA==} + engines: {node: ^16.14.0 || >=18.10.0} + peerDependencies: + '@angular/common': 16.2.12 + '@angular/core': 16.2.12 + '@angular/platform-browser': 16.2.12 + rxjs: ^6.5.3 || ^7.4.0 + + '@angular/service-worker@16.2.12': + resolution: {integrity: sha512-o0z0s4c76NmRASa+mUHn/q6vUKQNa06mGmLBDKm84vRQ1sQ2TJv+R1p8K9WkiM5mGy6tjQCDOgaz13TcxMFWOQ==} + engines: {node: ^16.14.0 || >=18.10.0} + hasBin: true + peerDependencies: + '@angular/common': 16.2.12 + '@angular/core': 16.2.12 + + '@antfu/install-pkg@1.1.0': + resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} + + '@antfu/utils@8.1.1': + resolution: {integrity: sha512-Mex9nXf9vR6AhcXmMrlz/HVgYYZpVGJ6YlPgwl7UnaFpnshXs6EK/oa5Gpf3CzENMjkvEx2tQtntGnb7UtSTOQ==} + + '@apidevtools/json-schema-ref-parser@11.9.3': + resolution: {integrity: sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ==} + engines: {node: '>= 16'} + + '@asamuzakjp/css-color@3.2.0': + resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} + + '@assemblyscript/loader@0.10.1': + resolution: {integrity: sha512-H71nDOOL8Y7kWRLqf6Sums+01Q5msqBW2KhDUTemh1tvY04eSkSXrK0uj/4mmY0Xr16/3zyZmsrxN7CKuRbNRg==} + + '@babel/code-frame@7.27.1': + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.28.0': + resolution: {integrity: sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.22.9': + resolution: {integrity: sha512-G2EgeufBcYw27U4hhoIwFcgc1XU7TlXJ3mv04oOv1WCuo900U/anZSPzEqNjwdjgffkk2Gs0AN0dW1CKVLcG7w==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.23.2': + resolution: {integrity: sha512-n7s51eWdaWZ3vGT2tD4T7J6eJs3QoBXydv7vkUM06Bf1cbVD2Kc2UrkzhiQwobfV7NwOnQXYL7UBJ5VPU+RGoQ==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.28.0': + resolution: {integrity: sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==} + engines: {node: '>=6.9.0'} + + '@babel/eslint-parser@7.28.0': + resolution: {integrity: sha512-N4ntErOlKvcbTt01rr5wj3y55xnIdx1ymrfIr8C2WnM1Y9glFgWaGDEULJIazOX3XM9NRzhfJ6zZnQ1sBNWU+w==} + engines: {node: ^10.13.0 || ^12.13.0 || >=14.0.0} + peerDependencies: + '@babel/core': ^7.11.0 + eslint: ^7.5.0 || ^8.0.0 || ^9.0.0 + + '@babel/generator@7.22.9': + resolution: {integrity: sha512-KtLMbmicyuK2Ak/FTCJVbDnkN1SlT8/kceFTiuDiiRUUSMnHMidxSCdG4ndkTOHHpoomWe/4xkvHkEOncwjYIw==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.28.0': + resolution: {integrity: sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-annotate-as-pure@7.22.5': + resolution: {integrity: sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-annotate-as-pure@7.27.3': + resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.27.2': + resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-create-class-features-plugin@7.27.1': + resolution: {integrity: sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-create-regexp-features-plugin@7.27.1': + resolution: {integrity: sha512-uVDC72XVf8UbrH5qQTc18Agb8emwjTiZrQE11Nv3CuBEZmVvTwwE9CBUEvHku06gQCAyYf8Nv6ja1IN+6LMbxQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-define-polyfill-provider@0.4.4': + resolution: {integrity: sha512-QcJMILQCu2jm5TFPGA3lCpJJTeEP+mqeXooG/NZbg/h5FTFi6V0+99ahlRsW8/kRLyb24LZVCCiclDedhLKcBA==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + '@babel/helper-define-polyfill-provider@0.5.0': + resolution: {integrity: sha512-NovQquuQLAQ5HuyjCz7WQP9MjRj7dx++yspwiyUiGl9ZyadHRSql1HZh5ogRd8W8w6YM6EQ/NTB8rgjLt5W65Q==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + '@babel/helper-define-polyfill-provider@0.6.5': + resolution: {integrity: sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + '@babel/helper-environment-visitor@7.24.7': + resolution: {integrity: sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-member-expression-to-functions@7.27.1': + resolution: {integrity: sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.27.1': + resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.27.3': + resolution: {integrity: sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-optimise-call-expression@7.27.1': + resolution: {integrity: sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-plugin-utils@7.27.1': + resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-remap-async-to-generator@7.27.1': + resolution: {integrity: sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-replace-supers@7.27.1': + resolution: {integrity: sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + resolution: {integrity: sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-split-export-declaration@7.22.6': + resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.27.1': + resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-wrap-function@7.27.1': + resolution: {integrity: sha512-NFJK2sHUvrjo8wAU/nQTWU890/zB2jj0qBcCbZbbf+005cAsv6tMjXz31fBign6M5ov1o0Bllu+9nbqkfsjjJQ==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.27.6': + resolution: {integrity: sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.28.0': + resolution: {integrity: sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.27.1': + resolution: {integrity: sha512-QPG3C9cCVRQLxAVwmefEmwdTanECuUBMQZ/ym5kiw3XKCGA7qkuQLcjWWHcrD/GKbn/WmJwaezfuuAOcyKlRPA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1': + resolution: {integrity: sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1': + resolution: {integrity: sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1': + resolution: {integrity: sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.13.0 + + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.27.1': + resolution: {integrity: sha512-6BpaYGDavZqkI6yT+KSPdpZFfpnd68UKXbcjI9pJ13pvHhPrCKWOOLp+ysvMeA+DxnhuPpgIaRpxRxo5A9t5jw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-proposal-async-generator-functions@7.20.7': + resolution: {integrity: sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA==} + engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-async-generator-functions instead. + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2': + resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-proposal-unicode-property-regex@7.18.6': + resolution: {integrity: sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==} + engines: {node: '>=4'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-unicode-property-regex instead. + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-async-generators@7.8.4': + resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-class-properties@7.12.13': + resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-class-static-block@7.14.5': + resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-dynamic-import@7.8.3': + resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-export-namespace-from@7.8.3': + resolution: {integrity: sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-assertions@7.27.1': + resolution: {integrity: sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-attributes@7.27.1': + resolution: {integrity: sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-meta@7.10.4': + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-json-strings@7.8.3': + resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-jsx@7.27.1': + resolution: {integrity: sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-logical-assignment-operators@7.10.4': + resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3': + resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-numeric-separator@7.10.4': + resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-object-rest-spread@7.8.3': + resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-optional-catch-binding@7.8.3': + resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-optional-chaining@7.8.3': + resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-private-property-in-object@7.14.5': + resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-top-level-await@7.14.5': + resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.27.1': + resolution: {integrity: sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-unicode-sets-regex@7.18.6': + resolution: {integrity: sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-arrow-functions@7.27.1': + resolution: {integrity: sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-async-generator-functions@7.28.0': + resolution: {integrity: sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-async-to-generator@7.22.5': + resolution: {integrity: sha512-b1A8D8ZzE/VhNDoV1MSJTnpKkCG5bJo+19R4o4oy03zM7ws8yEMK755j61Dc3EyvdysbqH5BOOTquJ7ZX9C6vQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-async-to-generator@7.27.1': + resolution: {integrity: sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-block-scoped-functions@7.27.1': + resolution: {integrity: sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-block-scoping@7.28.0': + resolution: {integrity: sha512-gKKnwjpdx5sER/wl0WN0efUBFzF/56YZO0RJrSYP4CljXnP31ByY7fol89AzomdlLNzI36AvOTmYHsnZTCkq8Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-class-properties@7.27.1': + resolution: {integrity: sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-class-static-block@7.27.1': + resolution: {integrity: sha512-s734HmYU78MVzZ++joYM+NkJusItbdRcbm+AGRgJCt3iA+yux0QpD9cBVdz3tKyrjVYWRl7j0mHSmv4lhV0aoA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.12.0 + + '@babel/plugin-transform-classes@7.28.0': + resolution: {integrity: sha512-IjM1IoJNw72AZFlj33Cu8X0q2XK/6AaVC3jQu+cgQ5lThWD5ajnuUAml80dqRmOhmPkTH8uAwnpMu9Rvj0LTRA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-computed-properties@7.27.1': + resolution: {integrity: sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-destructuring@7.28.0': + resolution: {integrity: sha512-v1nrSMBiKcodhsyJ4Gf+Z0U/yawmJDBOTpEB3mcQY52r9RIyPneGyAS/yM6seP/8I+mWI3elOMtT5dB8GJVs+A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-dotall-regex@7.27.1': + resolution: {integrity: sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-duplicate-keys@7.27.1': + resolution: {integrity: sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.27.1': + resolution: {integrity: sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-dynamic-import@7.27.1': + resolution: {integrity: sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-explicit-resource-management@7.28.0': + resolution: {integrity: sha512-K8nhUcn3f6iB+P3gwCv/no7OdzOZQcKchW6N389V6PD8NUWKZHzndOd9sPDVbMoBsbmjMqlB4L9fm+fEFNVlwQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-exponentiation-operator@7.27.1': + resolution: {integrity: sha512-uspvXnhHvGKf2r4VVtBpeFnuDWsJLQ6MF6lGJLC89jBR1uoVeqM416AZtTuhTezOfgHicpJQmoD5YUakO/YmXQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-export-namespace-from@7.27.1': + resolution: {integrity: sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-for-of@7.27.1': + resolution: {integrity: sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-function-name@7.27.1': + resolution: {integrity: sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-json-strings@7.27.1': + resolution: {integrity: sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-literals@7.27.1': + resolution: {integrity: sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-logical-assignment-operators@7.27.1': + resolution: {integrity: sha512-SJvDs5dXxiae4FbSL1aBJlG4wvl594N6YEVVn9e3JGulwioy6z3oPjx/sQBO3Y4NwUu5HNix6KJ3wBZoewcdbw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-member-expression-literals@7.27.1': + resolution: {integrity: sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-amd@7.27.1': + resolution: {integrity: sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-commonjs@7.27.1': + resolution: {integrity: sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-systemjs@7.27.1': + resolution: {integrity: sha512-w5N1XzsRbc0PQStASMksmUeqECuzKuTJer7kFagK8AXgpCMkeDMO5S+aaFb7A51ZYDF7XI34qsTX+fkHiIm5yA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-umd@7.27.1': + resolution: {integrity: sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-named-capturing-groups-regex@7.27.1': + resolution: {integrity: sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-new-target@7.27.1': + resolution: {integrity: sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-nullish-coalescing-operator@7.27.1': + resolution: {integrity: sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-numeric-separator@7.27.1': + resolution: {integrity: sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-object-rest-spread@7.28.0': + resolution: {integrity: sha512-9VNGikXxzu5eCiQjdE4IZn8sb9q7Xsk5EXLDBKUYg1e/Tve8/05+KJEtcxGxAgCY5t/BpKQM+JEL/yT4tvgiUA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-object-super@7.27.1': + resolution: {integrity: sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-optional-catch-binding@7.27.1': + resolution: {integrity: sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-optional-chaining@7.27.1': + resolution: {integrity: sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-parameters@7.27.7': + resolution: {integrity: sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-private-methods@7.27.1': + resolution: {integrity: sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-private-property-in-object@7.27.1': + resolution: {integrity: sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-property-literals@7.27.1': + resolution: {integrity: sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-constant-elements@7.27.1': + resolution: {integrity: sha512-edoidOjl/ZxvYo4lSBOQGDSyToYVkTAwyVoa2tkuYTSmjrB1+uAedoL5iROVLXkxH+vRgA7uP4tMg2pUJpZ3Ug==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-display-name@7.28.0': + resolution: {integrity: sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-development@7.27.1': + resolution: {integrity: sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-self@7.27.1': + resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.27.1': + resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx@7.27.1': + resolution: {integrity: sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-pure-annotations@7.27.1': + resolution: {integrity: sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-regenerator@7.28.1': + resolution: {integrity: sha512-P0QiV/taaa3kXpLY+sXla5zec4E+4t4Aqc9ggHlfZ7a2cp8/x/Gv08jfwEtn9gnnYIMvHx6aoOZ8XJL8eU71Dg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-regexp-modifiers@7.27.1': + resolution: {integrity: sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-reserved-words@7.27.1': + resolution: {integrity: sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-runtime@7.22.9': + resolution: {integrity: sha512-9KjBH61AGJetCPYp/IEyLEp47SyybZb0nDRpBvmtEkm+rUIwxdlKpyNHI1TmsGkeuLclJdleQHRZ8XLBnnh8CQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-runtime@7.28.0': + resolution: {integrity: sha512-dGopk9nZrtCs2+nfIem25UuHyt5moSJamArzIoh9/vezUQPmYDOzjaHDCkAzuGJibCIkPup8rMT2+wYB6S73cA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-shorthand-properties@7.27.1': + resolution: {integrity: sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-spread@7.27.1': + resolution: {integrity: sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-sticky-regex@7.27.1': + resolution: {integrity: sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-template-literals@7.27.1': + resolution: {integrity: sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typeof-symbol@7.27.1': + resolution: {integrity: sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typescript@7.28.0': + resolution: {integrity: sha512-4AEiDEBPIZvLQaWlc9liCavE0xRM0dNca41WtBeM3jgFptfUOSG9z0uteLhq6+3rq+WB6jIvUwKDTpXEHPJ2Vg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-escapes@7.27.1': + resolution: {integrity: sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-property-regex@7.27.1': + resolution: {integrity: sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-regex@7.27.1': + resolution: {integrity: sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-sets-regex@7.27.1': + resolution: {integrity: sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/preset-env@7.22.9': + resolution: {integrity: sha512-wNi5H/Emkhll/bqPjsjQorSykrlfY5OWakd6AulLvMEytpKasMVUpVy8RL4qBIBs5Ac6/5i0/Rv0b/Fg6Eag/g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/preset-env@7.28.0': + resolution: {integrity: sha512-VmaxeGOwuDqzLl5JUkIRM1X2Qu2uKGxHEQWh+cvvbl7JuJRgKGJSfsEF/bUaxFhJl/XAyxBe7q7qSuTbKFuCyg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/preset-modules@0.1.6': + resolution: {integrity: sha512-ID2yj6K/4lKfhuU3+EX4UvNbIt7eACFbHmNUjzA+ep+B5971CknnA/9DEWKbRokfbbtblxxxXFJJrH47UEAMVg==} + peerDependencies: + '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0 + + '@babel/preset-modules@0.1.6-no-external-plugins': + resolution: {integrity: sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==} + peerDependencies: + '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0 + + '@babel/preset-react@7.27.1': + resolution: {integrity: sha512-oJHWh2gLhU9dW9HHr42q0cI0/iHHXTLGe39qvpAZZzagHy0MzYLCnCVV0symeRvzmjHyVU7mw2K06E6u/JwbhA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/preset-typescript@7.27.1': + resolution: {integrity: sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/runtime-corejs3@7.28.0': + resolution: {integrity: sha512-nlIXnSqLcBij8K8TtkxbBJgfzfvi75V1pAKSM7dUXejGw12vJAqez74jZrHTsJ3Z+Aczc5Q/6JgNjKRMsVU44g==} + engines: {node: '>=6.9.0'} + + '@babel/runtime@7.22.6': + resolution: {integrity: sha512-wDb5pWm4WDdF6LFUde3Jl8WzPA+3ZbxYqkC6xAXuD3irdEHN1k0NfTRrJD8ZD378SJ61miMLCqIOXYhd8x+AJQ==} + engines: {node: '>=6.9.0'} + + '@babel/runtime@7.27.6': + resolution: {integrity: sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.22.5': + resolution: {integrity: sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.27.2': + resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.28.0': + resolution: {integrity: sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.28.1': + resolution: {integrity: sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==} + engines: {node: '>=6.9.0'} + + '@braintree/sanitize-url@7.1.1': + resolution: {integrity: sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw==} + + '@bufbuild/buf-darwin-arm64@1.55.1': + resolution: {integrity: sha512-g76yEF2ALyjj+R8KVoIjPPS7zaPy6VDWg2b5PCCK04fKTbe5jyzOdYdvNyuM5hO8xpRPBjBrqO6LUAfS+0aRCQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@bufbuild/buf-darwin-x64@1.55.1': + resolution: {integrity: sha512-hCkatzlV7DwHWEyzzcpsZgLtxABidT/EYBmLTy6oSCHimOJzR1c5ezKe75tX7myDAfV0HZExIM7+cSsWg3dTPg==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@bufbuild/buf-linux-aarch64@1.55.1': + resolution: {integrity: sha512-hA4jGPZ2N+FUZt03w+hPt6YsbhAdOh2gNKBQnuysj8kdTqZ4mw1wCOUoRg9h7eqOr/2XCcOibWYO36H2eS2OYQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@bufbuild/buf-linux-armv7@1.55.1': + resolution: {integrity: sha512-npnzJSAZRUdh8+fmgsbXt+dogA+iU/i/qWh+3XhdLXW220nWpd1jAXcpquaVfer8EwMEcYjqVf8FA7IXOgjGXw==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@bufbuild/buf-linux-x64@1.55.1': + resolution: {integrity: sha512-/48IjSA1kh/8kZl1bcDkikgH+9BMnPhhVqad+R7+3ttYOFqifet3n+hf2ipA26NtTpTfvOSRXj1tdx/80x0I1g==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@bufbuild/buf-win32-arm64@1.55.1': + resolution: {integrity: sha512-CE+jAN1ikRTIdny6Q/geccKsLhy4QEXzUaJUfAiUXqjSW2u/Et7+p9Wh6xUgXcSuIoz1aw8MVuCESrNMBt6LBg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@bufbuild/buf-win32-x64@1.55.1': + resolution: {integrity: sha512-C4VYS96YBJkLhIKH6yh8BqHgIjqGe+G6yuAOOKxWsYdx3QbT5LYOr38AP1bzkFm0Gz9jOOr5n0pmAFwsOLYjiw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@bufbuild/buf@1.55.1': + resolution: {integrity: sha512-V9tpe2XlRVyq33cct2lNz9nDDQG95WbPKlxQkMKt5i7tPsfqE3vzbGiEC96K0QJWhIU28OkjYD8+1SyYKBWVYg==} + engines: {node: '>=12'} + hasBin: true + + '@bufbuild/protobuf@2.6.1': + resolution: {integrity: sha512-DaG6XlyKpz08bmHY5SGX2gfIllaqtDJ/KwVoxsmP22COOLYwDBe7yD3DZGwXem/Xq7QOc9cuR7R3MpAv5CFfDw==} + + '@bufbuild/protocompile@0.0.1': + resolution: {integrity: sha512-cOTMtjcWLcbjF17dPYgeMtVC5jZyS0bSjz3jy8kDPjOgjgSYMD2u2It7w8aCc2z23hTPIKl/2SNdMnz0Jzu3Xg==} + peerDependencies: + '@bufbuild/buf': ^1.22.0 + + '@changesets/apply-release-plan@7.0.12': + resolution: {integrity: sha512-EaET7As5CeuhTzvXTQCRZeBUcisoYPDDcXvgTE/2jmmypKp0RC7LxKj/yzqeh/1qFTZI7oDGFcL1PHRuQuketQ==} + + '@changesets/assemble-release-plan@6.0.9': + resolution: {integrity: sha512-tPgeeqCHIwNo8sypKlS3gOPmsS3wP0zHt67JDuL20P4QcXiw/O4Hl7oXiuLnP9yg+rXLQ2sScdV1Kkzde61iSQ==} + + '@changesets/changelog-git@0.2.1': + resolution: {integrity: sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q==} + + '@changesets/cli@2.29.5': + resolution: {integrity: sha512-0j0cPq3fgxt2dPdFsg4XvO+6L66RC0pZybT9F4dG5TBrLA3jA/1pNkdTXH9IBBVHkgsKrNKenI3n1mPyPlIydg==} + hasBin: true + + '@changesets/config@3.1.1': + resolution: {integrity: sha512-bd+3Ap2TKXxljCggI0mKPfzCQKeV/TU4yO2h2C6vAihIo8tzseAn2e7klSuiyYYXvgu53zMN1OeYMIQkaQoWnA==} + + '@changesets/errors@0.2.0': + resolution: {integrity: sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow==} + + '@changesets/get-dependents-graph@2.1.3': + resolution: {integrity: sha512-gphr+v0mv2I3Oxt19VdWRRUxq3sseyUpX9DaHpTUmLj92Y10AGy+XOtV+kbM6L/fDcpx7/ISDFK6T8A/P3lOdQ==} + + '@changesets/get-release-plan@4.0.13': + resolution: {integrity: sha512-DWG1pus72FcNeXkM12tx+xtExyH/c9I1z+2aXlObH3i9YA7+WZEVaiHzHl03thpvAgWTRaH64MpfHxozfF7Dvg==} + + '@changesets/get-version-range-type@0.4.0': + resolution: {integrity: sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ==} + + '@changesets/git@3.0.4': + resolution: {integrity: sha512-BXANzRFkX+XcC1q/d27NKvlJ1yf7PSAgi8JG6dt8EfbHFHi4neau7mufcSca5zRhwOL8j9s6EqsxmT+s+/E6Sw==} + + '@changesets/logger@0.1.1': + resolution: {integrity: sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg==} + + '@changesets/parse@0.4.1': + resolution: {integrity: sha512-iwksMs5Bf/wUItfcg+OXrEpravm5rEd9Bf4oyIPL4kVTmJQ7PNDSd6MDYkpSJR1pn7tz/k8Zf2DhTCqX08Ou+Q==} + + '@changesets/pre@2.0.2': + resolution: {integrity: sha512-HaL/gEyFVvkf9KFg6484wR9s0qjAXlZ8qWPDkTyKF6+zqjBe/I2mygg3MbpZ++hdi0ToqNUF8cjj7fBy0dg8Ug==} + + '@changesets/read@0.6.5': + resolution: {integrity: sha512-UPzNGhsSjHD3Veb0xO/MwvasGe8eMyNrR/sT9gR8Q3DhOQZirgKhhXv/8hVsI0QpPjR004Z9iFxoJU6in3uGMg==} + + '@changesets/should-skip-package@0.1.2': + resolution: {integrity: sha512-qAK/WrqWLNCP22UDdBTMPH5f41elVDlsNyat180A33dWxuUDyNpg6fPi/FyTZwRriVjg0L8gnjJn2F9XAoF0qw==} + + '@changesets/types@4.1.0': + resolution: {integrity: sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw==} + + '@changesets/types@6.1.0': + resolution: {integrity: sha512-rKQcJ+o1nKNgeoYRHKOS07tAMNd3YSN0uHaJOZYjBAgxfV7TUE7JE+z4BzZdQwb5hKaYbayKN5KrYV7ODb2rAA==} + + '@changesets/write@0.4.0': + resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} + + '@chevrotain/cst-dts-gen@11.0.3': + resolution: {integrity: sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==} + + '@chevrotain/gast@11.0.3': + resolution: {integrity: sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==} + + '@chevrotain/regexp-to-ast@11.0.3': + resolution: {integrity: sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==} + + '@chevrotain/types@11.0.3': + resolution: {integrity: sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==} + + '@chevrotain/utils@11.0.3': + resolution: {integrity: sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==} + + '@colors/colors@1.5.0': + resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} + engines: {node: '>=0.1.90'} + + '@connectrpc/connect-node@2.0.2': + resolution: {integrity: sha512-33Ut3SRkb6SugpwVCtXXRvUrOdtiyG6z6d5+eijBOLOI75sw1tDCwcs0o/9WL3rUj1M08dLUrPmJB47fjpv6EA==} + engines: {node: '>=18.14.1'} + peerDependencies: + '@bufbuild/protobuf': ^2.2.0 + '@connectrpc/connect': 2.0.2 + + '@connectrpc/connect-web@2.0.2': + resolution: {integrity: sha512-QANMFPiL2o66BdBEctg4TsQLe5ozsBLqcle3dCBp7BwGlNGTY6NnNnqmt+YRnpeMW88GgomJwWNMGCrRD9pRKA==} + peerDependencies: + '@bufbuild/protobuf': ^2.2.0 + '@connectrpc/connect': 2.0.2 + + '@connectrpc/connect@2.0.2': + resolution: {integrity: sha512-xZuylIUNvNlH52e/4eQsZvY4QZyDJRtEFEDnn/yBrv5Xi5ZZI/p8X+GAHH35ucVaBvv9u7OzHZo8+tEh1EFTxA==} + peerDependencies: + '@bufbuild/protobuf': ^2.2.0 + + '@csstools/cascade-layer-name-parser@2.0.5': + resolution: {integrity: sha512-p1ko5eHgV+MgXFVa4STPKpvPxr6ReS8oS2jzTukjR74i5zJNyWO1ZM1m8YKBXnzDKWfBN1ztLYlHxbVemDD88A==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/color-helpers@5.0.2': + resolution: {integrity: sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==} + engines: {node: '>=18'} + + '@csstools/css-calc@2.1.4': + resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-color-parser@3.0.10': + resolution: {integrity: sha512-TiJ5Ajr6WRd1r8HSiwJvZBiJOqtH86aHpUjq5aEKWHiII2Qfjqd/HCWKPOW8EP4vcspXbHnXrwIDlu5savQipg==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-parser-algorithms@3.0.5': + resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-tokenizer@3.0.4': + resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} + engines: {node: '>=18'} + + '@csstools/media-query-list-parser@4.0.3': + resolution: {integrity: sha512-HAYH7d3TLRHDOUQK4mZKf9k9Ph/m8Akstg66ywKR4SFAigjs3yBiUeZtFxywiTm5moZMAp/5W/ZuFnNXXYLuuQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/postcss-cascade-layers@5.0.2': + resolution: {integrity: sha512-nWBE08nhO8uWl6kSAeCx4im7QfVko3zLrtgWZY4/bP87zrSPpSyN/3W3TDqz1jJuH+kbKOHXg5rJnK+ZVYcFFg==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-color-function@4.0.10': + resolution: {integrity: sha512-4dY0NBu7NVIpzxZRgh/Q/0GPSz/jLSw0i/u3LTUor0BkQcz/fNhN10mSWBDsL0p9nDb0Ky1PD6/dcGbhACuFTQ==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-color-mix-function@3.0.10': + resolution: {integrity: sha512-P0lIbQW9I4ShE7uBgZRib/lMTf9XMjJkFl/d6w4EMNHu2qvQ6zljJGEcBkw/NsBtq/6q3WrmgxSS8kHtPMkK4Q==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-color-mix-variadic-function-arguments@1.0.0': + resolution: {integrity: sha512-Z5WhouTyD74dPFPrVE7KydgNS9VvnjB8qcdes9ARpCOItb4jTnm7cHp4FhxCRUoyhabD0WVv43wbkJ4p8hLAlQ==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-content-alt-text@2.0.6': + resolution: {integrity: sha512-eRjLbOjblXq+byyaedQRSrAejKGNAFued+LcbzT+LCL78fabxHkxYjBbxkroONxHHYu2qxhFK2dBStTLPG3jpQ==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-exponential-functions@2.0.9': + resolution: {integrity: sha512-abg2W/PI3HXwS/CZshSa79kNWNZHdJPMBXeZNyPQFbbj8sKO3jXxOt/wF7juJVjyDTc6JrvaUZYFcSBZBhaxjw==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-font-format-keywords@4.0.0': + resolution: {integrity: sha512-usBzw9aCRDvchpok6C+4TXC57btc4bJtmKQWOHQxOVKen1ZfVqBUuCZ/wuqdX5GHsD0NRSr9XTP+5ID1ZZQBXw==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-gamut-mapping@2.0.10': + resolution: {integrity: sha512-QDGqhJlvFnDlaPAfCYPsnwVA6ze+8hhrwevYWlnUeSjkkZfBpcCO42SaUD8jiLlq7niouyLgvup5lh+f1qessg==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-gradients-interpolation-method@5.0.10': + resolution: {integrity: sha512-HHPauB2k7Oits02tKFUeVFEU2ox/H3OQVrP3fSOKDxvloOikSal+3dzlyTZmYsb9FlY9p5EUpBtz0//XBmy+aw==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-hwb-function@4.0.10': + resolution: {integrity: sha512-nOKKfp14SWcdEQ++S9/4TgRKchooLZL0TUFdun3nI4KPwCjETmhjta1QT4ICQcGVWQTvrsgMM/aLB5We+kMHhQ==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-ic-unit@4.0.2': + resolution: {integrity: sha512-lrK2jjyZwh7DbxaNnIUjkeDmU8Y6KyzRBk91ZkI5h8nb1ykEfZrtIVArdIjX4DHMIBGpdHrgP0n4qXDr7OHaKA==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-initial@2.0.1': + resolution: {integrity: sha512-L1wLVMSAZ4wovznquK0xmC7QSctzO4D0Is590bxpGqhqjboLXYA16dWZpfwImkdOgACdQ9PqXsuRroW6qPlEsg==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-is-pseudo-class@5.0.3': + resolution: {integrity: sha512-jS/TY4SpG4gszAtIg7Qnf3AS2pjcUM5SzxpApOrlndMeGhIbaTzWBzzP/IApXoNWEW7OhcjkRT48jnAUIFXhAQ==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-light-dark-function@2.0.9': + resolution: {integrity: sha512-1tCZH5bla0EAkFAI2r0H33CDnIBeLUaJh1p+hvvsylJ4svsv2wOmJjJn+OXwUZLXef37GYbRIVKX+X+g6m+3CQ==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-logical-float-and-clear@3.0.0': + resolution: {integrity: sha512-SEmaHMszwakI2rqKRJgE+8rpotFfne1ZS6bZqBoQIicFyV+xT1UF42eORPxJkVJVrH9C0ctUgwMSn3BLOIZldQ==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-logical-overflow@2.0.0': + resolution: {integrity: sha512-spzR1MInxPuXKEX2csMamshR4LRaSZ3UXVaRGjeQxl70ySxOhMpP2252RAFsg8QyyBXBzuVOOdx1+bVO5bPIzA==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-logical-overscroll-behavior@2.0.0': + resolution: {integrity: sha512-e/webMjoGOSYfqLunyzByZj5KKe5oyVg/YSbie99VEaSDE2kimFm0q1f6t/6Jo+VVCQ/jbe2Xy+uX+C4xzWs4w==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-logical-resize@3.0.0': + resolution: {integrity: sha512-DFbHQOFW/+I+MY4Ycd/QN6Dg4Hcbb50elIJCfnwkRTCX05G11SwViI5BbBlg9iHRl4ytB7pmY5ieAFk3ws7yyg==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-logical-viewport-units@3.0.4': + resolution: {integrity: sha512-q+eHV1haXA4w9xBwZLKjVKAWn3W2CMqmpNpZUk5kRprvSiBEGMgrNH3/sJZ8UA3JgyHaOt3jwT9uFa4wLX4EqQ==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-media-minmax@2.0.9': + resolution: {integrity: sha512-af9Qw3uS3JhYLnCbqtZ9crTvvkR+0Se+bBqSr7ykAnl9yKhk6895z9rf+2F4dClIDJWxgn0iZZ1PSdkhrbs2ig==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-media-queries-aspect-ratio-number-values@3.0.5': + resolution: {integrity: sha512-zhAe31xaaXOY2Px8IYfoVTB3wglbJUVigGphFLj6exb7cjZRH9A6adyE22XfFK3P2PzwRk0VDeTJmaxpluyrDg==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-nested-calc@4.0.0': + resolution: {integrity: sha512-jMYDdqrQQxE7k9+KjstC3NbsmC063n1FTPLCgCRS2/qHUbHM0mNy9pIn4QIiQGs9I/Bg98vMqw7mJXBxa0N88A==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-normalize-display-values@4.0.0': + resolution: {integrity: sha512-HlEoG0IDRoHXzXnkV4in47dzsxdsjdz6+j7MLjaACABX2NfvjFS6XVAnpaDyGesz9gK2SC7MbNwdCHusObKJ9Q==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-oklab-function@4.0.10': + resolution: {integrity: sha512-ZzZUTDd0fgNdhv8UUjGCtObPD8LYxMH+MJsW9xlZaWTV8Ppr4PtxlHYNMmF4vVWGl0T6f8tyWAKjoI6vePSgAg==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-progressive-custom-properties@4.1.0': + resolution: {integrity: sha512-YrkI9dx8U4R8Sz2EJaoeD9fI7s7kmeEBfmO+UURNeL6lQI7VxF6sBE+rSqdCBn4onwqmxFdBU3lTwyYb/lCmxA==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-random-function@2.0.1': + resolution: {integrity: sha512-q+FQaNiRBhnoSNo+GzqGOIBKoHQ43lYz0ICrV+UudfWnEF6ksS6DsBIJSISKQT2Bvu3g4k6r7t0zYrk5pDlo8w==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-relative-color-syntax@3.0.10': + resolution: {integrity: sha512-8+0kQbQGg9yYG8hv0dtEpOMLwB9M+P7PhacgIzVzJpixxV4Eq9AUQtQw8adMmAJU1RBBmIlpmtmm3XTRd/T00g==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-scope-pseudo-class@4.0.1': + resolution: {integrity: sha512-IMi9FwtH6LMNuLea1bjVMQAsUhFxJnyLSgOp/cpv5hrzWmrUYU5fm0EguNDIIOHUqzXode8F/1qkC/tEo/qN8Q==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-sign-functions@1.1.4': + resolution: {integrity: sha512-P97h1XqRPcfcJndFdG95Gv/6ZzxUBBISem0IDqPZ7WMvc/wlO+yU0c5D/OCpZ5TJoTt63Ok3knGk64N+o6L2Pg==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-stepped-value-functions@4.0.9': + resolution: {integrity: sha512-h9btycWrsex4dNLeQfyU3y3w40LMQooJWFMm/SK9lrKguHDcFl4VMkncKKoXi2z5rM9YGWbUQABI8BT2UydIcA==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-text-decoration-shorthand@4.0.2': + resolution: {integrity: sha512-8XvCRrFNseBSAGxeaVTaNijAu+FzUvjwFXtcrynmazGb/9WUdsPCpBX+mHEHShVRq47Gy4peYAoxYs8ltUnmzA==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-trigonometric-functions@4.0.9': + resolution: {integrity: sha512-Hnh5zJUdpNrJqK9v1/E3BbrQhaDTj5YiX7P61TOvUhoDHnUmsNNxcDAgkQ32RrcWx9GVUvfUNPcUkn8R3vIX6A==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/postcss-unset-value@4.0.0': + resolution: {integrity: sha512-cBz3tOCI5Fw6NIFEwU3RiwK6mn3nKegjpJuzCndoGq3BZPkUjnsq7uQmIeMNeMbMk7YD2MfKcgCpZwX5jyXqCA==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@csstools/selector-resolve-nested@3.1.0': + resolution: {integrity: sha512-mf1LEW0tJLKfWyvn5KdDrhpxHyuxpbNwTIwOYLIvsTffeyOf85j5oIzfG0yosxDgx/sswlqBnESYUcQH0vgZ0g==} + engines: {node: '>=18'} + peerDependencies: + postcss-selector-parser: ^7.0.0 + + '@csstools/selector-specificity@5.0.0': + resolution: {integrity: sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==} + engines: {node: '>=18'} + peerDependencies: + postcss-selector-parser: ^7.0.0 + + '@csstools/utilities@2.0.0': + resolution: {integrity: sha512-5VdOr0Z71u+Yp3ozOx8T11N703wIFGVRgOWbOZMKgglPJsWA54MRIoMNVMa7shUToIhx5J8vX4sOZgD2XiihiQ==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + '@ctrl/ngx-codemirror@6.1.0': + resolution: {integrity: sha512-73QeoNbnluZalWmNw+SOFctsE+oz0+4Bl9KhlJOIfbCPK/U6OIc+vQNr28hMAp15Y/Idde3LntTwLBDFW0bRVA==} + peerDependencies: + '@angular/core': '>=15.0.0-0' + '@angular/forms': '>=15.0.0-0' + codemirror: ^5.65.9 + + '@ctrl/tinycolor@3.6.1': + resolution: {integrity: sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==} + engines: {node: '>=10'} + + '@cypress/request@3.0.8': + resolution: {integrity: sha512-h0NFgh1mJmm1nr4jCwkGHwKneVYKghUyWe6TMNrk0B9zsjAJxpg8C4/+BAcmLgCPa1vj1V8rNUaILl+zYRUWBQ==} + engines: {node: '>= 6'} + + '@cypress/request@3.0.9': + resolution: {integrity: sha512-I3l7FdGRXluAS44/0NguwWlO83J18p0vlr2FYHrJkWdNYhgVoiYo61IXPqaOsL+vNxU1ZqMACzItGK3/KKDsdw==} + engines: {node: '>= 6'} + + '@cypress/xvfb@1.2.4': + resolution: {integrity: sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==} + + '@devcontainers/cli@0.80.0': + resolution: {integrity: sha512-w2EaxgjyeVGyzfA/KUEZBhyXqu/5PyWNXcnrXsZOBrt3aN2zyGiHrXoG54TF6K0b5DSCF01Rt5fnIyrCeFzFKw==} + engines: {node: ^16.13.0 || >=18.0.0} + hasBin: true + + '@discoveryjs/json-ext@0.5.7': + resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==} + engines: {node: '>=10.0.0'} + + '@docsearch/css@3.9.0': + resolution: {integrity: sha512-cQbnVbq0rrBwNAKegIac/t6a8nWoUAn8frnkLFW6YARaRmAQr5/Eoe6Ln2fqkUCZ40KpdrKbpSAmgrkviOxuWA==} + + '@docsearch/react@3.9.0': + resolution: {integrity: sha512-mb5FOZYZIkRQ6s/NWnM98k879vu5pscWqTLubLFBO87igYYT4VzVazh4h5o/zCvTIZgEt3PvsCOMOswOUo9yHQ==} + peerDependencies: + '@types/react': '>= 16.8.0 < 20.0.0' + react: '>= 16.8.0 < 20.0.0' + react-dom: '>= 16.8.0 < 20.0.0' + search-insights: '>= 1 < 3' + peerDependenciesMeta: + '@types/react': + optional: true + react: + optional: true + react-dom: + optional: true + search-insights: + optional: true + + '@docusaurus/babel@3.8.1': + resolution: {integrity: sha512-3brkJrml8vUbn9aeoZUlJfsI/GqyFcDgQJwQkmBtclJgWDEQBKKeagZfOgx0WfUQhagL1sQLNW0iBdxnI863Uw==} + engines: {node: '>=18.0'} + + '@docusaurus/bundler@3.8.1': + resolution: {integrity: sha512-/z4V0FRoQ0GuSLToNjOSGsk6m2lQUG4FRn8goOVoZSRsTrU8YR2aJacX5K3RG18EaX9b+52pN4m1sL3MQZVsQA==} + engines: {node: '>=18.0'} + peerDependencies: + '@docusaurus/faster': '*' + peerDependenciesMeta: + '@docusaurus/faster': + optional: true + + '@docusaurus/core@3.8.1': + resolution: {integrity: sha512-ENB01IyQSqI2FLtOzqSI3qxG2B/jP4gQPahl2C3XReiLebcVh5B5cB9KYFvdoOqOWPyr5gXK4sjgTKv7peXCrA==} + engines: {node: '>=18.0'} + hasBin: true + peerDependencies: + '@mdx-js/react': ^3.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + + '@docusaurus/cssnano-preset@3.8.1': + resolution: {integrity: sha512-G7WyR2N6SpyUotqhGznERBK+x84uyhfMQM2MmDLs88bw4Flom6TY46HzkRkSEzaP9j80MbTN8naiL1fR17WQug==} + engines: {node: '>=18.0'} + + '@docusaurus/faster@3.8.1': + resolution: {integrity: sha512-XYrj3qnTm+o2d5ih5drCq9s63GJoM8vZ26WbLG5FZhURsNxTSXgHJcx11Qo7nWPUStCQkuqk1HvItzscCUnd4A==} + engines: {node: '>=18.0'} + peerDependencies: + '@docusaurus/types': '*' + + '@docusaurus/logger@3.8.1': + resolution: {integrity: sha512-2wjeGDhKcExEmjX8k1N/MRDiPKXGF2Pg+df/bDDPnnJWHXnVEZxXj80d6jcxp1Gpnksl0hF8t/ZQw9elqj2+ww==} + engines: {node: '>=18.0'} + + '@docusaurus/mdx-loader@3.8.1': + resolution: {integrity: sha512-DZRhagSFRcEq1cUtBMo4TKxSNo/W6/s44yhr8X+eoXqCLycFQUylebOMPseHi5tc4fkGJqwqpWJLz6JStU9L4w==} + engines: {node: '>=18.0'} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + + '@docusaurus/module-type-aliases@3.8.1': + resolution: {integrity: sha512-6xhvAJiXzsaq3JdosS7wbRt/PwEPWHr9eM4YNYqVlbgG1hSK3uQDXTVvQktasp3VO6BmfYWPozueLWuj4gB+vg==} + peerDependencies: + react: '*' + react-dom: '*' + + '@docusaurus/plugin-content-blog@3.8.1': + resolution: {integrity: sha512-vNTpMmlvNP9n3hGEcgPaXyvTljanAKIUkuG9URQ1DeuDup0OR7Ltvoc8yrmH+iMZJbcQGhUJF+WjHLwuk8HSdw==} + engines: {node: '>=18.0'} + peerDependencies: + '@docusaurus/plugin-content-docs': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + + '@docusaurus/plugin-content-docs@3.8.1': + resolution: {integrity: sha512-oByRkSZzeGNQByCMaX+kif5Nl2vmtj2IHQI2fWjCfCootsdKZDPFLonhIp5s3IGJO7PLUfe0POyw0Xh/RrGXJA==} + engines: {node: '>=18.0'} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + + '@docusaurus/plugin-content-pages@3.8.1': + resolution: {integrity: sha512-a+V6MS2cIu37E/m7nDJn3dcxpvXb6TvgdNI22vJX8iUTp8eoMoPa0VArEbWvCxMY/xdC26WzNv4wZ6y0iIni/w==} + engines: {node: '>=18.0'} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + + '@docusaurus/plugin-css-cascade-layers@3.8.1': + resolution: {integrity: sha512-VQ47xRxfNKjHS5ItzaVXpxeTm7/wJLFMOPo1BkmoMG4Cuz4nuI+Hs62+RMk1OqVog68Swz66xVPK8g9XTrBKRw==} + engines: {node: '>=18.0'} + + '@docusaurus/plugin-debug@3.8.1': + resolution: {integrity: sha512-nT3lN7TV5bi5hKMB7FK8gCffFTBSsBsAfV84/v293qAmnHOyg1nr9okEw8AiwcO3bl9vije5nsUvP0aRl2lpaw==} + engines: {node: '>=18.0'} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + + '@docusaurus/plugin-google-analytics@3.8.1': + resolution: {integrity: sha512-Hrb/PurOJsmwHAsfMDH6oVpahkEGsx7F8CWMjyP/dw1qjqmdS9rcV1nYCGlM8nOtD3Wk/eaThzUB5TSZsGz+7Q==} + engines: {node: '>=18.0'} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + + '@docusaurus/plugin-google-gtag@3.8.1': + resolution: {integrity: sha512-tKE8j1cEZCh8KZa4aa80zpSTxsC2/ZYqjx6AAfd8uA8VHZVw79+7OTEP2PoWi0uL5/1Is0LF5Vwxd+1fz5HlKg==} + engines: {node: '>=18.0'} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + + '@docusaurus/plugin-google-tag-manager@3.8.1': + resolution: {integrity: sha512-iqe3XKITBquZq+6UAXdb1vI0fPY5iIOitVjPQ581R1ZKpHr0qe+V6gVOrrcOHixPDD/BUKdYwkxFjpNiEN+vBw==} + engines: {node: '>=18.0'} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + + '@docusaurus/plugin-sitemap@3.8.1': + resolution: {integrity: sha512-+9YV/7VLbGTq8qNkjiugIelmfUEVkTyLe6X8bWq7K5qPvGXAjno27QAfFq63mYfFFbJc7z+pudL63acprbqGzw==} + engines: {node: '>=18.0'} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + + '@docusaurus/plugin-svgr@3.8.1': + resolution: {integrity: sha512-rW0LWMDsdlsgowVwqiMb/7tANDodpy1wWPwCcamvhY7OECReN3feoFwLjd/U4tKjNY3encj0AJSTxJA+Fpe+Gw==} + engines: {node: '>=18.0'} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + + '@docusaurus/preset-classic@3.8.1': + resolution: {integrity: sha512-yJSjYNHXD8POMGc2mKQuj3ApPrN+eG0rO1UPgSx7jySpYU+n4WjBikbrA2ue5ad9A7aouEtMWUoiSRXTH/g7KQ==} + engines: {node: '>=18.0'} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + + '@docusaurus/react-loadable@6.0.0': + resolution: {integrity: sha512-YMMxTUQV/QFSnbgrP3tjDzLHRg7vsbMn8e9HAa8o/1iXoiomo48b7sk/kkmWEuWNDPJVlKSJRB6Y2fHqdJk+SQ==} + peerDependencies: + react: '*' + + '@docusaurus/theme-classic@3.8.1': + resolution: {integrity: sha512-bqDUCNqXeYypMCsE1VcTXSI1QuO4KXfx8Cvl6rYfY0bhhqN6d2WZlRkyLg/p6pm+DzvanqHOyYlqdPyP0iz+iw==} + engines: {node: '>=18.0'} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + + '@docusaurus/theme-common@3.8.1': + resolution: {integrity: sha512-UswMOyTnPEVRvN5Qzbo+l8k4xrd5fTFu2VPPfD6FcW/6qUtVLmJTQCktbAL3KJ0BVXGm5aJXz/ZrzqFuZERGPw==} + engines: {node: '>=18.0'} + peerDependencies: + '@docusaurus/plugin-content-docs': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + + '@docusaurus/theme-mermaid@3.8.1': + resolution: {integrity: sha512-IWYqjyTPjkNnHsFFu9+4YkeXS7PD1xI3Bn2shOhBq+f95mgDfWInkpfBN4aYvx4fTT67Am6cPtohRdwh4Tidtg==} + engines: {node: '>=18.0'} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + + '@docusaurus/theme-search-algolia@3.8.1': + resolution: {integrity: sha512-NBFH5rZVQRAQM087aYSRKQ9yGEK9eHd+xOxQjqNpxMiV85OhJDD4ZGz6YJIod26Fbooy54UWVdzNU0TFeUUUzQ==} + engines: {node: '>=18.0'} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + + '@docusaurus/theme-translations@3.8.1': + resolution: {integrity: sha512-OTp6eebuMcf2rJt4bqnvuwmm3NVXfzfYejL+u/Y1qwKhZPrjPoKWfk1CbOP5xH5ZOPkiAsx4dHdQBRJszK3z2g==} + engines: {node: '>=18.0'} + + '@docusaurus/types@3.8.1': + resolution: {integrity: sha512-ZPdW5AB+pBjiVrcLuw3dOS6BFlrG0XkS2lDGsj8TizcnREQg3J8cjsgfDviszOk4CweNfwo1AEELJkYaMUuOPg==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + + '@docusaurus/utils-common@3.8.1': + resolution: {integrity: sha512-zTZiDlvpvoJIrQEEd71c154DkcriBecm4z94OzEE9kz7ikS3J+iSlABhFXM45mZ0eN5pVqqr7cs60+ZlYLewtg==} + engines: {node: '>=18.0'} + + '@docusaurus/utils-validation@3.8.1': + resolution: {integrity: sha512-gs5bXIccxzEbyVecvxg6upTwaUbfa0KMmTj7HhHzc016AGyxH2o73k1/aOD0IFrdCsfJNt37MqNI47s2MgRZMA==} + engines: {node: '>=18.0'} + + '@docusaurus/utils@3.8.1': + resolution: {integrity: sha512-P1ml0nvOmEFdmu0smSXOqTS1sxU5tqvnc0dA4MTKV39kye+bhQnjkIKEE18fNOvxjyB86k8esoCIFM3x4RykOQ==} + engines: {node: '>=18.0'} + + '@emnapi/core@1.4.5': + resolution: {integrity: sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==} + + '@emnapi/runtime@1.4.5': + resolution: {integrity: sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==} + + '@emnapi/wasi-threads@1.0.4': + resolution: {integrity: sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==} + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.25.8': + resolution: {integrity: sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.18.17': + resolution: {integrity: sha512-9np+YYdNDed5+Jgr1TdWBsozZ85U1Oa3xW0c7TWqH0y2aGghXtZsuT8nYRbzOMcl0bXZXjOGbksoTtVOlWrRZg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.25.8': + resolution: {integrity: sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.18.17': + resolution: {integrity: sha512-wHsmJG/dnL3OkpAcwbgoBTTMHVi4Uyou3F5mf58ZtmUyIKfcdA7TROav/6tCzET4A3QW2Q2FC+eFneMU+iyOxg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.25.8': + resolution: {integrity: sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.18.17': + resolution: {integrity: sha512-O+FeWB/+xya0aLg23hHEM2E3hbfwZzjqumKMSIqcHbNvDa+dza2D0yLuymRBQQnC34CWrsJUXyH2MG5VnLd6uw==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.25.8': + resolution: {integrity: sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.18.17': + resolution: {integrity: sha512-M9uJ9VSB1oli2BE/dJs3zVr9kcCBBsE883prage1NWz6pBS++1oNn/7soPNS3+1DGj0FrkSvnED4Bmlu1VAE9g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.25.8': + resolution: {integrity: sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.18.17': + resolution: {integrity: sha512-XDre+J5YeIJDMfp3n0279DFNrGCXlxOuGsWIkRb1NThMZ0BsrWXoTg23Jer7fEXQ9Ye5QjrvXpxnhzl3bHtk0g==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.8': + resolution: {integrity: sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.18.17': + resolution: {integrity: sha512-cjTzGa3QlNfERa0+ptykyxs5A6FEUQQF0MuilYXYBGdBxD3vxJcKnzDlhDCa1VAJCmAxed6mYhA2KaJIbtiNuQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.25.8': + resolution: {integrity: sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.18.17': + resolution: {integrity: sha512-sOxEvR8d7V7Kw8QqzxWc7bFfnWnGdaFBut1dRUYtu+EIRXefBc/eIsiUiShnW0hM3FmQ5Zf27suDuHsKgZ5QrA==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.8': + resolution: {integrity: sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.18.17': + resolution: {integrity: sha512-c9w3tE7qA3CYWjT+M3BMbwMt+0JYOp3vCMKgVBrCl1nwjAlOMYzEo+gG7QaZ9AtqZFj5MbUc885wuBBmu6aADQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.25.8': + resolution: {integrity: sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.18.17': + resolution: {integrity: sha512-2d3Lw6wkwgSLC2fIvXKoMNGVaeY8qdN0IC3rfuVxJp89CRfA3e3VqWifGDfuakPmp90+ZirmTfye1n4ncjv2lg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.25.8': + resolution: {integrity: sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.18.17': + resolution: {integrity: sha512-1DS9F966pn5pPnqXYz16dQqWIB0dmDfAQZd6jSSpiT9eX1NzKh07J6VKR3AoXXXEk6CqZMojiVDSZi1SlmKVdg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.25.8': + resolution: {integrity: sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.18.17': + resolution: {integrity: sha512-EvLsxCk6ZF0fpCB6w6eOI2Fc8KW5N6sHlIovNe8uOFObL2O+Mr0bflPHyHwLT6rwMg9r77WOAWb2FqCQrVnwFg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.25.8': + resolution: {integrity: sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.18.17': + resolution: {integrity: sha512-e0bIdHA5p6l+lwqTE36NAW5hHtw2tNRmHlGBygZC14QObsA3bD4C6sXLJjvnDIjSKhW1/0S3eDy+QmX/uZWEYQ==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.25.8': + resolution: {integrity: sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.18.17': + resolution: {integrity: sha512-BAAilJ0M5O2uMxHYGjFKn4nJKF6fNCdP1E0o5t5fvMYYzeIqy2JdAP88Az5LHt9qBoUa4tDaRpfWt21ep5/WqQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.25.8': + resolution: {integrity: sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.18.17': + resolution: {integrity: sha512-Wh/HW2MPnC3b8BqRSIme/9Zhab36PPH+3zam5pqGRH4pE+4xTrVLx2+XdGp6fVS3L2x+DrsIcsbMleex8fbE6g==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.8': + resolution: {integrity: sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.18.17': + resolution: {integrity: sha512-j/34jAl3ul3PNcK3pfI0NSlBANduT2UO5kZ7FCaK33XFv3chDhICLY8wJJWIhiQ+YNdQ9dxqQctRg2bvrMlYgg==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.25.8': + resolution: {integrity: sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.18.17': + resolution: {integrity: sha512-QM50vJ/y+8I60qEmFxMoxIx4de03pGo2HwxdBeFd4nMh364X6TIBZ6VQ5UQmPbQWUVWHWws5MmJXlHAXvJEmpQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.25.8': + resolution: {integrity: sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.8': + resolution: {integrity: sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.18.17': + resolution: {integrity: sha512-/jGlhWR7Sj9JPZHzXyyMZ1RFMkNPjC6QIAan0sDOtIo2TYk3tZn5UDrkE0XgsTQCxWTTOcMPf9p6Rh2hXtl5TQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.8': + resolution: {integrity: sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.8': + resolution: {integrity: sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.18.17': + resolution: {integrity: sha512-rSEeYaGgyGGf4qZM2NonMhMOP/5EHp4u9ehFiBrg7stH6BYEEjlkVREuDEcQ0LfIl53OXLxNbfuIj7mr5m29TA==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.8': + resolution: {integrity: sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.8': + resolution: {integrity: sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.18.17': + resolution: {integrity: sha512-Y7ZBbkLqlSgn4+zot4KUNYst0bFoO68tRgI6mY2FIM+b7ZbyNVtNbDP5y8qlu4/knZZ73fgJDlXID+ohY5zt5g==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.25.8': + resolution: {integrity: sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.18.17': + resolution: {integrity: sha512-bwPmTJsEQcbZk26oYpc4c/8PvTY3J5/QK8jM19DVlEsAB41M39aWovWoHtNm78sd6ip6prilxeHosPADXtEJFw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.25.8': + resolution: {integrity: sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.18.17': + resolution: {integrity: sha512-H/XaPtPKli2MhW+3CQueo6Ni3Avggi6hP/YvgkEe1aSaxw+AeO8MFjq8DlgfTd9Iz4Yih3QCZI6YLMoyccnPRg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.25.8': + resolution: {integrity: sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.18.17': + resolution: {integrity: sha512-fGEb8f2BSA3CW7riJVurug65ACLuQAzKq0SSqkY2b2yHHH0MzDfbLyKIGzHwOI/gkHcxM/leuSW6D5w/LMNitA==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.25.8': + resolution: {integrity: sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.7.0': + resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.1': + resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/eslintrc@2.1.4': + resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@eslint/js@8.57.1': + resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@exodus/schemasafe@1.3.0': + resolution: {integrity: sha512-5Aap/GaRupgNx/feGBwLLTVv8OQFfv3pq2lPRzPg9R+IOBnDgghTGW7l7EuVXOvg5cc/xSAlRW8rBrjIC3Nvqw==} + + '@faker-js/faker@5.5.3': + resolution: {integrity: sha512-R11tGE6yIFwqpaIqcfkcg7AICXzFg14+5h5v0TfF/9+RMDL6jhzCy/pxHVOfbALGdtVYdt6JdR21tuxEgl34dw==} + deprecated: Please update to a newer version. + + '@faker-js/faker@9.9.0': + resolution: {integrity: sha512-OEl393iCOoo/z8bMezRlJu+GlRGlsKbUAN7jKB6LhnKoqKve5DXRpalbItIIcwnCjs1k/FOPjFzcA6Qn+H+YbA==} + engines: {node: '>=18.0.0', npm: '>=9.0.0'} + + '@floating-ui/core@1.7.2': + resolution: {integrity: sha512-wNB5ooIKHQc+Kui96jE/n69rHFWAVoxn5CAzL1Xdd8FG03cgY3MLO+GF9U3W737fYDSgPWA6MReKhBQBop6Pcw==} + + '@floating-ui/dom@1.7.2': + resolution: {integrity: sha512-7cfaOQuCS27HD7DX+6ib2OrnW+b4ZBwDNnCcT0uTyidcmyWb03FnQqJybDBoCnpdxwBSfA94UAYlRCt7mV+TbA==} + + '@floating-ui/react-dom@2.1.4': + resolution: {integrity: sha512-JbbpPhp38UmXDDAu60RJmbeme37Jbgsm7NrHGgzYYFKmblzRUh6Pa641dII6LsjwF4XlScDrde2UAzDo/b9KPw==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/react@0.26.28': + resolution: {integrity: sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.10': + resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + + '@formatjs/ecma402-abstract@2.3.4': + resolution: {integrity: sha512-qrycXDeaORzIqNhBOx0btnhpD1c+/qFIHAN9znofuMJX6QBwtbrmlpWfD4oiUUD2vJUOIYFA/gYtg2KAMGG7sA==} + + '@formatjs/fast-memoize@2.2.7': + resolution: {integrity: sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==} + + '@formatjs/icu-messageformat-parser@2.11.2': + resolution: {integrity: sha512-AfiMi5NOSo2TQImsYAg8UYddsNJ/vUEv/HaNqiFjnI3ZFfWihUtD5QtuX6kHl8+H+d3qvnE/3HZrfzgdWpsLNA==} + + '@formatjs/icu-skeleton-parser@1.8.14': + resolution: {integrity: sha512-i4q4V4qslThK4Ig8SxyD76cp3+QJ3sAqr7f6q9VVfeGtxG9OhiAk3y9XF6Q41OymsKzsGQ6OQQoJNY4/lI8TcQ==} + + '@formatjs/intl-localematcher@0.5.10': + resolution: {integrity: sha512-af3qATX+m4Rnd9+wHcjJ4w2ijq+rAVP3CCinJQvFv1kgSu1W6jypUmvleJxcewdxmutM8dmIRZFxO/IQBZmP2Q==} + + '@formatjs/intl-localematcher@0.6.1': + resolution: {integrity: sha512-ePEgLgVCqi2BBFnTMWPfIghu6FkbZnnBVhO2sSxvLfrdFw7wCHAHiDoM2h4NRgjbaY7+B7HgOLZGkK187pZTZg==} + + '@fortawesome/angular-fontawesome@0.13.0': + resolution: {integrity: sha512-gzSPRdveOXNO7NIiMgTyB46aiHG0i98KinnAEqHXi8qzraM/kCcHn/0y3f4MhemX6kftwsFli0IU8RyHmtXlSQ==} + peerDependencies: + '@angular/core': ^16.0.0 + '@fortawesome/fontawesome-svg-core': ~1.2.27 || ~1.3.0-beta2 || ^6.1.0 + + '@fortawesome/fontawesome-common-types@6.7.2': + resolution: {integrity: sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg==} + engines: {node: '>=6'} + + '@fortawesome/fontawesome-svg-core@6.7.2': + resolution: {integrity: sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA==} + engines: {node: '>=6'} + + '@fortawesome/free-brands-svg-icons@6.7.2': + resolution: {integrity: sha512-zu0evbcRTgjKfrr77/2XX+bU+kuGfjm0LbajJHVIgBWNIDzrhpRxiCPNT8DW5AdmSsq7Mcf9D1bH0aSeSUSM+Q==} + engines: {node: '>=6'} + + '@gar/promisify@1.1.3': + resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==} + + '@grpc/grpc-js@1.13.4': + resolution: {integrity: sha512-GsFaMXCkMqkKIvwCQjCrwH+GHbPKBjhwo/8ZuUkWHqbI73Kky9I+pQltrlT0+MWpedCoosda53lgjYfyEPgxBg==} + engines: {node: '>=12.10.0'} + + '@grpc/proto-loader@0.7.15': + resolution: {integrity: sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==} + engines: {node: '>=6'} + hasBin: true + + '@hapi/hoek@9.3.0': + resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==} + + '@hapi/topo@5.1.0': + resolution: {integrity: sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==} + + '@headlessui/react@1.7.19': + resolution: {integrity: sha512-Ll+8q3OlMJfJbAKM/+/Y2q6PPYbryqNTXDbryx7SXLIDamkF6iQFbriYHga0dY44PvDhvvBWCx1Xj4U5+G4hOw==} + engines: {node: '>=10'} + peerDependencies: + react: ^16 || ^17 || ^18 + react-dom: ^16 || ^17 || ^18 + + '@headlessui/react@2.2.4': + resolution: {integrity: sha512-lz+OGcAH1dK93rgSMzXmm1qKOJkBUqZf1L4M8TWLNplftQD3IkoEDdUFNfAn4ylsN6WOTVtWaLmvmaHOUk1dTA==} + engines: {node: '>=10'} + peerDependencies: + react: ^18 || ^19 || ^19.0.0-rc + react-dom: ^18 || ^19 || ^19.0.0-rc + + '@heroicons/react@2.1.3': + resolution: {integrity: sha512-fEcPfo4oN345SoqdlCDdSa4ivjaKbk0jTd+oubcgNxnNgAfzysfwWfQUr+51wigiWHQQRiZNd1Ao0M5Y3M2EGg==} + peerDependencies: + react: '>= 16' + + '@hookform/error-message@2.0.1': + resolution: {integrity: sha512-U410sAr92xgxT1idlu9WWOVjndxLdgPUHEB8Schr27C9eh7/xUnITWpCMF93s+lGiG++D4JnbSnrb5A21AdSNg==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + react-hook-form: ^7.0.0 + + '@humanwhocodes/config-array@0.13.0': + resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} + engines: {node: '>=10.10.0'} + deprecated: Use @eslint/config-array instead + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/object-schema@2.0.3': + resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} + deprecated: Use @eslint/object-schema instead + + '@iconify/types@2.0.0': + resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} + + '@iconify/utils@2.3.0': + resolution: {integrity: sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA==} + + '@img/sharp-darwin-arm64@0.34.3': + resolution: {integrity: sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.3': + resolution: {integrity: sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.2.0': + resolution: {integrity: sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.2.0': + resolution: {integrity: sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.2.0': + resolution: {integrity: sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.2.0': + resolution: {integrity: sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-ppc64@1.2.0': + resolution: {integrity: sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==} + cpu: [ppc64] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.2.0': + resolution: {integrity: sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.2.0': + resolution: {integrity: sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.0': + resolution: {integrity: sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.2.0': + resolution: {integrity: sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.34.3': + resolution: {integrity: sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linux-arm@0.34.3': + resolution: {integrity: sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-ppc64@0.34.3': + resolution: {integrity: sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + + '@img/sharp-linux-s390x@0.34.3': + resolution: {integrity: sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + + '@img/sharp-linux-x64@0.34.3': + resolution: {integrity: sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-linuxmusl-arm64@0.34.3': + resolution: {integrity: sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linuxmusl-x64@0.34.3': + resolution: {integrity: sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-wasm32@0.34.3': + resolution: {integrity: sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.3': + resolution: {integrity: sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.3': + resolution: {integrity: sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.34.3': + resolution: {integrity: sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + + '@inkeep/cxkit-color-mode@0.5.95': + resolution: {integrity: sha512-31VZO+NQ2EQDzmKJFToLDRne5FKs05El/AGpakU420nzbpamTqgTyDEF/BLips+E9hJM6VBKHu0tgUQvc/ipRg==} + + '@inkeep/cxkit-docusaurus@0.5.95': + resolution: {integrity: sha512-6ayt9lNFLvjyY8J03XjPMwjOVC/vsQu3O0AzkOq/D+I+sD6CAlZq52aA7JHOQhd6F+CjoDySmhbOwmM5BSuQcw==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@inkeep/cxkit-primitives@0.5.95': + resolution: {integrity: sha512-3/r7qtcmUqXr/fhUfTPlnsw3gWdVqRWuDzqIca2MFB7V1FOjRGE5tq4nMj22QgmgJAztBtARsT0wXjVfjont0w==} + peerDependencies: + react: '>=17.0.0' + react-dom: '>=17.0.0' + + '@inkeep/cxkit-react@0.5.95': + resolution: {integrity: sha512-M/zaGOxnuIe/K+myETt5OvduQ8i4ncUGQ+CL+bcbKJH13wfuaqa2PveIMmF/uOhskCl1DYHmTj5x5R5cGpR7lw==} + + '@inkeep/cxkit-styled@0.5.95': + resolution: {integrity: sha512-5FKiMh3MikqbUW51LaeDYE1X9aTT0Gu12Wm/5IQpjMP/gjJR3WJf1868JMkRpN9UR0hngq85/LRmDo8UW3wvGg==} + + '@inkeep/cxkit-theme@0.5.95': + resolution: {integrity: sha512-PbVJEP2sx4ox2puEfGom2c4alQkdo6RpfMrkAYbt6VpfJx2+Hb65+QU7FlFR+Fy0QMqlOrIS2Ki9Wl/jk/HX+Q==} + + '@inkeep/cxkit-types@0.5.95': + resolution: {integrity: sha512-UCRBGKWjkR10wPVf0fUlnp59I3q0Uo0xF+IsoWfbr8ksxXKoCw5P1+bNeXYUZpbtRpi/Fjq15TXdbOy1pIUvUA==} + + '@isaacs/balanced-match@4.0.1': + resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} + engines: {node: 20 || >=22} + + '@isaacs/brace-expansion@5.0.0': + resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==} + engines: {node: 20 || >=22} + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@istanbuljs/load-nyc-config@1.1.0': + resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} + engines: {node: '>=8'} + + '@istanbuljs/schema@0.1.3': + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} + + '@jest/schemas@29.6.3': + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/types@29.6.3': + resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jridgewell/gen-mapping@0.3.12': + resolution: {integrity: sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/source-map@0.3.10': + resolution: {integrity: sha512-0pPkgz9dY+bijgistcTTJ5mR+ocqRXLuhXHYdzoMmmoJ2C9S46RCm2GMUbatPEUK9Yjy26IrAy8D/M00lLkv+Q==} + + '@jridgewell/sourcemap-codec@1.5.4': + resolution: {integrity: sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==} + + '@jridgewell/trace-mapping@0.3.29': + resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==} + + '@js-sdsl/ordered-map@4.4.2': + resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} + + '@jsdevtools/ono@7.1.3': + resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==} + + '@leichtgewicht/ip-codec@2.0.5': + resolution: {integrity: sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==} + + '@manypkg/find-root@1.1.0': + resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} + + '@manypkg/get-packages@1.1.3': + resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==} + + '@mapbox/node-pre-gyp@1.0.11': + resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==} + hasBin: true + + '@material/animation@15.0.0-canary.bc9ae6c9c.0': + resolution: {integrity: sha512-leRf+BcZTfC/iSigLXnYgcHAGvFVQveoJT5+2PIRdyPI/bIG7hhciRgacHRsCKC0sGya81dDblLgdkjSUemYLw==} + + '@material/auto-init@15.0.0-canary.bc9ae6c9c.0': + resolution: {integrity: sha512-uxzDq7q3c0Bu1pAsMugc1Ik9ftQYQqZY+5e2ybNplT8gTImJhNt4M2mMiMHbMANk2l3UgICmUyRSomgPBWCPIA==} + + '@material/banner@15.0.0-canary.bc9ae6c9c.0': + resolution: {integrity: sha512-SHeVoidCUFVhXANN6MNWxK9SZoTSgpIP8GZB7kAl52BywLxtV+FirTtLXkg/8RUkxZRyRWl7HvQ0ZFZa7QQAyA==} + + '@material/base@15.0.0-canary.bc9ae6c9c.0': + resolution: {integrity: sha512-Fc3vGuOf+duGo22HTRP6dHdc+MUe0VqQfWOuKrn/wXKD62m0QQR2TqJd3rRhCumH557T5QUyheW943M3E+IGfg==} + + '@material/button@15.0.0-canary.bc9ae6c9c.0': + resolution: {integrity: sha512-3AQgwrPZCTWHDJvwgKq7Cj+BurQ4wTjDdGL+FEnIGUAjJDskwi1yzx5tW2Wf/NxIi7IoPFyOY3UB41jwMiOrnw==} + + '@material/card@15.0.0-canary.bc9ae6c9c.0': + resolution: {integrity: sha512-nPlhiWvbLmooTnBmV5gmzB0eLWSgLKsSRBYAbIBmO76Okgz1y+fQNLag+lpm/TDaHVsn5fmQJH8e0zIg0rYsQA==} + + '@material/checkbox@15.0.0-canary.bc9ae6c9c.0': + resolution: {integrity: sha512-4tpNnO1L0IppoMF3oeQn8F17t2n0WHB0D7mdJK9rhrujen/fLbekkIC82APB3fdGtLGg3qeNqDqPsJm1YnmrwA==} + + '@material/chips@15.0.0-canary.bc9ae6c9c.0': + resolution: {integrity: sha512-fqHKvE5bSWK0bXVkf57MWxZtytGqYBZvvHIOs4JI9HPHEhaJy4CpSw562BEtbm3yFxxALoQknvPW2KYzvADnmA==} + + '@material/circular-progress@15.0.0-canary.bc9ae6c9c.0': + resolution: {integrity: sha512-Lxe8BGAxQwCQqrLhrYrIP0Uok10h7aYS3RBXP41ph+5GmwJd5zdyE2t93qm2dyThvU6qKuXw9726Dtq/N+wvZQ==} + + '@material/data-table@15.0.0-canary.bc9ae6c9c.0': + resolution: {integrity: sha512-j/7qplT9+sUpfe4pyWhPbl01qJA+OoNAG3VMJruBBR461ZBKyTi7ssKH9yksFGZ8eCEPkOsk/+kDxsiZvRWkeQ==} + + '@material/density@15.0.0-canary.bc9ae6c9c.0': + resolution: {integrity: sha512-Zt3u07fXrBWLW06Tl5fgvjicxNQMkFdawLyNTzZ5TvbXfVkErILLePwwGaw8LNcvzqJP6ABLA8jiR+sKNoJQCg==} + + '@material/dialog@15.0.0-canary.bc9ae6c9c.0': + resolution: {integrity: sha512-o+9a/fmwJ9+gY3Z/uhj/PMVJDq7it1NTWKJn2GwAKdB+fDkT4hb9qEdcxMPyvJJ5ups+XiKZo03+tZrD+38c1w==} + + '@material/dom@15.0.0-canary.bc9ae6c9c.0': + resolution: {integrity: sha512-ly78R7aoCJtundSUu0UROU+5pQD5Piae0Y1MkN6bs0724azeazX1KeXFeaf06JOXnlr5/41ol+fSUPowjoqnOg==} + + '@material/drawer@15.0.0-canary.bc9ae6c9c.0': + resolution: {integrity: sha512-PFL4cEFnt7VTxDsuspFVNhsFDYyumjU0VWfj3PWB7XudsEfQ3lo85D3HCEtTTbRsCainGN8bgYNDNafLBqiigw==} + + '@material/elevation@15.0.0-canary.bc9ae6c9c.0': + resolution: {integrity: sha512-Ro+Pk8jFuap+T0B0shA3xI1hs2b89dNQ2EIPCNjNMp87emHKAzJfhKb7EZGIwv3+gFLlVaLyIVkb94I89KLsyg==} + + '@material/fab@15.0.0-canary.bc9ae6c9c.0': + resolution: {integrity: sha512-dvU0KWMRglwJEQwmQtFAmJcAjzg9VFF6Aqj78bJYu/DAIGFJ1VTTTSgoXM/XCm1YyQEZ7kZRvxBO37CH54rSDg==} + + '@material/feature-targeting@15.0.0-canary.bc9ae6c9c.0': + resolution: {integrity: sha512-wkDjVcoVEYYaJvun28IXdln/foLgPD7n9ZC9TY76GErGCwTq+HWpU6wBAAk+ePmpRFDayw4vI4wBlaWGxLtysQ==} + + '@material/floating-label@15.0.0-canary.bc9ae6c9c.0': + resolution: {integrity: sha512-bUWPtXzZITOD/2mkvLkEPO1ngDWmb74y0Kgbz6llHLOQBtycyJIpuoQJ1q2Ez0NM/tFLwPphhAgRqmL3YQ/Kzw==} + + '@material/focus-ring@15.0.0-canary.bc9ae6c9c.0': + resolution: {integrity: sha512-cZHThVose3GvAlJzpJoBI1iqL6d1/Jj9hXrR+r8Mwtb1hBIUEG3hxfsRd4vGREuzROPlf0OgNf/V+YHoSwgR5w==} + + '@material/form-field@15.0.0-canary.bc9ae6c9c.0': + resolution: {integrity: sha512-+JFXy5X44Gue1CbZZAQ6YejnI203lebYwL0i6k0ylDpWHEOdD5xkF2PyHR28r9/65Ebcbwbff6q7kI1SGoT7MA==} + + '@material/icon-button@15.0.0-canary.bc9ae6c9c.0': + resolution: {integrity: sha512-1a0MHgyIwOs4RzxrVljsqSizGYFlM1zY2AZaLDsgT4G3kzsplTx8HZQ022GpUCjAygW+WLvg4z1qAhQHvsbqlw==} + + '@material/image-list@15.0.0-canary.bc9ae6c9c.0': + resolution: {integrity: sha512-WKWmiYap2iu4QdqmeUSliLlN4O2Ueqa0OuVAYHn/TCzmQ2xmnhZ1pvDLbs6TplpOmlki7vFfe+aSt5SU9gwfOQ==} + + '@material/layout-grid@15.0.0-canary.bc9ae6c9c.0': + resolution: {integrity: sha512-5GqmT6oTZhUGWIb+CLD0ZNyDyTiJsr/rm9oRIi3+vCujACwxFkON9tzBlZohdtFS16nuzUusthN6Jt9UrJcN6Q==} + + '@material/line-ripple@15.0.0-canary.bc9ae6c9c.0': + resolution: {integrity: sha512-8S30WXEuUdgDdBulzUDlPXD6qMzwCX9SxYb5mGDYLwl199cpSGdXHtGgEcCjokvnpLhdZhcT1Dsxeo1g2Evh5Q==} + + '@material/linear-progress@15.0.0-canary.bc9ae6c9c.0': + resolution: {integrity: sha512-6EJpjrz6aoH2/gXLg9iMe0yF2C42hpQyZoHpmcgTLKeci85ktDvJIjwup8tnk8ULQyFiGiIrhXw2v2RSsiFjvQ==} + + '@material/list@15.0.0-canary.bc9ae6c9c.0': + resolution: {integrity: sha512-TQ1ppqiCMQj/P7bGD4edbIIv4goczZUoiUAaPq/feb1dflvrFMzYqJ7tQRRCyBL8nRhJoI2x99tk8Q2RXvlGUQ==} + + '@material/menu-surface@15.0.0-canary.bc9ae6c9c.0': + resolution: {integrity: sha512-dMtSPN+olTWE+08M5qe4ea1IZOhVryYqzK0Gyb2u1G75rSArUxCOB5rr6OC/ST3Mq3RS6zGuYo7srZt4534K9Q==} + + '@material/menu@15.0.0-canary.bc9ae6c9c.0': + resolution: {integrity: sha512-IlAh61xzrzxXs38QZlt74UYt8J431zGznSzDtB1Fqs6YFNd11QPKoiRXn1J2Qu/lUxbFV7i8NBKMCKtia0n6/Q==} + + '@material/notched-outline@15.0.0-canary.bc9ae6c9c.0': + resolution: {integrity: sha512-WuurMg44xexkvLTBTnsO0A+qnzFjpcPdvgWBGstBepYozsvSF9zJGdb1x7Zv1MmqbpYh/Ohnuxtb/Y3jOh6irg==} + + '@material/progress-indicator@15.0.0-canary.bc9ae6c9c.0': + resolution: {integrity: sha512-uOnsvqw5F2fkeTnTl4MrYzjI7KCLmmLyZaM0cgLNuLsWVlddQE+SGMl28tENx7DUK3HebWq0FxCP8f25LuDD+w==} + + '@material/radio@15.0.0-canary.bc9ae6c9c.0': + resolution: {integrity: sha512-ehzOK+U1IxQN+OQjgD2lsnf1t7t7RAwQzeO6Czkiuid29ookYbQynWuLWk7NW8H8ohl7lnmfqTP1xSNkkL/F0g==} + + '@material/ripple@15.0.0-canary.bc9ae6c9c.0': + resolution: {integrity: sha512-JfLW+g3GMVDv4cruQ19+HUxpKVdWCldFlIPw1UYezz2h3WTNDy05S3uP2zUdXzZ01C3dkBFviv4nqZ0GCT16MA==} + + '@material/rtl@15.0.0-canary.bc9ae6c9c.0': + resolution: {integrity: sha512-SkKLNLFp5QtG7/JEFg9R92qq4MzTcZ5As6sWbH7rRg6ahTHoJEuqE+pOb9Vrtbj84k5gtX+vCYPvCILtSlr2uw==} + + '@material/segmented-button@15.0.0-canary.bc9ae6c9c.0': + resolution: {integrity: sha512-YDwkCWP9l5mIZJ7pZJZ2hMDxfBlIGVJ+deNzr8O+Z7/xC5LGXbl4R5aPtUVHygvXAXxpf5096ZD+dSXzYzvWlw==} + + '@material/select@15.0.0-canary.bc9ae6c9c.0': + resolution: {integrity: sha512-unfOWVf7T0sixVG+3k3RTuATfzqvCF6QAzA6J9rlCh/Tq4HuIBNDdV4z19IVu4zwmgWYxY0iSvqWUvdJJYwakQ==} + + '@material/shape@15.0.0-canary.bc9ae6c9c.0': + resolution: {integrity: sha512-Dsvr771ZKC46ODzoixLdGwlLEQLfxfLrtnRojXABoZf5G3o9KtJU+J+5Ld5aa960OAsCzzANuaub4iR88b1guA==} + + '@material/slider@15.0.0-canary.bc9ae6c9c.0': + resolution: {integrity: sha512-3AEu+7PwW4DSNLndue47dh2u7ga4hDJRYmuu7wnJCIWJBnLCkp6C92kNc4Rj5iQY2ftJio5aj1gqryluh5tlYg==} + + '@material/snackbar@15.0.0-canary.bc9ae6c9c.0': + resolution: {integrity: sha512-TwwQSYxfGK6mc03/rdDamycND6o+1p61WNd7ElZv1F1CLxB4ihRjbCoH7Qo+oVDaP8CTpjeclka+24RLhQq0mA==} + + '@material/switch@15.0.0-canary.bc9ae6c9c.0': + resolution: {integrity: sha512-OjUjtT0kRz1ASAsOS+dNzwMwvsjmqy5edK57692qmrP6bL4GblFfBDoiNJ6t0AN4OaKcmL5Hy/xNrTdOZW7Qqw==} + + '@material/tab-bar@15.0.0-canary.bc9ae6c9c.0': + resolution: {integrity: sha512-Xmtq0wJGfu5k+zQeFeNsr4bUKv7L+feCmUp/gsapJ655LQKMXOUQZtSv9ZqWOfrCMy55hoF1CzGFV+oN3tyWWQ==} + + '@material/tab-indicator@15.0.0-canary.bc9ae6c9c.0': + resolution: {integrity: sha512-despCJYi1GrDDq7F2hvLQkObHnSLZPPDxnOzU16zJ6FNYvIdszgfzn2HgAZ6pl5hLOexQ8cla6cAqjTDuaJBhQ==} + + '@material/tab-scroller@15.0.0-canary.bc9ae6c9c.0': + resolution: {integrity: sha512-QWHG/EWxirj4V9u2IHz+OSY9XCWrnNrPnNgEufxAJVUKV/A8ma1DYeFSQqxhX709R8wKGdycJksg0Flkl7Gq7w==} + + '@material/tab@15.0.0-canary.bc9ae6c9c.0': + resolution: {integrity: sha512-s/L9otAwn/pZwVQZBRQJmPqYeNbjoEbzbjMpDQf/VBG/6dJ+aP03ilIBEkqo8NVnCoChqcdtVCoDNRtbU+yp6w==} + + '@material/textfield@15.0.0-canary.bc9ae6c9c.0': + resolution: {integrity: sha512-R3qRex9kCaZIAK8DuxPnVC42R0OaW7AB7fsFknDKeTeVQvRcbnV8E+iWSdqTiGdsi6QQHifX8idUrXw+O45zPw==} + + '@material/theme@15.0.0-canary.bc9ae6c9c.0': + resolution: {integrity: sha512-CpUwXGE0dbhxQ45Hu9r9wbJtO/MAlv5ER4tBHA9tp/K+SU+lDgurBE2touFMg5INmdfVNtdumxb0nPPLaNQcUg==} + + '@material/tokens@15.0.0-canary.bc9ae6c9c.0': + resolution: {integrity: sha512-nbEuGj05txWz6ZMUanpM47SaAD7soyjKILR+XwDell9Zg3bGhsnexCNXPEz2fD+YgomS+jM5XmIcaJJHg/H93Q==} + + '@material/tooltip@15.0.0-canary.bc9ae6c9c.0': + resolution: {integrity: sha512-UzuXp0b9NuWuYLYpPguxrjbJnCmT/Cco8CkjI/6JajxaeA3o2XEBbQfRMTq8PTafuBjCHTc0b0mQY7rtxUp1Gg==} + + '@material/top-app-bar@15.0.0-canary.bc9ae6c9c.0': + resolution: {integrity: sha512-vJWjsvqtdSD5+yQ/9vgoBtBSCvPJ5uF/DVssv8Hdhgs1PYaAcODUi77kdi0+sy/TaWyOsTkQixqmwnFS16zesA==} + + '@material/touch-target@15.0.0-canary.bc9ae6c9c.0': + resolution: {integrity: sha512-AqYh9fjt+tv4ZE0C6MeYHblS2H+XwLbDl2mtyrK0DOEnCVQk5/l5ImKDfhrUdFWHvS4a5nBM4AA+sa7KaroLoA==} + + '@material/typography@15.0.0-canary.bc9ae6c9c.0': + resolution: {integrity: sha512-CKsG1zyv34AKPNyZC8olER2OdPII64iR2SzQjpqh1UUvmIFiMPk23LvQ1OnC5aCB14pOXzmVgvJt31r9eNdZ6Q==} + + '@mdx-js/mdx@3.1.0': + resolution: {integrity: sha512-/QxEhPAvGwbQmy1Px8F899L5Uc2KZ6JtXwlCgJmjSTBedwOZkByYcBG4GceIGPXRDsmfxhHazuS+hlOShRLeDw==} + + '@mdx-js/react@3.1.0': + resolution: {integrity: sha512-QjHtSaoameoalGnKDT3FoIl4+9RwyTmo9ZJGBdLOks/YOiWHoRDI3PUwEzOE7kEmGcV3AFcp9K6dYu9rEuKLAQ==} + peerDependencies: + '@types/react': '>=16' + react: '>=16' + + '@mermaid-js/parser@0.6.2': + resolution: {integrity: sha512-+PO02uGF6L6Cs0Bw8RpGhikVvMWEysfAyl27qTlroUB8jSWr1lL0Sf6zi78ZxlSnmgSY2AMMKVgghnN9jTtwkQ==} + + '@module-federation/error-codes@0.17.0': + resolution: {integrity: sha512-+pZ12frhaDqh4Xs/MQj4Vu4CAjnJTiEb8Z6fqPfn/TLHh4YLWMOzpzxGuMFDHqXwMb3o8FRAUhNB0eX2ZmhwTA==} + + '@module-federation/runtime-core@0.17.0': + resolution: {integrity: sha512-MYwDDevYnBB9gXFfNOmJVIX5XZcbCHd0dral7gT7yVmlwOhbuGOLlm2dh2icwwdCYHA9AFDCfU9l1nJR4ex/ng==} + + '@module-federation/runtime-tools@0.17.0': + resolution: {integrity: sha512-t4QcKfhmwOHedwByDKUlTQVw4+gPotySYPyNa8GFrBSr1F6wcGdGyOhzP+PdgpiJLIM03cB6V+IKGGHE28SfDQ==} + + '@module-federation/runtime@0.17.0': + resolution: {integrity: sha512-eMtrtCSSV6neJpMmQ8WdFpYv93raSgsG5RiAPsKUuSCXfZ5D+yzvleZ+gPcEpFT9HokmloxAn0jep50/1upTQw==} + + '@module-federation/sdk@0.17.0': + resolution: {integrity: sha512-tjrNaYdDocHZsWu5iXlm83lwEK8A64r4PQB3/kY1cW1iOvggR2RESLAWPxRJXC2cLF8fg8LDKOBdgERZW1HPFA==} + + '@module-federation/webpack-bundler-runtime@0.17.0': + resolution: {integrity: sha512-o8XtXwqTDlqLgcALOfObcCbqXvUcSDHIEXrkcb4W+I8GJY7IqV0+x6rX4mJ3f59tca9qOF8zsZsOA6BU93Pvgw==} + + '@napi-rs/wasm-runtime@0.2.12': + resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} + + '@napi-rs/wasm-runtime@1.0.1': + resolution: {integrity: sha512-KVlQ/jgywZpixGCKMNwxStmmbYEMyokZpCf2YuIChhfJA2uqfAKNEM8INz7zzTo55iEXfBhIIs3VqYyqzDLj8g==} + + '@netlify/framework-info@9.9.3': + resolution: {integrity: sha512-kPTF5yemdmadP/+qMDcc3p10NkZKXHXGm2BCFvB192paCNxQrSJz+qb56SO+kvSn9exg+HvhGJ0gfIcVwPjzWw==} + engines: {node: ^14.14.0 || >=16.0.0} + + '@next/env@15.4.0-canary.86': + resolution: {integrity: sha512-WPrEvwqHnjeLx05ncJvqizbBJJFlQGRbxzOnL/pZWKzo19auM9x5Se87P27+E/D/d6jJS801l+thF85lfobAZQ==} + + '@next/eslint-plugin-next@15.4.0-canary.86': + resolution: {integrity: sha512-cOlp6ajA1ptiBxiProcXaNAR88O5ck3IwGJr+A5SnNKU4iTUg4nP0K5lS4Mkage+LAMIQ8dImkLR53PpebXICA==} + + '@next/swc-darwin-arm64@15.4.0-canary.86': + resolution: {integrity: sha512-1ofBmzjPkmoMdM+dXvybZ/Roq8HRo0sFzcwXk7/FJNOufuwyK+QKdSpLE7pHlPR7ZREqfEMj61ONO+gAK+zOJw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@next/swc-darwin-x64@15.4.0-canary.86': + resolution: {integrity: sha512-WCKSrllvwzYi4TgrSdgxKSOF2nhieeaWWOeGucn0OXy50uOAamr0HwP5OaIBCx3oRar4w66gvs4IrdTdMedeJA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@next/swc-linux-arm64-gnu@15.4.0-canary.86': + resolution: {integrity: sha512-8qn7DJVNFjhEIDo2ts0YCsO7g+vJjPWh8Ur8lBK3XspeX0BPsF4s+YmgidrpzRXeIfoo2uYLkkXcy/57CVDblw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@next/swc-linux-arm64-musl@15.4.0-canary.86': + resolution: {integrity: sha512-8MTn6N4Ja25neMLu2Bra1lqW9AWPqsYg0BVs5M/cxL0QkcN3mak/8LLX1vbzz7GigMGSA+NLwg+ol8lglfgIGA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@next/swc-linux-x64-gnu@15.4.0-canary.86': + resolution: {integrity: sha512-hIhzDwWDQHnH0M0Pzaqs1c5fa4+LHiLLEBuPJQvhBxQfH+Eh86DWiWHDCaoNiURvdRPg6uCuF2MjwptrMplEkg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@next/swc-linux-x64-musl@15.4.0-canary.86': + resolution: {integrity: sha512-FG6SBuSeRWYMNu6tsfaZ4iDzv3BLxlpRncO2xvKKQPeUdDSQ0cehuHYnx8fRte8IOAJ3rlbRd6NXvrDarqu92Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@next/swc-win32-arm64-msvc@15.4.0-canary.86': + resolution: {integrity: sha512-3HvZo4VuyINrNYplRhvC8ILdKwi/vFDHOcTN/I4ru039TFpu2eO6VtXsLBdOdJjGslSSSBYkX+6yRrghihAZDA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@next/swc-win32-x64-msvc@15.4.0-canary.86': + resolution: {integrity: sha512-UO9JzGGj7GhtSJFdI0Bl0dkIIBfgbhXLsgNVmq9Z/CsUsQB6J9RS/BMhsxfVwhO+RETk13nFpNutMAhAwcuD8w==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@ngtools/webpack@16.2.16': + resolution: {integrity: sha512-4gm2allK0Pjy/Lxb9IGRnhEZNEOJSOTWwy09VOdHouV2ODRK7Tto2LgteaFJUUSLkuvWRsI7pfuA6yrz8KDfHw==} + engines: {node: ^16.14.0 || >=18.10.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + peerDependencies: + '@angular/compiler-cli': ^16.0.0 + typescript: '>=4.9.3 <5.2' + webpack: ^5.54.0 + + '@ngx-translate/core@15.0.0': + resolution: {integrity: sha512-Am5uiuR0bOOxyoercDnAA3rJVizo4RRqJHo8N3RqJ+XfzVP/I845yEnMADykOHvM6HkVm4SZSnJBOiz0Anx5BA==} + engines: {node: ^16.13.0 || >=18.10.0} + peerDependencies: + '@angular/common': '>=16.0.0' + '@angular/core': '>=16.0.0' + rxjs: ^6.5.5 || ^7.4.0 + + '@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1': + resolution: {integrity: sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@nolyfill/is-core-module@1.0.39': + resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} + engines: {node: '>=12.4.0'} + + '@npmcli/fs@2.1.2': + resolution: {integrity: sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + '@npmcli/fs@3.1.1': + resolution: {integrity: sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + '@npmcli/git@4.1.0': + resolution: {integrity: sha512-9hwoB3gStVfa0N31ymBmrX+GuDGdVA/QWShZVqE0HK2Af+7QGGrCTbZia/SW0ImUTjTne7SP91qxDmtXvDHRPQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + '@npmcli/installed-package-contents@2.1.0': + resolution: {integrity: sha512-c8UuGLeZpm69BryRykLuKRyKFZYJsZSCT4aVY5ds4omyZqJ172ApzgfKJ5eV/r3HgLdUYgFVe54KSFVjKoe27w==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + hasBin: true + + '@npmcli/move-file@2.0.1': + resolution: {integrity: sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + deprecated: This functionality has been moved to @npmcli/fs + + '@npmcli/node-gyp@3.0.0': + resolution: {integrity: sha512-gp8pRXC2oOxu0DUE1/M3bYtb1b3/DbJ5aM113+XJBgfXdussRAsX0YOrOhdd8WvnAR6auDBvJomGAkLKA5ydxA==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + '@npmcli/promise-spawn@6.0.2': + resolution: {integrity: sha512-gGq0NJkIGSwdbUt4yhdF8ZrmkGKVz9vAdVzpOfnom+V8PLSmSOVhZwbNvZZS1EYcJN5hzzKBxmmVVAInM6HQLg==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + '@npmcli/run-script@6.0.2': + resolution: {integrity: sha512-NCcr1uQo1k5U+SYlnIrbAh3cxy+OQT1VtqiAbxdymSlptbzBb62AjH2xXgjNCoP073hoa1CfCAcwoZ8k96C4nA==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + '@nrwl/devkit@16.5.1': + resolution: {integrity: sha512-NB+DE/+AFJ7lKH/WBFyatJEhcZGj25F24ncDkwjZ6MzEiSOGOJS0LaV/R+VUsmS5EHTPXYOpn3zHWWAcJhyOmA==} + + '@nrwl/tao@16.5.1': + resolution: {integrity: sha512-x+gi/fKdM6uQNIti9exFlm3V5LBP3Y8vOEziO42HdOigyrXa0S0HD2WMpccmp6PclYKhwEDUjKJ39xh5sdh4Ig==} + hasBin: true + + '@nx/devkit@16.5.1': + resolution: {integrity: sha512-T1acZrVVmJw/sJ4PIGidCBYBiBqlg/jT9e8nIGXLSDS20xcLvfo4zBQf8UZLrmHglnwwpDpOWuVJCp2rYA5aDg==} + peerDependencies: + nx: '>= 15 <= 17' + + '@nx/nx-darwin-arm64@16.5.1': + resolution: {integrity: sha512-q98TFI4B/9N9PmKUr1jcbtD4yAFs1HfYd9jUXXTQOlfO9SbDjnrYJgZ4Fp9rMNfrBhgIQ4x1qx0AukZccKmH9Q==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@nx/nx-darwin-x64@16.5.1': + resolution: {integrity: sha512-j9HmL1l8k7EVJ3eOM5y8COF93gqrydpxCDoz23ZEtsY+JHY77VAiRQsmqBgEx9GGA2dXi9VEdS67B0+1vKariw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@nx/nx-freebsd-x64@16.5.1': + resolution: {integrity: sha512-CXSPT01aVS869tvCCF2tZ7LnCa8l41wJ3mTVtWBkjmRde68E5Up093hklRMyXb3kfiDYlfIKWGwrV4r0eH6x1A==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@nx/nx-linux-arm-gnueabihf@16.5.1': + resolution: {integrity: sha512-BhrumqJSZCWFfLFUKl4CAUwR0Y0G2H5EfFVGKivVecEQbb+INAek1aa6c89evg2/OvetQYsJ+51QknskwqvLsA==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@nx/nx-linux-arm64-gnu@16.5.1': + resolution: {integrity: sha512-x7MsSG0W+X43WVv7JhiSq2eKvH2suNKdlUHEG09Yt0vm3z0bhtym1UCMUg3IUAK7jy9hhLeDaFVFkC6zo+H/XQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@nx/nx-linux-arm64-musl@16.5.1': + resolution: {integrity: sha512-J+/v/mFjOm74I0PNtH5Ka+fDd+/dWbKhpcZ2R1/6b9agzZk+Ff/SrwJcSYFXXWKbPX+uQ4RcJoytT06Zs3s0ow==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@nx/nx-linux-x64-gnu@16.5.1': + resolution: {integrity: sha512-igooWJ5YxQ94Zft7IqgL+Lw0qHaY15Btw4gfK756g/YTYLZEt4tTvR1y6RnK/wdpE3sa68bFTLVBNCGTyiTiDQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@nx/nx-linux-x64-musl@16.5.1': + resolution: {integrity: sha512-zF/exnPqFYbrLAduGhTmZ7zNEyADid2bzNQiIjJkh8Y6NpDwrQIwVIyvIxqynsjMrIs51kBH+8TUjKjj2Jgf5A==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@nx/nx-win32-arm64-msvc@16.5.1': + resolution: {integrity: sha512-qtqiLS9Y9TYyAbbpq58kRoOroko4ZXg5oWVqIWFHoxc5bGPweQSJCROEqd1AOl2ZDC6BxfuVHfhDDop1kK05WA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@nx/nx-win32-x64-msvc@16.5.1': + resolution: {integrity: sha512-kUJBLakK7iyA9WfsGGQBVennA4jwf5XIgm0lu35oMOphtZIluvzItMt0EYBmylEROpmpEIhHq0P6J9FA+WH0Rg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@otplib/core@12.0.1': + resolution: {integrity: sha512-4sGntwbA/AC+SbPhbsziRiD+jNDdIzsZ3JUyfZwjtKyc/wufl1pnSIaG4Uqx8ymPagujub0o92kgBnB89cuAMA==} + + '@otplib/plugin-crypto@12.0.1': + resolution: {integrity: sha512-qPuhN3QrT7ZZLcLCyKOSNhuijUi9G5guMRVrxq63r9YNOxxQjPm59gVxLM+7xGnHnM6cimY57tuKsjK7y9LM1g==} + + '@otplib/plugin-thirty-two@12.0.1': + resolution: {integrity: sha512-MtT+uqRso909UkbrrYpJ6XFjj9D+x2Py7KjTO9JDPhL0bJUYVu5kFP4TFZW4NFAywrAtFRxOVY261u0qwb93gA==} + + '@oxc-resolver/binding-android-arm-eabi@11.6.0': + resolution: {integrity: sha512-UJTf5uZs919qavt9Btvbzkr3eaUu4d+FXBri8AB2BtOezriaTTUvArab2K9fdACQ4yFggTD5ews1l19V/6SW2Q==} + cpu: [arm] + os: [android] + + '@oxc-resolver/binding-android-arm64@11.6.0': + resolution: {integrity: sha512-v17j1WLEAIlyc+6JOWPXcky7dkU3fN8nHTP8KSK05zkkBO0t28R3Q0udmNBiJtVSnw4EFB/fy/3Mu2ItpG6bVQ==} + cpu: [arm64] + os: [android] + + '@oxc-resolver/binding-darwin-arm64@11.6.0': + resolution: {integrity: sha512-ZrU+qd5AKe8s7PZDLCHY23UpbGn1RAkcNd4JYjOTnX22XEjSqLvyC6pCMngTyfgGVJ4zXFubBkRzt/k3xOjNlQ==} + cpu: [arm64] + os: [darwin] + + '@oxc-resolver/binding-darwin-x64@11.6.0': + resolution: {integrity: sha512-qBIlX0X0RSxQHcXQnFpBGKxrDVtj7OdpWFGmrcR3NcndVjZ/wJRPST5uTTM83NfsHyuUeOi/vRZjmDrthvhnSQ==} + cpu: [x64] + os: [darwin] + + '@oxc-resolver/binding-freebsd-x64@11.6.0': + resolution: {integrity: sha512-tTyMlHHNhbkq/oEP/fM8hPZ6lqntHIz6EfOt577/lslrwxC5a/ii0lOOHjPuQtkurpyUBWYPs7Z17EgrZulc4Q==} + cpu: [x64] + os: [freebsd] + + '@oxc-resolver/binding-linux-arm-gnueabihf@11.6.0': + resolution: {integrity: sha512-tYinHy5k9/rujo21mG2jZckJJD7fsceNDl5HOl/eh5NPjSt2vXQv181PVKeITw3+3i+gI1d666w5EtgpiCegRA==} + cpu: [arm] + os: [linux] + + '@oxc-resolver/binding-linux-arm-musleabihf@11.6.0': + resolution: {integrity: sha512-aOlGlSiT9fBgSyiIWvSxbyzaBx3XrgCy6UJRrqBkIvMO9D7W90JmV0RsiLua4w43zJSSrfuQQWqmFCwgIib3Iw==} + cpu: [arm] + os: [linux] + + '@oxc-resolver/binding-linux-arm64-gnu@11.6.0': + resolution: {integrity: sha512-EZ/OuxZA9qQoAANBDb9V4krfYXU3MC+LZ9qY+cE0yMYMIxm7NT5AdR0OaRQqfa3tWIbina1VF7FaMR6rpKvmlA==} + cpu: [arm64] + os: [linux] + + '@oxc-resolver/binding-linux-arm64-musl@11.6.0': + resolution: {integrity: sha512-NpF7sID4NnPetpqDk2eOu6TPUt381Qlpos8nGDcSkAluqSsSGFOPfETEB5VbJeqNVQbepEQX9mOxZygFpW0+nA==} + cpu: [arm64] + os: [linux] + + '@oxc-resolver/binding-linux-ppc64-gnu@11.6.0': + resolution: {integrity: sha512-Sqn9Ha4rxCCpjpfkFi9f9y9phsaBnseaKw+JqHgBQoNMToe+/20A1jwIu9OX+484UuLpduM+wLydgngjnoi7Dg==} + cpu: [ppc64] + os: [linux] + + '@oxc-resolver/binding-linux-riscv64-gnu@11.6.0': + resolution: {integrity: sha512-eFoNcPhImp1FLAQf5U3Nlph4WNWEsdWohSThSTtKPrX+jhPZiVsj3iBC9gjaRwq2Ez4QhP1x7/PSL6mtKnS6rw==} + cpu: [riscv64] + os: [linux] + + '@oxc-resolver/binding-linux-riscv64-musl@11.6.0': + resolution: {integrity: sha512-WQw3CT10aJg7SIc/X1QPrh6lTx2wOLg5IaCu/+Mqlxf1nZBEW3+tV/+y3PzXG0MCRhq7FDTiHaW8MBVAwBineQ==} + cpu: [riscv64] + os: [linux] + + '@oxc-resolver/binding-linux-s390x-gnu@11.6.0': + resolution: {integrity: sha512-p5qcPr/EtGJ2PpeeArL3ifZU/YljWLypeu38+e19z2dyPv8Aoby8tjM+D1VTI8+suMwTkseyove/uu6zIUiqRw==} + cpu: [s390x] + os: [linux] + + '@oxc-resolver/binding-linux-x64-gnu@11.6.0': + resolution: {integrity: sha512-/9M/ieoY5v54k3UjtF9Vw43WQ4bBfed+qRL1uIpFbZcO2qi5aXwVMYnjSd/BoaRtDs5JFV9iOjzHwpw0zdOYZA==} + cpu: [x64] + os: [linux] + + '@oxc-resolver/binding-linux-x64-musl@11.6.0': + resolution: {integrity: sha512-HMtWWHTU7zbwceTFZPAPMMhhWR1nNO2OR60r6i55VprCMvttTWPQl7uLP0AUtAPoU9B/2GqP48rzOuaaKhHnYw==} + cpu: [x64] + os: [linux] + + '@oxc-resolver/binding-wasm32-wasi@11.6.0': + resolution: {integrity: sha512-rDAwr2oqmnG/6LSZJwvO3Bmt/RC3/Q6myyaUmg3P7GhZDyFPrWJONB7NFhPwU2Q4JIpA73ST4LBdhzmGxMTmrw==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@oxc-resolver/binding-win32-arm64-msvc@11.6.0': + resolution: {integrity: sha512-COzy8weljZo2lObWl6ZzW6ypDx1v1rtLdnt7JPjTUARikK1gMzlz9kouQhCtCegNFILx2L2oWw7714fnchqujw==} + cpu: [arm64] + os: [win32] + + '@oxc-resolver/binding-win32-ia32-msvc@11.6.0': + resolution: {integrity: sha512-p2tMRdi91CovjLBApDPD/uEy1/5r7U6iVkfagLYDytgvj6nJ1EAxLUdXbhoe6//50IvDC/5I51nGCdxmOUiXlQ==} + cpu: [ia32] + os: [win32] + + '@oxc-resolver/binding-win32-x64-msvc@11.6.0': + resolution: {integrity: sha512-p6b9q5TACd/y39kDK2HENXqd4lThoVrTkxdvizqd5/VwyHcoSd0cDcIEhHpxvfjc83VsODCBgB/zcjp//TlaqA==} + cpu: [x64] + os: [win32] + + '@parcel/watcher-android-arm64@2.5.1': + resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + + '@parcel/watcher-darwin-arm64@2.5.1': + resolution: {integrity: sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + + '@parcel/watcher-darwin-x64@2.5.1': + resolution: {integrity: sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + + '@parcel/watcher-freebsd-x64@2.5.1': + resolution: {integrity: sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + + '@parcel/watcher-linux-arm-glibc@2.5.1': + resolution: {integrity: sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm-musl@2.5.1': + resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm64-glibc@2.5.1': + resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-arm64-musl@2.5.1': + resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-x64-glibc@2.5.1': + resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-linux-x64-musl@2.5.1': + resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-win32-arm64@2.5.1': + resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-ia32@2.5.1': + resolution: {integrity: sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==} + engines: {node: '>= 10.0.0'} + cpu: [ia32] + os: [win32] + + '@parcel/watcher-win32-x64@2.5.1': + resolution: {integrity: sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + + '@parcel/watcher@2.0.4': + resolution: {integrity: sha512-cTDi+FUDBIUOBKEtj+nhiJ71AZVlkAsQFuGQTun5tV9mwQBQgZvhCzG+URPQc8myeN32yRVZEfVAPCs1RW+Jvg==} + engines: {node: '>= 10.0.0'} + + '@parcel/watcher@2.5.1': + resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} + engines: {node: '>= 10.0.0'} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@playwright/test@1.54.1': + resolution: {integrity: sha512-FS8hQ12acieG2dYSksmLOF7BNxnVf2afRJdCuM1eMSxj6QTSE6G4InGF7oApGgDb65MX7AwMVlIkpru0yZA4Xw==} + engines: {node: '>=18'} + hasBin: true + + '@pnpm/config.env-replace@1.1.0': + resolution: {integrity: sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==} + engines: {node: '>=12.22.0'} + + '@pnpm/network.ca-file@1.0.2': + resolution: {integrity: sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==} + engines: {node: '>=12.22.0'} + + '@pnpm/npm-conf@2.3.1': + resolution: {integrity: sha512-c83qWb22rNRuB0UaVCI0uRPNRr8Z0FWnEIvT47jiHAmOIUHbBOg5XvV7pM5x+rKn9HRpjxquDbXYSXr3fAKFcw==} + engines: {node: '>=12'} + + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.4': + resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} + + '@protobufjs/eventemitter@1.1.0': + resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} + + '@protobufjs/fetch@1.1.0': + resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/inquire@1.1.0': + resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.0': + resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + + '@puppeteer/browsers@2.3.0': + resolution: {integrity: sha512-ioXoq9gPxkss4MYhD+SFaU9p1IHFUX0ILAWFPyjGaBdjLsYAlZw6j1iLA0N/m12uVHLFDfSYNF7EQccjinIMDA==} + engines: {node: '>=18'} + hasBin: true + + '@radix-ui/number@1.1.0': + resolution: {integrity: sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==} + + '@radix-ui/number@1.1.1': + resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} + + '@radix-ui/primitive@1.1.1': + resolution: {integrity: sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==} + + '@radix-ui/primitive@1.1.2': + resolution: {integrity: sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==} + + '@radix-ui/react-arrow@1.1.1': + resolution: {integrity: sha512-NaVpZfmv8SKeZbn4ijN2V3jlHA9ngBG16VnIIm22nUR0Yk8KUALyBxT3KYEUnNuch9sTE8UTsS3whzBgKOL30w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-arrow@1.1.2': + resolution: {integrity: sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-arrow@1.1.7': + resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-avatar@1.1.2': + resolution: {integrity: sha512-GaC7bXQZ5VgZvVvsJ5mu/AEbjYLnhhkoidOboC50Z6FFlLA03wG2ianUoH+zgDQ31/9gCF59bE4+2bBgTyMiig==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-checkbox@1.1.3': + resolution: {integrity: sha512-HD7/ocp8f1B3e6OHygH0n7ZKjONkhciy1Nh0yuBgObqThc3oyx+vuMfFHKAknXRHHWVE9XvXStxJFyjUmB8PIw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collection@1.1.7': + resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-compose-refs@1.1.1': + resolution: {integrity: sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-compose-refs@1.1.2': + resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-context@1.1.1': + resolution: {integrity: sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-context@1.1.2': + resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-direction@1.1.0': + resolution: {integrity: sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-direction@1.1.1': + resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dismissable-layer@1.1.10': + resolution: {integrity: sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-dismissable-layer@1.1.3': + resolution: {integrity: sha512-onrWn/72lQoEucDmJnr8uczSNTujT0vJnA/X5+3AkChVPowr8n1yvIKIabhWyMQeMvvmdpsvcyDqx3X1LEXCPg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-dismissable-layer@1.1.5': + resolution: {integrity: sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-focus-guards@1.1.1': + resolution: {integrity: sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-focus-guards@1.1.2': + resolution: {integrity: sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-focus-scope@1.1.2': + resolution: {integrity: sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-focus-scope@1.1.7': + resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-hover-card@1.1.14': + resolution: {integrity: sha512-CPYZ24Mhirm+g6D8jArmLzjYu4Eyg3TTUHswR26QgzXBHBe64BO/RHOJKzmF/Dxb4y4f9PKyJdwm/O/AhNkb+Q==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-id@1.1.0': + resolution: {integrity: sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-id@1.1.1': + resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-popover@1.1.6': + resolution: {integrity: sha512-NQouW0x4/GnkFJ/pRqsIS3rM/k97VzKnVb2jB7Gq7VEGPy5g7uNV1ykySFt7eWSp3i2uSGFwaJcvIRJBAHmmFg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popper@1.2.1': + resolution: {integrity: sha512-3kn5Me69L+jv82EKRuQCXdYyf1DqHwD2U/sxoNgBGCB7K9TRc3bQamQ+5EPM9EvyPdli0W41sROd+ZU1dTCztw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popper@1.2.2': + resolution: {integrity: sha512-Rvqc3nOpwseCyj/rgjlJDYAgyfw7OC1tTkKn2ivhaMGcYt8FSBlahHOZak2i3QwkRXUXgGgzeEe2RuqeEHuHgA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popper@1.2.7': + resolution: {integrity: sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-portal@1.1.3': + resolution: {integrity: sha512-NciRqhXnGojhT93RPyDaMPfLH3ZSl4jjIFbZQ1b/vxvZEdHsBZ49wP9w8L3HzUQwep01LcWtkUvm0OVB5JAHTw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-portal@1.1.4': + resolution: {integrity: sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-portal@1.1.9': + resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-presence@1.1.2': + resolution: {integrity: sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-presence@1.1.4': + resolution: {integrity: sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@2.0.1': + resolution: {integrity: sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@2.0.2': + resolution: {integrity: sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@2.1.3': + resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-roving-focus@1.1.10': + resolution: {integrity: sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-scroll-area@1.2.2': + resolution: {integrity: sha512-EFI1N/S3YxZEW/lJ/H1jY3njlvTd8tBmgKEn4GHi51+aMm94i6NmAJstsm5cu3yJwYqYc93gpCPm21FeAbFk6g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slot@1.1.1': + resolution: {integrity: sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-slot@1.1.2': + resolution: {integrity: sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-slot@1.2.3': + resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-tabs@1.1.12': + resolution: {integrity: sha512-GTVAlRVrQrSw3cEARM0nAx73ixrWDPNZAruETn3oHCNP6SbZ/hNxdxp+u7VkIEv3/sFoLq1PfcHrl7Pnp0CDpw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-tooltip@1.1.6': + resolution: {integrity: sha512-TLB5D8QLExS1uDn7+wH/bjEmRurNMTzNrtq7IjaS4kjion9NtzsTGkvR5+i7yc9q01Pi2KMM2cN3f8UG4IvvXA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-tooltip@1.2.7': + resolution: {integrity: sha512-Ap+fNYwKTYJ9pzqW+Xe2HtMRbQ/EeWkj2qykZ6SuEV4iS/o1bZI5ssJbk4D2r8XuDuOBVz/tIx2JObtuqU+5Zw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-use-callback-ref@1.1.0': + resolution: {integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-callback-ref@1.1.1': + resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-controllable-state@1.1.0': + resolution: {integrity: sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-controllable-state@1.2.2': + resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-effect-event@0.0.2': + resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-escape-keydown@1.1.0': + resolution: {integrity: sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-escape-keydown@1.1.1': + resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.1.0': + resolution: {integrity: sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.1.1': + resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-previous@1.1.0': + resolution: {integrity: sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-rect@1.1.0': + resolution: {integrity: sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-rect@1.1.1': + resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-size@1.1.0': + resolution: {integrity: sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-size@1.1.1': + resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-visually-hidden@1.1.1': + resolution: {integrity: sha512-vVfA2IZ9q/J+gEamvj761Oq1FpWgCDaNOOIfbPVp2MVPLEomUr5+Vf7kJGwQ24YxZSlQVar7Bes8kyTo5Dshpg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-visually-hidden@1.2.3': + resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/rect@1.1.0': + resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==} + + '@radix-ui/rect@1.1.1': + resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + + '@react-aria/focus@3.20.5': + resolution: {integrity: sha512-JpFtXmWQ0Oca7FcvkqgjSyo6xEP7v3oQOLUId6o0xTvm4AD5W0mU2r3lYrbhsJ+XxdUUX4AVR5473sZZ85kU4A==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@react-aria/interactions@3.25.3': + resolution: {integrity: sha512-J1bhlrNtjPS/fe5uJQ+0c7/jiXniwa4RQlP+Emjfc/iuqpW2RhbF9ou5vROcLzWIyaW8tVMZ468J68rAs/aZ5A==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@react-aria/ssr@3.9.9': + resolution: {integrity: sha512-2P5thfjfPy/np18e5wD4WPt8ydNXhij1jwA8oehxZTFqlgVMGXzcWKxTb4RtJrLFsqPO7RUQTiY8QJk0M4Vy2g==} + engines: {node: '>= 12'} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@react-aria/utils@3.29.1': + resolution: {integrity: sha512-yXMFVJ73rbQ/yYE/49n5Uidjw7kh192WNN9PNQGV0Xoc7EJUlSOxqhnpHmYTyO0EotJ8fdM1fMH8durHjUSI8g==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@react-stately/flags@3.1.2': + resolution: {integrity: sha512-2HjFcZx1MyQXoPqcBGALwWWmgFVUk2TuKVIQxCbRq7fPyWXIl6VHcakCLurdtYC2Iks7zizvz0Idv48MQ38DWg==} + + '@react-stately/utils@3.10.7': + resolution: {integrity: sha512-cWvjGAocvy4abO9zbr6PW6taHgF24Mwy/LbQ4TC4Aq3tKdKDntxyD+sh7AkSRfJRT2ccMVaHVv2+FfHThd3PKQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@react-types/shared@3.30.0': + resolution: {integrity: sha512-COIazDAx1ncDg046cTJ8SFYsX8aS3lB/08LDnbkH/SkdYrFPWDlXMrO/sUam8j1WWM+PJ+4d1mj7tODIKNiFog==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + '@redocly/ajv@8.11.2': + resolution: {integrity: sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==} + + '@redocly/config@0.22.2': + resolution: {integrity: sha512-roRDai8/zr2S9YfmzUfNhKjOF0NdcOIqF7bhf4MVC5UxpjIysDjyudvlAiVbpPHp3eDRWbdzUgtkK1a7YiDNyQ==} + + '@redocly/openapi-core@1.34.3': + resolution: {integrity: sha512-3arRdUp1fNx55itnjKiUhO6t4Mf91TsrTIYINDNLAZPS0TPd5YpiXRctwjel0qqWoOOhjA34cZ3m4dksLDFUYg==} + engines: {node: '>=18.17.0', npm: '>=9.5.0'} + + '@reduxjs/toolkit@1.9.7': + resolution: {integrity: sha512-t7v8ZPxhhKgOKtU+uyJT13lu4vL7az5aFi4IdoDs/eS548edn2M8Ik9h8fxgvMjGoAUVFSt6ZC1P5cWmQ014QQ==} + peerDependencies: + react: ^16.9.0 || ^17.0.0 || ^18 + react-redux: ^7.2.1 || ^8.0.2 + peerDependenciesMeta: + react: + optional: true + react-redux: + optional: true + + '@rolldown/pluginutils@1.0.0-beta.27': + resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + + '@rollup/rollup-android-arm-eabi@4.45.1': + resolution: {integrity: sha512-NEySIFvMY0ZQO+utJkgoMiCAjMrGvnbDLHvcmlA33UXJpYBCvlBEbMMtV837uCkS+plG2umfhn0T5mMAxGrlRA==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.45.1': + resolution: {integrity: sha512-ujQ+sMXJkg4LRJaYreaVx7Z/VMgBBd89wGS4qMrdtfUFZ+TSY5Rs9asgjitLwzeIbhwdEhyj29zhst3L1lKsRQ==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.45.1': + resolution: {integrity: sha512-FSncqHvqTm3lC6Y13xncsdOYfxGSLnP+73k815EfNmpewPs+EyM49haPS105Rh4aF5mJKywk9X0ogzLXZzN9lA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.45.1': + resolution: {integrity: sha512-2/vVn/husP5XI7Fsf/RlhDaQJ7x9zjvC81anIVbr4b/f0xtSmXQTFcGIQ/B1cXIYM6h2nAhJkdMHTnD7OtQ9Og==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.45.1': + resolution: {integrity: sha512-4g1kaDxQItZsrkVTdYQ0bxu4ZIQ32cotoQbmsAnW1jAE4XCMbcBPDirX5fyUzdhVCKgPcrwWuucI8yrVRBw2+g==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.45.1': + resolution: {integrity: sha512-L/6JsfiL74i3uK1Ti2ZFSNsp5NMiM4/kbbGEcOCps99aZx3g8SJMO1/9Y0n/qKlWZfn6sScf98lEOUe2mBvW9A==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.45.1': + resolution: {integrity: sha512-RkdOTu2jK7brlu+ZwjMIZfdV2sSYHK2qR08FUWcIoqJC2eywHbXr0L8T/pONFwkGukQqERDheaGTeedG+rra6Q==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.45.1': + resolution: {integrity: sha512-3kJ8pgfBt6CIIr1o+HQA7OZ9mp/zDk3ctekGl9qn/pRBgrRgfwiffaUmqioUGN9hv0OHv2gxmvdKOkARCtRb8Q==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.45.1': + resolution: {integrity: sha512-k3dOKCfIVixWjG7OXTCOmDfJj3vbdhN0QYEqB+OuGArOChek22hn7Uy5A/gTDNAcCy5v2YcXRJ/Qcnm4/ma1xw==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.45.1': + resolution: {integrity: sha512-PmI1vxQetnM58ZmDFl9/Uk2lpBBby6B6rF4muJc65uZbxCs0EA7hhKCk2PKlmZKuyVSHAyIw3+/SiuMLxKxWog==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loongarch64-gnu@4.45.1': + resolution: {integrity: sha512-9UmI0VzGmNJ28ibHW2GpE2nF0PBQqsyiS4kcJ5vK+wuwGnV5RlqdczVocDSUfGX/Na7/XINRVoUgJyFIgipoRg==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-powerpc64le-gnu@4.45.1': + resolution: {integrity: sha512-7nR2KY8oEOUTD3pBAxIBBbZr0U7U+R9HDTPNy+5nVVHDXI4ikYniH1oxQz9VoB5PbBU1CZuDGHkLJkd3zLMWsg==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.45.1': + resolution: {integrity: sha512-nlcl3jgUultKROfZijKjRQLUu9Ma0PeNv/VFHkZiKbXTBQXhpytS8CIj5/NfBeECZtY2FJQubm6ltIxm/ftxpw==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.45.1': + resolution: {integrity: sha512-HJV65KLS51rW0VY6rvZkiieiBnurSzpzore1bMKAhunQiECPuxsROvyeaot/tcK3A3aGnI+qTHqisrpSgQrpgA==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.45.1': + resolution: {integrity: sha512-NITBOCv3Qqc6hhwFt7jLV78VEO/il4YcBzoMGGNxznLgRQf43VQDae0aAzKiBeEPIxnDrACiMgbqjuihx08OOw==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.45.1': + resolution: {integrity: sha512-+E/lYl6qu1zqgPEnTrs4WysQtvc/Sh4fC2nByfFExqgYrqkKWp1tWIbe+ELhixnenSpBbLXNi6vbEEJ8M7fiHw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.45.1': + resolution: {integrity: sha512-a6WIAp89p3kpNoYStITT9RbTbTnqarU7D8N8F2CV+4Cl9fwCOZraLVuVFvlpsW0SbIiYtEnhCZBPLoNdRkjQFw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-win32-arm64-msvc@4.45.1': + resolution: {integrity: sha512-T5Bi/NS3fQiJeYdGvRpTAP5P02kqSOpqiopwhj0uaXB6nzs5JVi2XMJb18JUSKhCOX8+UE1UKQufyD6Or48dJg==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.45.1': + resolution: {integrity: sha512-lxV2Pako3ujjuUe9jiU3/s7KSrDfH6IgTSQOnDWr9aJ92YsFd7EurmClK0ly/t8dzMkDtd04g60WX6yl0sGfdw==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.45.1': + resolution: {integrity: sha512-M/fKi4sasCdM8i0aWJjCSFm2qEnYRR8AMLG2kxp6wD13+tMGA4Z1tVAuHkNRjud5SW2EM3naLuK35w9twvf6aA==} + cpu: [x64] + os: [win32] + + '@rspack/binding-darwin-arm64@1.4.9': + resolution: {integrity: sha512-P0O10aXEaLLrwKXK7muSXl64wGJsLGbJEE97zeFe0mFVFo44m3iVC+KVpRpBFBrXhnL1ylCYsu2mS/dTJ+970g==} + cpu: [arm64] + os: [darwin] + + '@rspack/binding-darwin-x64@1.4.9': + resolution: {integrity: sha512-eCbjVEkrSpFzLYye8Xd3SJgoaJ+GXCEVXJNLIqqt+BwxAknuVcHOHWFtppCw5/FcPWZkB03fWMah7aW8/ZqDyg==} + cpu: [x64] + os: [darwin] + + '@rspack/binding-linux-arm64-gnu@1.4.9': + resolution: {integrity: sha512-OTsco8WagOax9o6W66i//GjgrjhNFFOXhcS/vl81t7Hx5APEpEXX+pnccirH0e67Gs5sNlm/uLVS1cyA/B77Sg==} + cpu: [arm64] + os: [linux] + + '@rspack/binding-linux-arm64-musl@1.4.9': + resolution: {integrity: sha512-vxnh8TwTX5tquZz8naGd1NIBOESyKAPRemHZUWfAnK1p4WzM+dbTkGeIU7Z1fUzF/AXEbdRQ/omWlvp5nCOOZA==} + cpu: [arm64] + os: [linux] + + '@rspack/binding-linux-x64-gnu@1.4.9': + resolution: {integrity: sha512-MitSilaS23e7EPNqYT9PEr2Zomc51GZSaCRCXscNOica5V/oAVBcEMUFbrNoD4ugohDXM68RvK0kVyFmfYuW+Q==} + cpu: [x64] + os: [linux] + + '@rspack/binding-linux-x64-musl@1.4.9': + resolution: {integrity: sha512-fdBLz3RPvEEaz91IHXP4pMDNh9Nfl6nkYDmmLBJRu4yHi97j1BEeymrq3lKssy/1kDR70t6T47ZjfRJIgM6nYg==} + cpu: [x64] + os: [linux] + + '@rspack/binding-wasm32-wasi@1.4.9': + resolution: {integrity: sha512-yWd5llZHBCsA0S5W0UGuXdQQ5zkZC4PQbOQS7XiblBII9RIMZZKJV/3AsYAHUeskTBPnwYMQsm8QCV52BNAE9A==} + cpu: [wasm32] + + '@rspack/binding-win32-arm64-msvc@1.4.9': + resolution: {integrity: sha512-3+oG19ye2xOmVGGKHao0EXmvPaiGvaFnxJRQ6tc6T7MSxhOvvDhQ1zmx+9X/wXKv/iytAHXMuoLGLHwdGd7GJg==} + cpu: [arm64] + os: [win32] + + '@rspack/binding-win32-ia32-msvc@1.4.9': + resolution: {integrity: sha512-l9K68LNP2j2QnCFYz17Rea7wdk04m4jnGB6CyRrS0iuanTn+Hvz3wgAn1fqADJxE4dtX+wNbTPOWJr0SrVHccw==} + cpu: [ia32] + os: [win32] + + '@rspack/binding-win32-x64-msvc@1.4.9': + resolution: {integrity: sha512-2i4+/E5HjqobNBA86DuqQfqw6mW/jsHGUzUfgwKEKW8I6wLU0Gz7dUcz0fExvr8W5I8f/ccOfqR2bPGnxJ8vNw==} + cpu: [x64] + os: [win32] + + '@rspack/binding@1.4.9': + resolution: {integrity: sha512-9EY8OMCNZrwCupQMZccMgrTxWGUQvZGFrLFw/rxfTt+uT4fS4CAbNwHVFxsnROaRd+EE6EXfUpUYu66j6vd4qA==} + + '@rspack/core@1.4.9': + resolution: {integrity: sha512-fHEGOzVcyESVfprFTqgeJ7vAnmkmY/nbljaeGsJY4zLmROmkbGTh4xgLEY3O5nEukLfEFbdLapvBqYb5tE/fmA==} + engines: {node: '>=16.0.0'} + peerDependencies: + '@swc/helpers': '>=0.5.1' + peerDependenciesMeta: + '@swc/helpers': + optional: true + + '@rspack/lite-tapable@1.0.1': + resolution: {integrity: sha512-VynGOEsVw2s8TAlLf/uESfrgfrq2+rcXB1muPJYBWbsm1Oa6r5qVQhjA5ggM6z/coYPrsVMgovl3Ff7Q7OCp1w==} + engines: {node: '>=16.0.0'} + + '@rtsao/scc@1.1.0': + resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + + '@rushstack/eslint-patch@1.12.0': + resolution: {integrity: sha512-5EwMtOqvJMMa3HbmxLlF74e+3/HhwBTMcvt3nqVJgGCozO6hzIPOBlwm8mGVNR9SN2IJpxSnlxczyDjcn7qIyw==} + + '@schematics/angular@16.2.16': + resolution: {integrity: sha512-V4cE4R5MbusKaNW9DWsisiSRUoQzbAaBIeJh42yCkg5H/lUdf18hUB7DG6Pl7yH6/tjzzz4SqIVD7N64uCDC2A==} + engines: {node: ^16.14.0 || >=18.10.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + + '@sideway/address@4.1.5': + resolution: {integrity: sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==} + + '@sideway/formula@3.0.1': + resolution: {integrity: sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==} + + '@sideway/pinpoint@2.0.0': + resolution: {integrity: sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==} + + '@signalwire/docusaurus-plugin-llms-txt@1.2.1': + resolution: {integrity: sha512-v/KluP6XMYOFKwsquFpcAbieSpj2mkoKij0GTZQHObrjMvCcJ6LADH0UnYUlHtkTbJTzglhH5wTgZ8ff3NwrVA==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@docusaurus/core': ^3.0.0 + + '@sigstore/bundle@1.1.0': + resolution: {integrity: sha512-PFutXEy0SmQxYI4texPw3dd2KewuNqv7OuK1ZFtY2fM754yhvG2KdgwIhRnoEE2uHdtdGNQ8s0lb94dW9sELog==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + '@sigstore/protobuf-specs@0.2.1': + resolution: {integrity: sha512-XTWVxnWJu+c1oCshMLwnKvz8ZQJJDVOlciMfgpJBQbThVjKTCG8dwyhgLngBD2KN0ap9F/gOV8rFDEx8uh7R2A==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + '@sigstore/sign@1.0.0': + resolution: {integrity: sha512-INxFVNQteLtcfGmcoldzV6Je0sbbfh9I16DM4yJPw3j5+TFP8X6uIiA18mvpEa9yyeycAKgPmOA3X9hVdVTPUA==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + '@sigstore/tuf@1.0.3': + resolution: {integrity: sha512-2bRovzs0nJZFlCN3rXirE4gwxCn97JNjMmwpecqlbgV9WcxX7WRuIrgzx/X7Ib7MYRbyUTpBYE0s2x6AmZXnlg==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + '@sinclair/typebox@0.27.8': + resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + + '@sindresorhus/is@4.6.0': + resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} + engines: {node: '>=10'} + + '@sindresorhus/is@5.6.0': + resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==} + engines: {node: '>=14.16'} + + '@slorber/react-helmet-async@1.3.0': + resolution: {integrity: sha512-e9/OK8VhwUSc67diWI8Rb3I0YgI9/SBQtnhe9aEuK6MhZm7ntZZimXgwXnd8W96YTmSOb9M4d8LwhRZyhWr/1A==} + peerDependencies: + react: ^16.6.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.6.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@slorber/remark-comment@1.0.0': + resolution: {integrity: sha512-RCE24n7jsOj1M0UPvIQCHTe7fI0sFL4S2nwKVWwHyVr/wI/H8GosgsJGyhnsZoGFnD/P2hLf1mSbrrgSLN93NA==} + + '@socket.io/component-emitter@3.1.2': + resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} + + '@svgr/babel-plugin-add-jsx-attribute@8.0.0': + resolution: {integrity: sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@svgr/babel-plugin-remove-jsx-attribute@8.0.0': + resolution: {integrity: sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0': + resolution: {integrity: sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0': + resolution: {integrity: sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@svgr/babel-plugin-svg-dynamic-title@8.0.0': + resolution: {integrity: sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@svgr/babel-plugin-svg-em-dimensions@8.0.0': + resolution: {integrity: sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@svgr/babel-plugin-transform-react-native-svg@8.1.0': + resolution: {integrity: sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@svgr/babel-plugin-transform-svg-component@8.0.0': + resolution: {integrity: sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw==} + engines: {node: '>=12'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@svgr/babel-preset@8.1.0': + resolution: {integrity: sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@svgr/core@8.1.0': + resolution: {integrity: sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==} + engines: {node: '>=14'} + + '@svgr/hast-util-to-babel-ast@8.0.0': + resolution: {integrity: sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q==} + engines: {node: '>=14'} + + '@svgr/plugin-jsx@8.1.0': + resolution: {integrity: sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA==} + engines: {node: '>=14'} + peerDependencies: + '@svgr/core': '*' + + '@svgr/plugin-svgo@8.1.0': + resolution: {integrity: sha512-Ywtl837OGO9pTLIN/onoWLmDQ4zFUycI1g76vuKGEz6evR/ZTJlJuz3G/fIkb6OVBJ2g0o6CGJzaEjfmEo3AHA==} + engines: {node: '>=14'} + peerDependencies: + '@svgr/core': '*' + + '@svgr/webpack@8.1.0': + resolution: {integrity: sha512-LnhVjMWyMQV9ZmeEy26maJk+8HTIbd59cH4F2MJ439k9DqejRisfFNGAPvRYlKETuh9LrImlS8aKsBgKjMA8WA==} + engines: {node: '>=14'} + + '@swc/core-darwin-arm64@1.13.1': + resolution: {integrity: sha512-zO6SW/jSMTUORPm6dUZFPUwf+EFWZsaXWMGXadRG6akCofYpoQb8pcY2QZkVr43z8TMka6BtXpyoD/DJ0iOPHQ==} + engines: {node: '>=10'} + cpu: [arm64] + os: [darwin] + + '@swc/core-darwin-x64@1.13.1': + resolution: {integrity: sha512-8RjaTZYxrlYKE5PgzZYWSOT4mAsyhIuh30Nu4dnn/2r0Ef68iNCbvX4ynGnFMhOIhqunjQbJf+mJKpwTwdHXhw==} + engines: {node: '>=10'} + cpu: [x64] + os: [darwin] + + '@swc/core-linux-arm-gnueabihf@1.13.1': + resolution: {integrity: sha512-jEqK6pECs2m4BpL2JA/4CCkq04p6iFOEtVNXTisO+lJ3zwmxlnIEm9UfJZG6VSu8GS9MHRKGB0ieZ1tEdN1qDA==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux] + + '@swc/core-linux-arm64-gnu@1.13.1': + resolution: {integrity: sha512-PbkuIOYXO/gQbWQ7NnYIwm59ygNqmUcF8LBeoKvxhx1VtOwE+9KiTfoplOikkPLhMiTzKsd8qentTslbITIg+Q==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + + '@swc/core-linux-arm64-musl@1.13.1': + resolution: {integrity: sha512-JaqFdBCarIBKiMu5bbAp+kWPMNGg97ej+7KzbKOzWP5pRptqKi86kCDZT3WmjPe8hNG6dvBwbm7Y8JNry5LebQ==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + + '@swc/core-linux-x64-gnu@1.13.1': + resolution: {integrity: sha512-t4cLkku10YECDaakWUH0452WJHIZtrLPRwezt6BdoMntVMwNjvXRX7C8bGuYcKC3YxRW7enZKFpozLhQIQ37oA==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + + '@swc/core-linux-x64-musl@1.13.1': + resolution: {integrity: sha512-fSMwZOaG+3ukUucbEbzz9GhzGhUhXoCPqHe9qW0/Vc2IZRp538xalygKyZynYweH5d9EHux1aj3+IO8/xBaoiA==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + + '@swc/core-win32-arm64-msvc@1.13.1': + resolution: {integrity: sha512-tweCXK/79vAwj1NhAsYgICy8T1z2QEairmN2BFEBYFBFNMEB1iI1YlXwBkBtuihRvgZrTh1ORusKa4jLYzLCZA==} + engines: {node: '>=10'} + cpu: [arm64] + os: [win32] + + '@swc/core-win32-ia32-msvc@1.13.1': + resolution: {integrity: sha512-zi7hO9D+2R2yQN9D7T10/CAI9KhuXkNkz8tcJOW6+dVPtAk/gsIC5NoGPELjgrAlLL9CS38ZQpLDslLfpP15ng==} + engines: {node: '>=10'} + cpu: [ia32] + os: [win32] + + '@swc/core-win32-x64-msvc@1.13.1': + resolution: {integrity: sha512-KubYjzqs/nz3H69ncX/XHKsC8c1xqc7UvonQAj26BhbL22HBsqdAaVutZ+Obho6RMpd3F5qQ95ldavUTWskRrw==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] + + '@swc/core@1.13.1': + resolution: {integrity: sha512-jEKKErLC6uwSqA+p6bmZR08usZM5Fpc+HdEu5CAzvye0q43yf1si1kjhHEa9XMkz0A2SAaal3eKCg/YYmtOsCA==} + engines: {node: '>=10'} + peerDependencies: + '@swc/helpers': '>=0.5.17' + peerDependenciesMeta: + '@swc/helpers': + optional: true + + '@swc/counter@0.1.3': + resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + + '@swc/helpers@0.5.15': + resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + + '@swc/helpers@0.5.17': + resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==} + + '@swc/html-darwin-arm64@1.13.1': + resolution: {integrity: sha512-PkzUL/dj5KtcMRSbCMy4ZYTql/92rCBw6fjFt5QvdJCITPC/MQcgEMWWFjj6iHbAAkSv4cJDTtEyZdRKTaKGsw==} + engines: {node: '>=10'} + cpu: [arm64] + os: [darwin] + + '@swc/html-darwin-x64@1.13.1': + resolution: {integrity: sha512-W0CpvqgPQYtaP5fLG1DIKQkfpygVYQzU0+b62nxbDhv5d2p+n7moHocym7xCM3oJnXAzqAklQlUvQlcrVycFuw==} + engines: {node: '>=10'} + cpu: [x64] + os: [darwin] + + '@swc/html-linux-arm-gnueabihf@1.13.1': + resolution: {integrity: sha512-zWAZddBDO0/8FKyAm5QYJae3H0ihDdK6Mq/Bu4VUuTkFU64mz5i5WAhAHSQg57SyDywMNefBTLp2Lc7yo0SdSA==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux] + + '@swc/html-linux-arm64-gnu@1.13.1': + resolution: {integrity: sha512-72Iap52NSQ3AOyOwJ3NiOLJ1wkyYdYxM9WMVhEtxB21lRDRvtVRtrFS/ssXM29hTt6kKQQeqjEbk36ZDRJdJew==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + + '@swc/html-linux-arm64-musl@1.13.1': + resolution: {integrity: sha512-3eSwAcTxE3g9pyn3Htm4qLCZdumKP4/vAYub/a32L9arqio/DXNoizHAu7H1qyXUX9UH1iTaryPLXwpsrMR+Bg==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + + '@swc/html-linux-x64-gnu@1.13.1': + resolution: {integrity: sha512-z+L1ilgzTulBCr3mLys9BktHbCaEsr9Hk1tKnwnPZ67IPA/ti4BZVVogAt32vB6EL2svEOnG6af1HUvTE+MRPw==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + + '@swc/html-linux-x64-musl@1.13.1': + resolution: {integrity: sha512-bWfR4BWq61BWdYU7dyrBfwND5OAUd03Z7Y3gsYHqj9sFxPz2VVMkJ6gqjmR9MJseHNj/2Z5Baqzutqlo+WtQjg==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + + '@swc/html-win32-arm64-msvc@1.13.1': + resolution: {integrity: sha512-/O+3sjnpvNW8uDD1vdCddDJ8GtsOE3ihjdV6dpSBW++Z7kj5j4QKyayU+T3bBxUsW56kiMk6iv7AhYhmUdRnHA==} + engines: {node: '>=10'} + cpu: [arm64] + os: [win32] + + '@swc/html-win32-ia32-msvc@1.13.1': + resolution: {integrity: sha512-SFkBGorkSs1xW2RlRI4Kk/N11Pah7/NjKQhHhY0ogbP0yp8ivMXE50tY8efL+6uOi2MF+jjXJfCNhm5yNDwTvA==} + engines: {node: '>=10'} + cpu: [ia32] + os: [win32] + + '@swc/html-win32-x64-msvc@1.13.1': + resolution: {integrity: sha512-xAe6n6J15vx9ZIksBPtyG5e+gkZCC6QZ/9g9ufilGkcO/tSGA4yHacwSrXnurRkhqhk8I9idw9x3iBfoNkAofA==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] + + '@swc/html@1.13.1': + resolution: {integrity: sha512-Yj3p6uouxVzvcLdJpXzWyvQp1Y5x8MTpN6Toq8XL4UE6i1EkILSgbXFX59c+Dm0UIyQdlZNrSJjRPhAJ425hZw==} + engines: {node: '>=14'} + + '@swc/types@0.1.23': + resolution: {integrity: sha512-u1iIVZV9Q0jxY+yM2vw/hZGDNudsN85bBpTqzAQ9rzkxW9D+e3aEM4Han+ow518gSewkXgjmEK0BD79ZcNVgPw==} + + '@szmarczak/http-timer@5.0.1': + resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==} + engines: {node: '>=14.16'} + + '@tailwindcss/forms@0.5.7': + resolution: {integrity: sha512-QE7X69iQI+ZXwldE+rzasvbJiyV/ju1FGHH0Qn2W3FKbuYtqp8LKcy6iSw79fVUT5/Vvf+0XgLCeYVG+UV6hOw==} + peerDependencies: + tailwindcss: '>=3.0.0 || >= 3.0.0-alpha.1' + + '@tanem/svg-injector@10.1.68': + resolution: {integrity: sha512-UkJajeR44u73ujtr5GVSbIlELDWD/mzjqWe54YMK61ljKxFcJoPd9RBSaO7xj02ISCWUqJW99GjrS+sVF0UnrA==} + + '@tanstack/react-virtual@3.13.12': + resolution: {integrity: sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==} + 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 + + '@tanstack/virtual-core@3.13.12': + resolution: {integrity: sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==} + + '@testing-library/dom@10.4.0': + resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} + engines: {node: '>=18'} + + '@testing-library/jest-dom@6.6.3': + resolution: {integrity: sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==} + engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + + '@testing-library/react@16.3.0': + resolution: {integrity: sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==} + engines: {node: '>=18'} + peerDependencies: + '@testing-library/dom': ^10.0.0 + '@types/react': ^18.0.0 || ^19.0.0 + '@types/react-dom': ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@tootallnate/once@1.1.2': + resolution: {integrity: sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==} + engines: {node: '>= 6'} + + '@tootallnate/once@2.0.0': + resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} + engines: {node: '>= 10'} + + '@tootallnate/quickjs-emscripten@0.23.0': + resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==} + + '@trysound/sax@0.2.0': + resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} + engines: {node: '>=10.13.0'} + + '@tufjs/canonical-json@1.0.0': + resolution: {integrity: sha512-QTnf++uxunWvG2z3UFNzAoQPHxnSXOwtaI3iJ+AohhV+5vONuArPjJE7aPXPVXfXJsqrVbZBu9b81AJoSd09IQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + '@tufjs/models@1.0.4': + resolution: {integrity: sha512-qaGV9ltJP0EO25YfFUPhxRVK0evXFIAGicsVXuRim4Ed9cjPxYhNnNJ49SFmbeLgtxpslIkX317IgpfcHPVj/A==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + '@tybys/wasm-util@0.10.0': + resolution: {integrity: sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==} + + '@types/acorn@4.0.6': + resolution: {integrity: sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ==} + + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.20.7': + resolution: {integrity: sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==} + + '@types/body-parser@1.19.6': + resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + + '@types/bonjour@3.5.13': + resolution: {integrity: sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==} + + '@types/codemirror@5.60.16': + resolution: {integrity: sha512-V/yHdamffSS075jit+fDxaOAmdP2liok8NSNJnAZfDJErzOheuygHZEhAJrfmk5TEyM32MhkZjwo/idX791yxw==} + + '@types/connect-history-api-fallback@1.5.4': + resolution: {integrity: sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==} + + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + + '@types/cors@2.8.19': + resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==} + + '@types/d3-array@3.2.1': + resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==} + + '@types/d3-axis@3.0.6': + resolution: {integrity: sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==} + + '@types/d3-brush@3.0.6': + resolution: {integrity: sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==} + + '@types/d3-chord@3.0.6': + resolution: {integrity: sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==} + + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-contour@3.0.6': + resolution: {integrity: sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==} + + '@types/d3-delaunay@6.0.4': + resolution: {integrity: sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==} + + '@types/d3-dispatch@3.0.6': + resolution: {integrity: sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ==} + + '@types/d3-drag@3.0.7': + resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==} + + '@types/d3-dsv@3.0.7': + resolution: {integrity: sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==} + + '@types/d3-ease@3.0.2': + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + + '@types/d3-fetch@3.0.7': + resolution: {integrity: sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==} + + '@types/d3-force@3.0.10': + resolution: {integrity: sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==} + + '@types/d3-format@3.0.4': + resolution: {integrity: sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==} + + '@types/d3-geo@3.1.0': + resolution: {integrity: sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==} + + '@types/d3-hierarchy@3.1.7': + resolution: {integrity: sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-path@3.1.1': + resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} + + '@types/d3-polygon@3.0.2': + resolution: {integrity: sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==} + + '@types/d3-quadtree@3.0.6': + resolution: {integrity: sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==} + + '@types/d3-random@3.0.3': + resolution: {integrity: sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==} + + '@types/d3-scale-chromatic@3.1.0': + resolution: {integrity: sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==} + + '@types/d3-scale@4.0.9': + resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} + + '@types/d3-selection@3.0.11': + resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==} + + '@types/d3-shape@3.1.7': + resolution: {integrity: sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==} + + '@types/d3-time-format@4.0.3': + resolution: {integrity: sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==} + + '@types/d3-time@3.0.4': + resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} + + '@types/d3-timer@3.0.2': + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + + '@types/d3-transition@3.0.9': + resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==} + + '@types/d3-zoom@3.0.8': + resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==} + + '@types/d3@7.4.3': + resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==} + + '@types/debug@4.1.12': + resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + + '@types/eslint-scope@3.7.7': + resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} + + '@types/eslint@9.6.1': + resolution: {integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==} + + '@types/estree-jsx@1.0.5': + resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/express-serve-static-core@4.19.6': + resolution: {integrity: sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==} + + '@types/express-serve-static-core@5.0.7': + resolution: {integrity: sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ==} + + '@types/express@4.17.23': + resolution: {integrity: sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ==} + + '@types/file-saver@2.0.7': + resolution: {integrity: sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==} + + '@types/geojson@7946.0.16': + resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} + + '@types/google-protobuf@3.15.12': + resolution: {integrity: sha512-40um9QqwHjRS92qnOaDpL7RmDK15NuZYo9HihiJRbYkMQZlWnuH8AdvbMy8/o6lgLmKbDUKa+OALCltHdbOTpQ==} + + '@types/gtag.js@0.0.12': + resolution: {integrity: sha512-YQV9bUsemkzG81Ea295/nF/5GijnD2Af7QhEofh7xu+kvCN6RdodgNwwGWXB5GMI3NoyvQo0odNctoH/qLMIpg==} + + '@types/hast@2.3.10': + resolution: {integrity: sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==} + + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + + '@types/history@4.7.11': + resolution: {integrity: sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==} + + '@types/hoist-non-react-statics@3.3.7': + resolution: {integrity: sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g==} + peerDependencies: + '@types/react': '*' + + '@types/html-minifier-terser@6.1.0': + resolution: {integrity: sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==} + + '@types/http-cache-semantics@4.0.4': + resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} + + '@types/http-errors@2.0.5': + resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + + '@types/http-proxy@1.17.16': + resolution: {integrity: sha512-sdWoUajOB1cd0A8cRRQ1cfyWNbmFKLAqBB89Y8x5iYyG/mkJHc0YUH8pdWBy2omi9qtCpiIgGjuwO0dQST2l5w==} + + '@types/istanbul-lib-coverage@2.0.6': + resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} + + '@types/istanbul-lib-report@3.0.3': + resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} + + '@types/istanbul-reports@3.0.4': + resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} + + '@types/jasmine@5.1.8': + resolution: {integrity: sha512-u7/CnvRdh6AaaIzYjCgUuVbREFgulhX05Qtf6ZtW+aOcjCKKVvKgpkPYJBFTZSHtFBYimzU4zP0V2vrEsq9Wcg==} + + '@types/jasminewd2@2.0.13': + resolution: {integrity: sha512-aJ3wj8tXMpBrzQ5ghIaqMisD8C3FIrcO6sDKHqFbuqAsI7yOxj0fA7MrRCPLZHIVUjERIwsMmGn/vB0UQ9u0Hg==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/json5@0.0.29': + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + + '@types/jsonwebtoken@9.0.10': + resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==} + + '@types/mdast@3.0.15': + resolution: {integrity: sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/mdx@2.0.13': + resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==} + + '@types/mime@1.3.5': + resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + + '@types/node-fetch@2.6.12': + resolution: {integrity: sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==} + + '@types/node-forge@1.3.13': + resolution: {integrity: sha512-zePQJSW5QkwSHKRApqWCVKeKoSOt4xvEnLENZPjyvm9Ezdf/EyDeJM7jqLzOwjVICQQzvLZ63T55MKdJB5H6ww==} + + '@types/node@12.20.55': + resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} + + '@types/node@17.0.45': + resolution: {integrity: sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==} + + '@types/node@18.19.120': + resolution: {integrity: sha512-WtCGHFXnVI8WHLxDAt5TbnCM4eSE+nI0QN2NJtwzcgMhht2eNz6V9evJrk+lwC8bCY8OWV5Ym8Jz7ZEyGnKnMA==} + + '@types/node@22.16.5': + resolution: {integrity: sha512-bJFoMATwIGaxxx8VJPeM8TonI8t579oRvgAuT8zFugJsJZgzqv0Fu8Mhp68iecjzG7cnN3mO2dJQ5uUM2EFrgQ==} + + '@types/node@24.1.0': + resolution: {integrity: sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==} + + '@types/normalize-package-data@2.4.4': + resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} + + '@types/opentype.js@1.3.8': + resolution: {integrity: sha512-H6qeTp03jrknklSn4bpT1/9+1xCAEIU2CnjcWPkicJy8A1SKuthanbvoHYMiv79/2W3Xn1XE4gfSJFzt2U3JSw==} + + '@types/parse5@6.0.3': + resolution: {integrity: sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==} + + '@types/pg@8.15.4': + resolution: {integrity: sha512-I6UNVBAoYbvuWkkU3oosC8yxqH21f4/Jc4DK71JLG3dT2mdlGe1z+ep/LQGXaKaOgcvUrsQoPRqfgtMcvZiJhg==} + + '@types/prismjs@1.26.5': + resolution: {integrity: sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==} + + '@types/prop-types@15.7.15': + resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + + '@types/qrcode@1.5.5': + resolution: {integrity: sha512-CdfBi/e3Qk+3Z/fXYShipBT13OJ2fDO2Q2w5CIP5anLTLIndQG9z6P1cnm+8zCWSpm5dnxMFd/uREtb0EXuQzg==} + + '@types/qs@6.14.0': + resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==} + + '@types/range-parser@1.2.7': + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + + '@types/react-dom@19.1.2': + resolution: {integrity: sha512-XGJkWF41Qq305SKWEILa1O8vzhb3aOo3ogBlSmiqNko/WmRb6QIaweuZCXjKygVDXpzXb5wyxKTSOsmkuqj+Qw==} + peerDependencies: + '@types/react': ^19.0.0 + + '@types/react-redux@7.1.34': + resolution: {integrity: sha512-GdFaVjEbYv4Fthm2ZLvj1VSCedV7TqE5y1kNwnjSdBOTXuRSgowux6J8TAct15T3CKBr63UMk+2CO7ilRhyrAQ==} + + '@types/react-router-config@5.0.11': + resolution: {integrity: sha512-WmSAg7WgqW7m4x8Mt4N6ZyKz0BubSj/2tVUMsAHp+Yd2AMwcSbeFq9WympT19p5heCFmF97R9eD5uUR/t4HEqw==} + + '@types/react-router-dom@5.3.3': + resolution: {integrity: sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==} + + '@types/react-router@5.1.20': + resolution: {integrity: sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==} + + '@types/react@19.1.2': + resolution: {integrity: sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw==} + + '@types/retry@0.12.0': + resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} + + '@types/sax@1.2.7': + resolution: {integrity: sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==} + + '@types/semver@7.7.0': + resolution: {integrity: sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==} + + '@types/send@0.17.5': + resolution: {integrity: sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==} + + '@types/serve-index@1.9.4': + resolution: {integrity: sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==} + + '@types/serve-static@1.15.8': + resolution: {integrity: sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==} + + '@types/sinonjs__fake-timers@8.1.1': + resolution: {integrity: sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==} + + '@types/sizzle@2.3.9': + resolution: {integrity: sha512-xzLEyKB50yqCUPUJkIsrVvoWNfFUbIZI+RspLWt8u+tIW/BetMBZtgV2LY/2o+tYH8dRvQ+eoPf3NdhQCcLE2w==} + + '@types/sockjs@0.3.36': + resolution: {integrity: sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==} + + '@types/tern@0.23.9': + resolution: {integrity: sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw==} + + '@types/tinycolor2@1.4.3': + resolution: {integrity: sha512-Kf1w9NE5HEgGxCRyIcRXR/ZYtDv0V8FVPtYHwLxl0O+maGX0erE77pQlD0gpP+/KByMZ87mOA79SjifhSB3PjQ==} + + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + + '@types/unist@2.0.11': + resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + + '@types/uuid@10.0.0': + resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==} + + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + + '@types/yargs-parser@21.0.3': + resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} + + '@types/yargs@17.0.33': + resolution: {integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==} + + '@types/yauzl@2.10.3': + resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} + + '@typescript-eslint/eslint-plugin@5.62.0': + resolution: {integrity: sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + '@typescript-eslint/parser': ^8.35.1 + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/eslint-plugin@7.18.0': + resolution: {integrity: sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + '@typescript-eslint/parser': ^8.35.1 + eslint: ^8.56.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/eslint-plugin@8.38.0': + resolution: {integrity: sha512-CPoznzpuAnIOl4nhj4tRr4gIPj5AfKgkiJmGQDaq+fQnRJTYlcBjbX3wbciGmpoPf8DREufuPRe1tNMZnGdanA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.35.1 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/parser@8.38.0': + resolution: {integrity: sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/project-service@8.38.0': + resolution: {integrity: sha512-dbK7Jvqcb8c9QfH01YB6pORpqX1mn5gDZc9n63Ak/+jD67oWXn3Gs0M6vddAN+eDXBCS5EmNWzbSxsn9SzFWWg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/scope-manager@5.62.0': + resolution: {integrity: sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@typescript-eslint/scope-manager@7.18.0': + resolution: {integrity: sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==} + engines: {node: ^18.18.0 || >=20.0.0} + + '@typescript-eslint/scope-manager@8.0.0-alpha.20': + resolution: {integrity: sha512-+Ncj0Q6DT8ZHYNp8h5RndW4Siv52kiPpHEz/i8Sj2rh2y8ZCc5pKSHSslk+eZi0Bdj+/+swNOmDNcL2CrlaEnA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/scope-manager@8.38.0': + resolution: {integrity: sha512-WJw3AVlFFcdT9Ri1xs/lg8LwDqgekWXWhH3iAF+1ZM+QPd7oxQ6jvtW/JPwzAScxitILUIFs0/AnQ/UWHzbATQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.38.0': + resolution: {integrity: sha512-Lum9RtSE3EroKk/bYns+sPOodqb2Fv50XOl/gMviMKNvanETUuUcC9ObRbzrJ4VSd2JalPqgSAavwrPiPvnAiQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/type-utils@5.62.0': + resolution: {integrity: sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '*' + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/type-utils@7.18.0': + resolution: {integrity: sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + eslint: ^8.56.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/type-utils@8.38.0': + resolution: {integrity: sha512-c7jAvGEZVf0ao2z+nnz8BUaHZD09Agbh+DY7qvBQqLiz8uJzRgVPj5YvOh8I8uEiH8oIUGIfHzMwUcGVco/SJg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/types@5.62.0': + resolution: {integrity: sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@typescript-eslint/types@7.18.0': + resolution: {integrity: sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==} + engines: {node: ^18.18.0 || >=20.0.0} + + '@typescript-eslint/types@8.0.0-alpha.20': + resolution: {integrity: sha512-xpU1rMQfnnNZxpHN6YUfr18sGOMcpC9hvt54fupcU6N1qMCagEtkRt1U15x086oJAgAITJGa67454ffAoCxv/w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/types@8.38.0': + resolution: {integrity: sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@5.62.0': + resolution: {integrity: sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/typescript-estree@7.18.0': + resolution: {integrity: sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/typescript-estree@8.0.0-alpha.20': + resolution: {integrity: sha512-VQ8Mf8upDCuf0uMTjX/Pdw3gvCZomkG43nuThUuzhK3YFwFmIDTqx0ZWSsYJkVGfll0WrXgIua+rKSP/n6NBWw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/typescript-estree@8.38.0': + resolution: {integrity: sha512-fooELKcAKzxux6fA6pxOflpNS0jc+nOQEEOipXFNjSlBS6fqrJOVY/whSn70SScHrcJ2LDsxWrneFoWYSVfqhQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/utils@5.62.0': + resolution: {integrity: sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + + '@typescript-eslint/utils@7.18.0': + resolution: {integrity: sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + eslint: ^8.56.0 + + '@typescript-eslint/utils@8.0.0-alpha.20': + resolution: {integrity: sha512-0aMhjDTvIrkGkHqyM0ZByAwR8BV1f2HhKdYyjtxko8S/Ca4PGjOIjub6VoF+bQwCRxEuV8viNUld78rqm9jqLA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + + '@typescript-eslint/utils@8.38.0': + resolution: {integrity: sha512-hHcMA86Hgt+ijJlrD8fX0j1j8w4C92zue/8LOPAFioIno+W0+L7KqE8QZKCcPGc/92Vs9x36w/4MPTJhqXdyvg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/visitor-keys@5.62.0': + resolution: {integrity: sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@typescript-eslint/visitor-keys@7.18.0': + resolution: {integrity: sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==} + engines: {node: ^18.18.0 || >=20.0.0} + + '@typescript-eslint/visitor-keys@8.0.0-alpha.20': + resolution: {integrity: sha512-ej06rfct0kalfJgIR8nTR7dF1mgfF83hkylrYas7IAElHfgw4zx99BUGa6VrnHZ1PkxdJBp5PgcO2FmmlOoaRQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/visitor-keys@8.38.0': + resolution: {integrity: sha512-pWrTcoFNWuwHlA9CvlfSsGWs14JxfN1TH25zM5L7o0pRLhsoZkDnTsXfQRJBEWJoV5DL0jf+Z+sxiud+K0mq1g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + + '@unrs/resolver-binding-android-arm-eabi@1.11.1': + resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==} + cpu: [arm] + os: [android] + + '@unrs/resolver-binding-android-arm64@1.11.1': + resolution: {integrity: sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==} + cpu: [arm64] + os: [android] + + '@unrs/resolver-binding-darwin-arm64@1.11.1': + resolution: {integrity: sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==} + cpu: [arm64] + os: [darwin] + + '@unrs/resolver-binding-darwin-x64@1.11.1': + resolution: {integrity: sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==} + cpu: [x64] + os: [darwin] + + '@unrs/resolver-binding-freebsd-x64@1.11.1': + resolution: {integrity: sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==} + cpu: [x64] + os: [freebsd] + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': + resolution: {integrity: sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': + resolution: {integrity: sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': + resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} + cpu: [arm64] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-musl@1.11.1': + resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} + cpu: [arm64] + os: [linux] + + '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': + resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} + cpu: [ppc64] + os: [linux] + + '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': + resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} + cpu: [riscv64] + os: [linux] + + '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': + resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} + cpu: [riscv64] + os: [linux] + + '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': + resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} + cpu: [s390x] + os: [linux] + + '@unrs/resolver-binding-linux-x64-gnu@1.11.1': + resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} + cpu: [x64] + os: [linux] + + '@unrs/resolver-binding-linux-x64-musl@1.11.1': + resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} + cpu: [x64] + os: [linux] + + '@unrs/resolver-binding-wasm32-wasi@1.11.1': + resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': + resolution: {integrity: sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==} + cpu: [arm64] + os: [win32] + + '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': + resolution: {integrity: sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==} + cpu: [ia32] + os: [win32] + + '@unrs/resolver-binding-win32-x64-msvc@1.11.1': + resolution: {integrity: sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==} + cpu: [x64] + os: [win32] + + '@vercel/analytics@1.5.0': + resolution: {integrity: sha512-MYsBzfPki4gthY5HnYN7jgInhAZ7Ac1cYDoRWFomwGHWEX7odTEzbtg9kf/QSo7XEsEAqlQugA6gJ2WS2DEa3g==} + peerDependencies: + '@remix-run/react': ^2 + '@sveltejs/kit': ^1 || ^2 + next: '>= 13' + react: ^18 || ^19 || ^19.0.0-rc + svelte: '>= 4' + vue: ^3 + vue-router: ^4 + peerDependenciesMeta: + '@remix-run/react': + optional: true + '@sveltejs/kit': + optional: true + next: + optional: true + react: + optional: true + svelte: + optional: true + vue: + optional: true + vue-router: + optional: true + + '@vercel/git-hooks@1.0.0': + resolution: {integrity: sha512-OxDFAAdyiJ/H0b8zR9rFCu3BIb78LekBXOphOYG3snV4ULhKFX387pBPpqZ9HLiRTejBWBxYEahkw79tuIgdAA==} + + '@vitejs/plugin-basic-ssl@1.0.1': + resolution: {integrity: sha512-pcub+YbFtFhaGRTo1832FQHQSHvMrlb43974e2eS8EKleR3p1cDdkJFPci1UhwkEf1J9Bz+wKBSzqpKp7nNj2A==} + engines: {node: '>=14.6.0'} + peerDependencies: + vite: ^3.0.0 || ^4.0.0 + + '@vitejs/plugin-react@4.7.0': + resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + + '@vitest/expect@2.1.9': + resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} + + '@vitest/mocker@2.1.9': + resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@2.1.9': + resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} + + '@vitest/runner@2.1.9': + resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==} + + '@vitest/snapshot@2.1.9': + resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==} + + '@vitest/spy@2.1.9': + resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==} + + '@vitest/utils@2.1.9': + resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} + + '@webassemblyjs/ast@1.14.1': + resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} + + '@webassemblyjs/floating-point-hex-parser@1.13.2': + resolution: {integrity: sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==} + + '@webassemblyjs/helper-api-error@1.13.2': + resolution: {integrity: sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==} + + '@webassemblyjs/helper-buffer@1.14.1': + resolution: {integrity: sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==} + + '@webassemblyjs/helper-numbers@1.13.2': + resolution: {integrity: sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==} + + '@webassemblyjs/helper-wasm-bytecode@1.13.2': + resolution: {integrity: sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==} + + '@webassemblyjs/helper-wasm-section@1.14.1': + resolution: {integrity: sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==} + + '@webassemblyjs/ieee754@1.13.2': + resolution: {integrity: sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==} + + '@webassemblyjs/leb128@1.13.2': + resolution: {integrity: sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==} + + '@webassemblyjs/utf8@1.13.2': + resolution: {integrity: sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==} + + '@webassemblyjs/wasm-edit@1.14.1': + resolution: {integrity: sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==} + + '@webassemblyjs/wasm-gen@1.14.1': + resolution: {integrity: sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==} + + '@webassemblyjs/wasm-opt@1.14.1': + resolution: {integrity: sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==} + + '@webassemblyjs/wasm-parser@1.14.1': + resolution: {integrity: sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==} + + '@webassemblyjs/wast-printer@1.14.1': + resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==} + + '@wessberg/ts-evaluator@0.0.27': + resolution: {integrity: sha512-7gOpVm3yYojUp/Yn7F4ZybJRxyqfMNf0LXK5KJiawbPfL0XTsJV+0mgrEDjOIR6Bi0OYk2Cyg4tjFu1r8MCZaA==} + engines: {node: '>=10.1.0'} + deprecated: this package has been renamed to ts-evaluator. Please install ts-evaluator instead + peerDependencies: + typescript: '>=3.2.x || >= 4.x' + + '@xtuc/ieee754@1.2.0': + resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} + + '@xtuc/long@4.2.2': + resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} + + '@yarnpkg/lockfile@1.1.0': + resolution: {integrity: sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==} + + '@yarnpkg/parsers@3.0.0-rc.46': + resolution: {integrity: sha512-aiATs7pSutzda/rq8fnuPwTglyVwjM22bNnK2ZgjrpAjQHSSl3lztd2f9evst1W/qnC58DRz7T7QndUDumAR4Q==} + engines: {node: '>=14.15.0'} + + '@zag-js/core@1.19.0': + resolution: {integrity: sha512-f1h4q/quCnz6X+mxEsDUSBDrUu6d3EO00X+DztdewU6KVBBW6ZYIIHtSRYT4FSsSV/Wi4OuhxwUKtlXDervhVg==} + + '@zag-js/dom-query@1.19.0': + resolution: {integrity: sha512-h8TyG79w9AsCKWqAGJ5Uvk/qYfyfCry5F+3K/+uCdkIFTDiuAoNU6TitHwKaYvVsvD8WmOrNHNfZR8X3UwVakw==} + + '@zag-js/focus-trap@1.19.0': + resolution: {integrity: sha512-3Csc+Mot+uclRL9wqapHvRyT09Ga5Zhul0v8HmHER+YxQM011ZmobrvwPd87izKdiyVcEc4QhlsFmqbF++jzQQ==} + + '@zag-js/presence@1.19.0': + resolution: {integrity: sha512-w0T42WDrdkmkZ+5WlkycVHQlBlR+bEb7l8XeCrrrGd3czemU3pGXfH5Ns394hkjOF1/qlwKyG0VwbQ5RZlWvHA==} + + '@zag-js/react@1.19.0': + resolution: {integrity: sha512-XCwg3YWqlvDo3+jFIkrjJJF8wX8sG+2MjyXnakwjUqnHMxx0ZMOpVNkQTGiy378NDnfbnEnZTNSwzZUJAyXyqQ==} + peerDependencies: + react: '>=18.0.0' + react-dom: '>=18.0.0' + + '@zag-js/store@1.19.0': + resolution: {integrity: sha512-yPvmf+COJdebxzIFnh0xa2eVfP7LETHPycmTET5MyrRlWVqDK2+51w0wcXk3LBsmanTC1kvndD2ZlIXHxrJ/AA==} + + '@zag-js/types@1.19.0': + resolution: {integrity: sha512-4oJPNGO00+fmIElaJayHSW8nuc0Dgb//u639BsHn3GzW1gsekfHcn8ZmNsBBp7yOBhrEBYpe9qp6YpKQYsmFcQ==} + + '@zag-js/utils@1.19.0': + resolution: {integrity: sha512-wFMIOs3vePtVtEr9nmipZoCi6YLf2diWX+q+rbUfKKwMvejiP++lWpoU7oZEB/C6B9yoE77AF+OyjDQyqQaqRg==} + + '@zkochan/js-yaml@0.0.6': + resolution: {integrity: sha512-nzvgl3VfhcELQ8LyVrYOru+UtAy1nrygk2+AGbTm8a5YcO6o8lSjAT+pfg3vJWxIoZKOUhrK6UU7xW/+00kQrg==} + hasBin: true + + abab@2.0.6: + resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} + deprecated: Use your platform's native atob() and btoa() methods instead + + abbrev@1.1.1: + resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + + abort-controller-x@0.4.3: + resolution: {integrity: sha512-VtUwTNU8fpMwvWGn4xE93ywbogTYsuT+AUxAXOeelbXuQVIwNmC5YLeho9sH4vZ4ITW8414TTAOG1nW6uIVHCA==} + + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + + accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + + acorn-globals@6.0.0: + resolution: {integrity: sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==} + + acorn-import-attributes@1.9.5: + resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} + peerDependencies: + acorn: ^8 + + acorn-import-phases@1.0.4: + resolution: {integrity: sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==} + engines: {node: '>=10.13.0'} + peerDependencies: + acorn: ^8.14.0 + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn-walk@7.2.0: + resolution: {integrity: sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==} + engines: {node: '>=0.4.0'} + + acorn-walk@8.3.4: + resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} + engines: {node: '>=0.4.0'} + + acorn@7.4.1: + resolution: {integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==} + engines: {node: '>=0.4.0'} + hasBin: true + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + address@1.2.2: + resolution: {integrity: sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==} + engines: {node: '>= 10.0.0'} + + adjust-sourcemap-loader@4.0.0: + resolution: {integrity: sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==} + engines: {node: '>=8.9'} + + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + agentkeepalive@4.6.0: + resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} + engines: {node: '>= 8.0.0'} + + aggregate-error@3.1.0: + resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} + engines: {node: '>=8'} + + ajv-draft-04@1.0.0: + resolution: {integrity: sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==} + peerDependencies: + ajv: ^8.5.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv-formats@2.1.1: + resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv-keywords@3.5.2: + resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} + peerDependencies: + ajv: ^6.9.1 + + ajv-keywords@5.1.0: + resolution: {integrity: sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==} + peerDependencies: + ajv: ^8.8.2 + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ajv@8.11.0: + resolution: {integrity: sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==} + + ajv@8.12.0: + resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==} + + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + + algoliasearch-helper@3.26.0: + resolution: {integrity: sha512-Rv2x3GXleQ3ygwhkhJubhhYGsICmShLAiqtUuJTUkr9uOCOXyF2E71LVT4XDnVffbknv8XgScP4U0Oxtgm+hIw==} + peerDependencies: + algoliasearch: '>= 3.1 < 6' + + algoliasearch@5.34.0: + resolution: {integrity: sha512-wioVnf/8uuG8Bmywhk5qKIQ3wzCCtmdvicPRb0fa3kKYGGoewfgDqLEaET1MV2NbTc3WGpPv+AgauLVBp1nB9A==} + engines: {node: '>= 14.0.0'} + + allof-merge@0.6.6: + resolution: {integrity: sha512-116eZBf2he0/J4Tl7EYMz96I5Anaeio+VL0j/H2yxW9CoYQAMMv8gYcwkVRoO7XfIOv/qzSTfVzDVGAYxKFi3g==} + + altcha-lib@1.3.0: + resolution: {integrity: sha512-PpFg/JPuR+Jiud7Vs54XSDqDxvylcp+0oDa/i1ARxBA/iKDqLeNlO8PorQbfuDTMVLYRypAa/2VDK3nbBTAu5A==} + + angular-oauth2-oidc@15.0.1: + resolution: {integrity: sha512-5gpqO9QL+qFqMItYFHe8F6H5nOIEaowcNUc9iTDs3P1bfVYnoKoVAaijob53PuPTF4YwzdfwKWZi4Mq6P7GENQ==} + peerDependencies: + '@angular/common': '>=14.0.0' + '@angular/core': '>=14.0.0' + + angularx-qrcode@16.0.2: + resolution: {integrity: sha512-FztOM7vjNu88sGxUU5jG2I+A9TxZBXXYBWINjpwIBbTL+COMgrtzXnScG7TyQeNknv5w3WFJWn59PcngRRYVXA==} + peerDependencies: + '@angular/core': ^16.0.0 + + ansi-align@3.0.1: + resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} + + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + + ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + + ansi-escapes@7.0.0: + resolution: {integrity: sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==} + engines: {node: '>=18'} + + ansi-html-community@0.0.8: + resolution: {integrity: sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==} + engines: {'0': node >= 0.8.0} + hasBin: true + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.1.0: + resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + aproba@2.1.0: + resolution: {integrity: sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==} + + arch@2.2.0: + resolution: {integrity: sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==} + + are-we-there-yet@2.0.0: + resolution: {integrity: sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==} + engines: {node: '>=10'} + deprecated: This package is no longer supported. + + are-we-there-yet@3.0.1: + resolution: {integrity: sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + deprecated: This package is no longer supported. + + arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-hidden@1.2.6: + resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} + engines: {node: '>=10'} + + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + + array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + + array-includes@3.1.9: + resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} + engines: {node: '>= 0.4'} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + array.prototype.findlast@1.2.5: + resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} + engines: {node: '>= 0.4'} + + array.prototype.findlastindex@1.2.6: + resolution: {integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==} + engines: {node: '>= 0.4'} + + array.prototype.flat@1.3.3: + resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} + engines: {node: '>= 0.4'} + + array.prototype.flatmap@1.3.3: + resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} + engines: {node: '>= 0.4'} + + array.prototype.tosorted@1.1.4: + resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==} + engines: {node: '>= 0.4'} + + arraybuffer.prototype.slice@1.0.4: + resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} + engines: {node: '>= 0.4'} + + asn1@0.2.6: + resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==} + + assert-plus@1.0.0: + resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==} + engines: {node: '>=0.8'} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + ast-types-flow@0.0.8: + resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} + + ast-types@0.13.4: + resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} + engines: {node: '>=4'} + + astral-regex@2.0.0: + resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} + engines: {node: '>=8'} + + astring@1.9.0: + resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==} + hasBin: true + + async-function@1.0.0: + resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} + engines: {node: '>= 0.4'} + + async@3.2.2: + resolution: {integrity: sha512-H0E+qZaDEfx/FY4t7iLRv1W2fFI6+pyCeTw1uN20AQPiwqwM6ojPxHxdLv4z8hi2DtnW9BOckSspLucW7pIE5g==} + + async@3.2.4: + resolution: {integrity: sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + at-least-node@1.0.0: + resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} + engines: {node: '>= 4.0.0'} + + autoprefixer@10.4.14: + resolution: {integrity: sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + + autoprefixer@10.4.21: + resolution: {integrity: sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + aws-sign2@0.7.0: + resolution: {integrity: sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==} + + aws4@1.13.2: + resolution: {integrity: sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==} + + axe-core@4.10.3: + resolution: {integrity: sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==} + engines: {node: '>=4'} + + axios@1.10.0: + resolution: {integrity: sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==} + + axobject-query@3.2.1: + resolution: {integrity: sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==} + + axobject-query@4.0.0: + resolution: {integrity: sha512-+60uv1hiVFhHZeO+Lz0RYzsVHy5Wr1ayX0mwda9KPDVLNJgZ1T9Ny7VmFbLDzxsH0D87I86vgj3gFrjTJUYznw==} + + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + + b4a@1.6.7: + resolution: {integrity: sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==} + + babel-loader@9.1.3: + resolution: {integrity: sha512-xG3ST4DglodGf8qSwv0MdeWLhrDsw/32QMdTO5T1ZIp9gQur0HkCyFs7Awskr10JKXFXwpAhiCuYX5oGXnRGbw==} + engines: {node: '>= 14.15.0'} + peerDependencies: + '@babel/core': ^7.12.0 + webpack: '>=5' + + babel-loader@9.2.1: + resolution: {integrity: sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA==} + engines: {node: '>= 14.15.0'} + peerDependencies: + '@babel/core': ^7.12.0 + webpack: '>=5' + + babel-plugin-dynamic-import-node@2.3.3: + resolution: {integrity: sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==} + + babel-plugin-istanbul@6.1.1: + resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} + engines: {node: '>=8'} + + babel-plugin-polyfill-corejs2@0.4.14: + resolution: {integrity: sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-plugin-polyfill-corejs3@0.13.0: + resolution: {integrity: sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-plugin-polyfill-corejs3@0.8.7: + resolution: {integrity: sha512-KyDvZYxAzkC0Aj2dAPyDzi2Ym15e5JKZSK+maI7NAwSqofvuFglbSsxE7wUOvTg9oFVnHMzVzBKcqEb4PJgtOA==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-plugin-polyfill-regenerator@0.5.5: + resolution: {integrity: sha512-OJGYZlhLqBh2DDHeqAxWB1XIvr49CxiJ2gIt61/PU55CQK4Z58OzMqjDe1zwQdQk+rBYsRc+1rJmdajM3gimHg==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-plugin-polyfill-regenerator@0.6.5: + resolution: {integrity: sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + bare-events@2.6.0: + resolution: {integrity: sha512-EKZ5BTXYExaNqi3I3f9RtEsaI/xBSGjE0XZCZilPzFAV/goswFHuPd9jEZlPIZ/iNZJwDSao9qRiScySz7MbQg==} + + bare-fs@4.1.6: + resolution: {integrity: sha512-25RsLF33BqooOEFNdMcEhMpJy8EoR88zSMrnOQOaM3USnOK2VmaJ1uaQEwPA6AQjrv1lXChScosN6CzbwbO9OQ==} + engines: {bare: '>=1.16.0'} + peerDependencies: + bare-buffer: '*' + peerDependenciesMeta: + bare-buffer: + optional: true + + bare-os@3.6.1: + resolution: {integrity: sha512-uaIjxokhFidJP+bmmvKSgiMzj2sV5GPHaZVAIktcxcpCyBFFWO+YlikVAdhmUo2vYFvFhOXIAlldqV29L8126g==} + engines: {bare: '>=1.14.0'} + + bare-path@3.0.0: + resolution: {integrity: sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==} + + bare-stream@2.6.5: + resolution: {integrity: sha512-jSmxKJNJmHySi6hC42zlZnq00rga4jjxcgNZjY9N5WlOe/iOoGRtdwGsHzQv2RlH2KOYMwGUXhf2zXd32BA9RA==} + peerDependencies: + bare-buffer: '*' + bare-events: '*' + peerDependenciesMeta: + bare-buffer: + optional: true + bare-events: + optional: true + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + base64id@2.0.0: + resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==} + engines: {node: ^4.5.0 || >= 5.9} + + basic-ftp@5.0.5: + resolution: {integrity: sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==} + engines: {node: '>=10.0.0'} + + batch@0.6.1: + resolution: {integrity: sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==} + + bcp-47-match@2.0.3: + resolution: {integrity: sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ==} + + bcrypt-pbkdf@1.0.2: + resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} + + better-path-resolve@1.0.0: + resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} + engines: {node: '>=4'} + + big.js@5.2.2: + resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + blob-util@2.0.2: + resolution: {integrity: sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==} + + bluebird@3.7.2: + resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} + + body-parser@1.20.3: + resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + bonjour-service@1.3.0: + resolution: {integrity: sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA==} + + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + + boxen@6.2.1: + resolution: {integrity: sha512-H4PEsJXfFI/Pt8sjDWbHlQPx4zL/bvSQjcilJmaulGt5mLDorHOHpmdXAJcBcmru7PhYSp/cDMWRko4ZUMFkSw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + boxen@7.1.1: + resolution: {integrity: sha512-2hCgjEmP8YLWQ130n2FerGv7rYpfBmnmp9Uy2Le1vge6X3gZIfSmEzP5QTDElFxcvVcXlEn8Aq6MU/PZygIOog==} + engines: {node: '>=14.16'} + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browser-process-hrtime@1.0.0: + resolution: {integrity: sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==} + + browser-stdout@1.3.1: + resolution: {integrity: sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==} + + browserslist@4.25.1: + resolution: {integrity: sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buffer-crc32@0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + + bundle-require@5.1.0: + resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + peerDependencies: + esbuild: '>=0.18' + + bytes@3.0.0: + resolution: {integrity: sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==} + engines: {node: '>= 0.8'} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + cacache@16.1.3: + resolution: {integrity: sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + cacache@17.1.4: + resolution: {integrity: sha512-/aJwG2l3ZMJ1xNAnqbMpA40of9dj/pIH3QfiuQSqjfPJF747VR0J/bHn+/KdNnHKc6XQcWt/AfRSBft82W1d2A==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + cacheable-lookup@7.0.0: + resolution: {integrity: sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==} + engines: {node: '>=14.16'} + + cacheable-request@10.2.14: + resolution: {integrity: sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==} + engines: {node: '>=14.16'} + + cachedir@2.4.0: + resolution: {integrity: sha512-9EtFOZR8g22CL7BWjJ9BUx1+A/djkofnyW3aOXZORNW2kxoUpx2h+uN2cOqwPmFhnpVmxg+KW2OjOSgChTEvsQ==} + engines: {node: '>=6'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.8: + resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + call-me-maybe@1.0.2: + resolution: {integrity: sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + camel-case@4.1.2: + resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==} + + camelcase-css@2.0.1: + resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} + engines: {node: '>= 6'} + + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + + camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + + camelcase@7.0.1: + resolution: {integrity: sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==} + engines: {node: '>=14.16'} + + caniuse-api@3.0.0: + resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==} + + caniuse-lite@1.0.30001727: + resolution: {integrity: sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==} + + case-anything@2.1.13: + resolution: {integrity: sha512-zlOQ80VrQ2Ue+ymH5OuM/DlDq64mEm+B9UTdHULv5osUMD6HalNTblf2b1u/m6QecjsnOkBpqVZ+XPwIVsy7Ng==} + engines: {node: '>=12.13'} + + caseless@0.12.0: + resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} + + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + + chai@5.2.1: + resolution: {integrity: sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A==} + engines: {node: '>=18'} + + chalk@3.0.0: + resolution: {integrity: sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==} + engines: {node: '>=8'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chalk@5.4.1: + resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + char-regex@1.0.2: + resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} + engines: {node: '>=10'} + + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + character-reference-invalid@2.0.1: + resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + + chardet@0.7.0: + resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} + + charset@1.0.1: + resolution: {integrity: sha512-6dVyOOYjpfFcL1Y4qChrAoQLRHvj2ziyhcm0QJlhOcAhykL/k1kTUPbeo+87MNRTRdk2OIIsIXbuF3x2wi5EXg==} + engines: {node: '>=4.0.0'} + + check-error@2.1.1: + resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} + engines: {node: '>= 16'} + + check-more-types@2.24.0: + resolution: {integrity: sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==} + engines: {node: '>= 0.8.0'} + + cheerio-select@2.1.0: + resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} + + cheerio@1.0.0-rc.12: + resolution: {integrity: sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==} + engines: {node: '>= 6'} + + chevrotain-allstar@0.3.1: + resolution: {integrity: sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==} + peerDependencies: + chevrotain: ^11.0.0 + + chevrotain@11.0.3: + resolution: {integrity: sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==} + + chokidar@3.5.3: + resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} + engines: {node: '>= 8.10.0'} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + chownr@2.0.0: + resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} + engines: {node: '>=10'} + + chrome-trace-event@1.0.4: + resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} + engines: {node: '>=6.0'} + + chromium-bidi@0.6.3: + resolution: {integrity: sha512-qXlsCmpCZJAnoTYI83Iu6EdYQpMYdVkCfq08KDh2pmlVqK5t5IA9mGs4/LwCwp4fqisSOMXZxP3HIh8w8aRn0A==} + peerDependencies: + devtools-protocol: '*' + + ci-info@3.9.0: + resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} + engines: {node: '>=8'} + + ci-info@4.3.0: + resolution: {integrity: sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==} + engines: {node: '>=8'} + + class-variance-authority@0.7.1: + resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + + clean-css@5.3.3: + resolution: {integrity: sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==} + engines: {node: '>= 10.0'} + + clean-stack@2.2.0: + resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} + engines: {node: '>=6'} + + cli-boxes@3.0.0: + resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} + engines: {node: '>=10'} + + cli-cursor@3.1.0: + resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} + engines: {node: '>=8'} + + cli-cursor@5.0.0: + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} + engines: {node: '>=18'} + + cli-spinners@2.6.1: + resolution: {integrity: sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g==} + engines: {node: '>=6'} + + cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + + cli-table3@0.6.1: + resolution: {integrity: sha512-w0q/enDHhPLq44ovMGdQeeDLvwxwavsJX7oQGYt/LrBlYsyaxyDnp6z3QzFut/6kLLKnlcUVJLrpB7KBfgG/RA==} + engines: {node: 10.* || >= 12.*} + + cli-table3@0.6.5: + resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} + engines: {node: 10.* || >= 12.*} + + cli-truncate@2.1.0: + resolution: {integrity: sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==} + engines: {node: '>=8'} + + cli-truncate@4.0.0: + resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==} + engines: {node: '>=18'} + + cli-width@3.0.0: + resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==} + engines: {node: '>= 10'} + + client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + + cliui@6.0.0: + resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} + + cliui@7.0.4: + resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + clone-deep@4.0.1: + resolution: {integrity: sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==} + engines: {node: '>=6'} + + clone@1.0.4: + resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} + engines: {node: '>=0.8'} + + clsx@1.2.1: + resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==} + engines: {node: '>=6'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + codemirror@5.65.19: + resolution: {integrity: sha512-+aFkvqhaAVr1gferNMuN8vkTSrWIFvzlMV9I2KBLCWS2WpZ2+UAkZjlMZmEuT+gcXTi6RrGQCkWq1/bDtGqhIA==} + + collapse-white-space@2.1.0: + resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + + color-support@1.1.3: + resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} + hasBin: true + + color@4.2.3: + resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} + engines: {node: '>=12.5.0'} + + colord@2.9.3: + resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} + + colorette@1.4.0: + resolution: {integrity: sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==} + + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + + colorjs.io@0.5.2: + resolution: {integrity: sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==} + + colors@1.4.0: + resolution: {integrity: sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==} + engines: {node: '>=0.1.90'} + + combine-promises@1.2.0: + resolution: {integrity: sha512-VcQB1ziGD0NXrhKxiwyNbCDmRzs/OShMs2GqW2DlU2A/Sd0nQxE1oWDAE5O0ygSx5mgQOn9eIFh7yKPgFRVkPQ==} + engines: {node: '>=10'} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + + commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + + commander@13.1.0: + resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} + engines: {node: '>=18'} + + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + commander@5.1.0: + resolution: {integrity: sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==} + engines: {node: '>= 6'} + + commander@6.2.1: + resolution: {integrity: sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==} + engines: {node: '>= 6'} + + commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + + commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} + + common-path-prefix@3.0.0: + resolution: {integrity: sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==} + + common-tags@1.8.2: + resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==} + engines: {node: '>=4.0.0'} + + compressible@2.0.18: + resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==} + engines: {node: '>= 0.6'} + + compression@1.8.1: + resolution: {integrity: sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==} + engines: {node: '>= 0.8.0'} + + compute-gcd@1.2.1: + resolution: {integrity: sha512-TwMbxBNz0l71+8Sc4czv13h4kEqnchV9igQZBi6QUaz09dnz13juGnnaWWJTRsP3brxOoxeB4SA2WELLw1hCtg==} + + compute-lcm@1.1.2: + resolution: {integrity: sha512-OFNPdQAXnQhDSKioX8/XYT6sdUlXwpeMjfd6ApxMJfyZ4GxmLR1xvMERctlYhlHwIiz6CSpBc2+qYKjHGZw4TQ==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + concurrently@9.2.0: + resolution: {integrity: sha512-IsB/fiXTupmagMW4MNp2lx2cdSN2FfZq78vF90LBB+zZHArbIQZjQtzXCiXnvTxCZSvXanTqFLWBjw2UkLx1SQ==} + engines: {node: '>=18'} + hasBin: true + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + confbox@0.2.2: + resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==} + + config-chain@1.1.13: + resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} + + configstore@6.0.0: + resolution: {integrity: sha512-cD31W1v3GqUlQvbBCGcXmd2Nj9SvLDOP1oQ0YFuLETufzSPaKp11rYBsSOm7rCsW3OnIRAFM3OxRhceaXNYHkA==} + engines: {node: '>=12'} + + connect-history-api-fallback@2.0.0: + resolution: {integrity: sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==} + engines: {node: '>=0.8'} + + connect@3.7.0: + resolution: {integrity: sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==} + engines: {node: '>= 0.10.0'} + + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + + console-control-strings@1.1.0: + resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} + + content-disposition@0.5.2: + resolution: {integrity: sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==} + engines: {node: '>= 0.6'} + + content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + convert-source-map@1.9.0: + resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie-signature@1.0.6: + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + + cookie@0.7.1: + resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} + engines: {node: '>= 0.6'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + copy-anything@2.0.6: + resolution: {integrity: sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==} + + copy-text-to-clipboard@3.2.0: + resolution: {integrity: sha512-RnJFp1XR/LOBDckxTib5Qjr/PMfkatD0MUCQgdpqS8MdKiNUzBjAQBEN6oUy+jW7LI93BBG3DtMB2KOOKpGs2Q==} + engines: {node: '>=12'} + + copy-to-clipboard@3.3.3: + resolution: {integrity: sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==} + + copy-webpack-plugin@11.0.0: + resolution: {integrity: sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==} + engines: {node: '>= 14.15.0'} + peerDependencies: + webpack: ^5.1.0 + + core-js-compat@3.44.0: + resolution: {integrity: sha512-JepmAj2zfl6ogy34qfWtcE7nHKAJnKsQFRn++scjVS2bZFllwptzw61BZcZFYBPpUznLfAvh0LGhxKppk04ClA==} + + core-js-pure@3.44.0: + resolution: {integrity: sha512-gvMQAGB4dfVUxpYD0k3Fq8J+n5bB6Ytl15lqlZrOIXFzxOhtPaObfkQGHtMRdyjIf7z2IeNULwi1jEwyS+ltKQ==} + + core-js@3.44.0: + resolution: {integrity: sha512-aFCtd4l6GvAXwVEh3XbbVqJGHDJt0OZRa+5ePGx3LLwi12WfexqQxcsohb2wgsa/92xtl19Hd66G/L+TaAxDMw==} + + core-util-is@1.0.2: + resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} + + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + + cors@2.8.5: + resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + engines: {node: '>= 0.10'} + + cose-base@1.0.3: + resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==} + + cose-base@2.2.0: + resolution: {integrity: sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==} + + cosmiconfig@8.3.6: + resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + + cosmiconfig@9.0.0: + resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + + critters@0.0.20: + resolution: {integrity: sha512-CImNRorKOl5d8TWcnAz5n5izQ6HFsvz29k327/ELy6UFcmbiZNOsinaKvzv16WZR0P6etfSWYzE47C4/56B3Uw==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + crypto-js@4.2.0: + resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} + + crypto-random-string@4.0.0: + resolution: {integrity: sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==} + engines: {node: '>=12'} + + css-blank-pseudo@7.0.1: + resolution: {integrity: sha512-jf+twWGDf6LDoXDUode+nc7ZlrqfaNphrBIBrcmeP3D8yw1uPaix1gCC8LUQUGQ6CycuK2opkbFFWFuq/a94ag==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + css-declaration-sorter@7.2.0: + resolution: {integrity: sha512-h70rUM+3PNFuaBDTLe8wF/cdWu+dOZmb7pJt8Z2sedYbAcQVQV/tEchueg3GWxwqS0cxtbxmaHEdkNACqcvsow==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss: ^8.0.9 + + css-has-pseudo@7.0.2: + resolution: {integrity: sha512-nzol/h+E0bId46Kn2dQH5VElaknX2Sr0hFuB/1EomdC7j+OISt2ZzK7EHX9DZDY53WbIVAR7FYKSO2XnSf07MQ==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + css-loader@6.11.0: + resolution: {integrity: sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g==} + engines: {node: '>= 12.13.0'} + peerDependencies: + '@rspack/core': 0.x || 1.x + webpack: ^5.0.0 + peerDependenciesMeta: + '@rspack/core': + optional: true + webpack: + optional: true + + css-loader@6.8.1: + resolution: {integrity: sha512-xDAXtEVGlD0gJ07iclwWVkLoZOpEvAWaSyf6W18S2pOC//K8+qUDIx8IIT3D+HjnmkJPQeesOPv5aiUaJsCM2g==} + engines: {node: '>= 12.13.0'} + peerDependencies: + webpack: ^5.0.0 + + css-minimizer-webpack-plugin@5.0.1: + resolution: {integrity: sha512-3caImjKFQkS+ws1TGcFn0V1HyDJFq1Euy589JlD6/3rV2kj+w7r5G9WDMgSHvpvXHNZ2calVypZWuEDQd9wfLg==} + engines: {node: '>= 14.15.0'} + peerDependencies: + '@parcel/css': '*' + '@swc/css': '*' + clean-css: '*' + csso: '*' + esbuild: '*' + lightningcss: '*' + webpack: ^5.0.0 + peerDependenciesMeta: + '@parcel/css': + optional: true + '@swc/css': + optional: true + clean-css: + optional: true + csso: + optional: true + esbuild: + optional: true + lightningcss: + optional: true + + css-prefers-color-scheme@10.0.0: + resolution: {integrity: sha512-VCtXZAWivRglTZditUfB4StnsWr6YVZ2PRtuxQLKTNRdtAf8tpzaVPE9zXIF3VaSc7O70iK/j1+NXxyQCqdPjQ==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + css-select@4.3.0: + resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==} + + css-select@5.2.2: + resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} + + css-selector-parser@3.1.3: + resolution: {integrity: sha512-gJMigczVZqYAk0hPVzx/M4Hm1D9QOtqkdQk9005TNzDIUGzo5cnHEDiKUT7jGPximL/oYb+LIitcHFQ4aKupxg==} + + css-tree@2.2.1: + resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + + css-tree@2.3.1: + resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + css-what@6.2.2: + resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} + engines: {node: '>= 6'} + + css.escape@1.5.1: + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + + cssdb@8.3.1: + resolution: {integrity: sha512-XnDRQMXucLueX92yDe0LPKupXetWoFOgawr4O4X41l5TltgK2NVbJJVDnnOywDYfW1sTJ28AcXGKOqdRKwCcmQ==} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + cssnano-preset-advanced@6.1.2: + resolution: {integrity: sha512-Nhao7eD8ph2DoHolEzQs5CfRpiEP0xa1HBdnFZ82kvqdmbwVBUr2r1QuQ4t1pi+D1ZpqpcO4T+wy/7RxzJ/WPQ==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + cssnano-preset-default@6.1.2: + resolution: {integrity: sha512-1C0C+eNaeN8OcHQa193aRgYexyJtU8XwbdieEjClw+J9d94E41LwT6ivKH0WT+fYwYWB0Zp3I3IZ7tI/BbUbrg==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + cssnano-utils@4.0.2: + resolution: {integrity: sha512-ZR1jHg+wZ8o4c3zqf1SIUSTIvm/9mU343FMR6Obe/unskbvpGhZOo1J6d/r8D1pzkRQYuwbcH3hToOuoA2G7oQ==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + cssnano@6.1.2: + resolution: {integrity: sha512-rYk5UeX7VAM/u0lNqewCdasdtPK81CgX8wJFLEIXHbV2oldWRgJAsZrdhRXkV1NJzA2g850KiFm9mMU2HxNxMA==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + csso@5.0.5: + resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + + cssom@0.3.8: + resolution: {integrity: sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==} + + cssom@0.4.4: + resolution: {integrity: sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==} + + cssstyle@2.3.0: + resolution: {integrity: sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==} + engines: {node: '>=8'} + + cssstyle@4.6.0: + resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} + engines: {node: '>=18'} + + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + custom-event@1.0.1: + resolution: {integrity: sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg==} + + cypress-wait-until@3.0.2: + resolution: {integrity: sha512-iemies796dD5CgjG5kV0MnpEmKSH+s7O83ZoJLVzuVbZmm4lheMsZqAVT73hlMx4QlkwhxbyUzhOBUOZwoOe0w==} + + cypress@14.5.2: + resolution: {integrity: sha512-O4E4CEBqDHLDrJD/dfStHPcM+8qFgVVZ89Li7xDU0yL/JxO/V0PEcfF2I8aGa7uA2MGNLkNUAnghPM83UcHOJw==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + + cypress@14.5.3: + resolution: {integrity: sha512-syLwKjDeMg77FRRx68bytLdlqHXDT4yBVh0/PPkcgesChYDjUZbwxLqMXuryYKzAyJsPsQHUDW1YU74/IYEUIA==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + + cytoscape-cose-bilkent@4.1.0: + resolution: {integrity: sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==} + peerDependencies: + cytoscape: ^3.2.0 + + cytoscape-fcose@2.2.0: + resolution: {integrity: sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==} + peerDependencies: + cytoscape: ^3.2.0 + + cytoscape@3.32.1: + resolution: {integrity: sha512-dbeqFTLYEwlFg7UGtcZhCCG/2WayX72zK3Sq323CEX29CY81tYfVhw1MIdduCtpstB0cTOhJswWlM/OEB3Xp+Q==} + engines: {node: '>=0.10'} + + d3-array@2.12.1: + resolution: {integrity: sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==} + + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-axis@3.0.0: + resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==} + engines: {node: '>=12'} + + d3-brush@3.0.0: + resolution: {integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==} + engines: {node: '>=12'} + + d3-chord@3.0.1: + resolution: {integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-contour@4.0.2: + resolution: {integrity: sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==} + engines: {node: '>=12'} + + d3-delaunay@6.0.4: + resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==} + engines: {node: '>=12'} + + d3-dispatch@3.0.1: + resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} + engines: {node: '>=12'} + + d3-drag@3.0.0: + resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==} + engines: {node: '>=12'} + + d3-dsv@3.0.1: + resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==} + engines: {node: '>=12'} + hasBin: true + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-fetch@3.0.1: + resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==} + engines: {node: '>=12'} + + d3-force@3.0.0: + resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==} + engines: {node: '>=12'} + + d3-format@3.1.0: + resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==} + engines: {node: '>=12'} + + d3-geo@3.1.1: + resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==} + engines: {node: '>=12'} + + d3-hierarchy@3.1.2: + resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@1.0.9: + resolution: {integrity: sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-polygon@3.0.1: + resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==} + engines: {node: '>=12'} + + d3-quadtree@3.0.1: + resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==} + engines: {node: '>=12'} + + d3-random@3.0.1: + resolution: {integrity: sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==} + engines: {node: '>=12'} + + d3-sankey@0.12.3: + resolution: {integrity: sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==} + + d3-scale-chromatic@3.1.0: + resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-selection@3.0.0: + resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} + engines: {node: '>=12'} + + d3-shape@1.3.7: + resolution: {integrity: sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + + d3-transition@3.0.1: + resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==} + engines: {node: '>=12'} + peerDependencies: + d3-selection: 2 - 3 + + d3-zoom@3.0.0: + resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} + engines: {node: '>=12'} + + d3@7.9.0: + resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==} + engines: {node: '>=12'} + + dagre-d3-es@7.0.11: + resolution: {integrity: sha512-tvlJLyQf834SylNKax8Wkzco/1ias1OPw8DcUMDE7oUIoSEW25riQVuiu/0OWEFqT0cxHT3Pa9/D82Jr47IONw==} + + damerau-levenshtein@1.0.8: + resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} + + dashdash@1.14.1: + resolution: {integrity: sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==} + engines: {node: '>=0.10'} + + data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + + data-uri-to-buffer@6.0.2: + resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==} + engines: {node: '>= 14'} + + data-urls@2.0.0: + resolution: {integrity: sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==} + engines: {node: '>=10'} + + data-urls@5.0.0: + resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} + engines: {node: '>=18'} + + data-view-buffer@1.0.2: + resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.2: + resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.1: + resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} + engines: {node: '>= 0.4'} + + date-format@4.0.14: + resolution: {integrity: sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==} + engines: {node: '>=4.0'} + + dateformat@4.6.3: + resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} + + dayjs@1.11.13: + resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} + + debounce@1.2.1: + resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==} + + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.3.7: + resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.1: + resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + + decamelize@4.0.0: + resolution: {integrity: sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==} + engines: {node: '>=10'} + + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + + decode-named-character-reference@1.2.0: + resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==} + + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + default-gateway@6.0.3: + resolution: {integrity: sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==} + engines: {node: '>= 10'} + + defaults@1.0.4: + resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + + defer-to-connect@2.0.1: + resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} + engines: {node: '>=10'} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-lazy-prop@2.0.0: + resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} + engines: {node: '>=8'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + degenerator@5.0.1: + resolution: {integrity: sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==} + engines: {node: '>= 14'} + + delaunator@5.0.1: + resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + delegates@1.0.0: + resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} + + depd@1.1.2: + resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==} + engines: {node: '>= 0.6'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + detect-indent@6.1.0: + resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} + engines: {node: '>=8'} + + detect-libc@1.0.3: + resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} + engines: {node: '>=0.10'} + hasBin: true + + detect-libc@2.0.4: + resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} + engines: {node: '>=8'} + + detect-node-es@1.1.0: + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + + detect-node@2.1.0: + resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} + + detect-package-manager@3.0.2: + resolution: {integrity: sha512-8JFjJHutStYrfWwzfretQoyNGoZVW1Fsrp4JO9spa7h/fBfwgTMEIy4/LBzRDGsxwVPHU0q+T9YvwLDJoOApLQ==} + engines: {node: '>=12'} + + detect-port@1.6.1: + resolution: {integrity: sha512-CmnVc+Hek2egPx1PeTFVta2W78xy2K/9Rkf6cC4T59S50tVnzKj+tnx5mmx5lwvCkujZ4uRrpRSuV+IVs3f90Q==} + engines: {node: '>= 4.0.0'} + hasBin: true + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + + devtools-protocol@0.0.1312386: + resolution: {integrity: sha512-DPnhUXvmvKT2dFA/j7B+riVLUt9Q6RKJlcppojL5CoRywJJKLDYnRlw0gTFKfgDPHP5E04UoB71SxoJlVZy8FA==} + + di@0.0.1: + resolution: {integrity: sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA==} + + diacritics@1.3.0: + resolution: {integrity: sha512-wlwEkqcsaxvPJML+rDh/2iS824jbREk6DUMUKkEaSlxdYHeS43cClJtsWglvw2RfeXGm6ohKDqsXteJ5sP5enA==} + + didyoumean@1.2.2: + resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + + diff@5.2.0: + resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==} + engines: {node: '>=0.3.1'} + + diff@7.0.0: + resolution: {integrity: sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==} + engines: {node: '>=0.3.1'} + + dijkstrajs@1.0.3: + resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==} + + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + direction@2.0.1: + resolution: {integrity: sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA==} + hasBin: true + + dlv@1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + + dns-packet@5.6.1: + resolution: {integrity: sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==} + engines: {node: '>=6'} + + doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + + doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + + docusaurus-plugin-image-zoom@3.0.1: + resolution: {integrity: sha512-mQrqA99VpoMQJNbi02qkWAMVNC4+kwc6zLLMNzraHAJlwn+HrlUmZSEDcTwgn+H4herYNxHKxveE2WsYy73eGw==} + peerDependencies: + '@docusaurus/theme-classic': '>=3.0.0' + + docusaurus-plugin-openapi-docs@4.4.0: + resolution: {integrity: sha512-VFW0euAyM6i6U6Q2WrNXkp1LnxQFGszZbmloMFYrs1qwBjPLkuHfQ4OJMXGDsGcGl4zNDJ9cwODmJlmdwl1hwg==} + engines: {node: '>=14'} + peerDependencies: + '@docusaurus/plugin-content-docs': ^3.5.0 + '@docusaurus/utils': ^3.5.0 + '@docusaurus/utils-validation': ^3.5.0 + react: ^16.8.4 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + docusaurus-plugin-sass@0.2.6: + resolution: {integrity: sha512-2hKQQDkrufMong9upKoG/kSHJhuwd+FA3iAe/qzS/BmWpbIpe7XKmq5wlz4J5CJaOPu4x+iDJbgAxZqcoQf0kg==} + peerDependencies: + '@docusaurus/core': ^2.0.0-beta || ^3.0.0-alpha + sass: ^1.30.0 + + docusaurus-theme-github-codeblock@2.0.2: + resolution: {integrity: sha512-H2WoQPWOLjGZO6KS58Gsd+eUVjTFJemkReiSSu9chqokyLc/3Ih3+zPRYfuEZ/HsDvSMIarf7CNcp+Vt+/G+ig==} + + docusaurus-theme-openapi-docs@4.4.0: + resolution: {integrity: sha512-wmc2b946rqBcdjgEHi6Up7e8orasYk5RnIUerTfmZ/Hi006I8FIjMnJEmHAF6t5PbFiiYnlkB6vYK0CC5xBnCQ==} + engines: {node: '>=14'} + peerDependencies: + '@docusaurus/theme-common': ^3.5.0 + docusaurus-plugin-openapi-docs: ^4.0.0 + docusaurus-plugin-sass: ^0.2.3 + react: ^16.8.4 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.4 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + + dom-accessibility-api@0.6.3: + resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + + dom-converter@0.2.0: + resolution: {integrity: sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==} + + dom-serialize@2.2.1: + resolution: {integrity: sha512-Yra4DbvoW7/Z6LBN560ZwXMjoNOSAN2wRsKFGc4iBeso+mpIA6qj1vfdf9HpMaKAqG6wXTy+1SYEzmNpKXOSsQ==} + + dom-serializer@1.4.1: + resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==} + + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domexception@2.0.1: + resolution: {integrity: sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==} + engines: {node: '>=8'} + deprecated: Use your platform's native DOMException instead + + domhandler@4.3.1: + resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==} + engines: {node: '>= 4'} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + dompurify@3.2.6: + resolution: {integrity: sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==} + + domutils@2.8.0: + resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} + + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + + dot-case@3.0.4: + resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} + + dot-prop@6.0.1: + resolution: {integrity: sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==} + engines: {node: '>=10'} + + dotenv-cli@8.0.0: + resolution: {integrity: sha512-aLqYbK7xKOiTMIRf1lDPbI+Y+Ip/wo5k3eyp6ePysVaSqbyxjyK3dK35BTxG+rmd7djf5q2UPs4noPNH+cj0Qw==} + hasBin: true + + dotenv-expand@10.0.0: + resolution: {integrity: sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==} + engines: {node: '>=12'} + + dotenv@10.0.0: + resolution: {integrity: sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==} + engines: {node: '>=10'} + + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + engines: {node: '>=12'} + + dprint-node@1.0.8: + resolution: {integrity: sha512-iVKnUtYfGrYcW1ZAlfR/F59cUVL8QIhWoBJoSjkkdua/dkWIgjZfiLMeTjiB06X0ZLkQ0M2C1VbUj/CxkIf1zg==} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + duplexer@0.1.2: + resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + ecc-jsbn@0.1.2: + resolution: {integrity: sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==} + + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + ejs@3.1.10: + resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} + engines: {node: '>=0.10.0'} + hasBin: true + + electron-to-chromium@1.5.189: + resolution: {integrity: sha512-y9D1ntS1ruO/pZ/V2FtLE+JXLQe28XoRpZ7QCCo0T8LdQladzdcOVQZH/IWLVJvCw12OGMb6hYOeOAjntCmJRQ==} + + emoji-regex@10.4.0: + resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + emojilib@2.4.0: + resolution: {integrity: sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==} + + emojis-list@3.0.0: + resolution: {integrity: sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==} + engines: {node: '>= 4'} + + emoticon@4.1.0: + resolution: {integrity: sha512-VWZfnxqwNcc51hIy/sbOdEem6D+cVtpPzEEtVAFdaas30+1dgkyaOQ4sQ6Bp0tOMqWO1v+HQfYaoodOkdhK6SQ==} + + encode-utf8@1.0.3: + resolution: {integrity: sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==} + + encodeurl@1.0.2: + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + encoding@0.1.13: + resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + + engine.io-parser@5.2.3: + resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==} + engines: {node: '>=10.0.0'} + + engine.io@6.6.4: + resolution: {integrity: sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==} + engines: {node: '>=10.2.0'} + + enhanced-resolve@5.18.2: + resolution: {integrity: sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==} + engines: {node: '>=10.13.0'} + + enquirer@2.3.6: + resolution: {integrity: sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==} + engines: {node: '>=8.6'} + + enquirer@2.4.1: + resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} + engines: {node: '>=8.6'} + + ent@2.2.2: + resolution: {integrity: sha512-kKvD1tO6BM+oK9HzCPpUdRb4vKFQY/FPTFmurMvh6LlN68VMrdj77w8yp51/kDbpkFOS9J8w5W6zIzgM2H8/hw==} + engines: {node: '>= 0.4'} + + entities@2.2.0: + resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + + env-cmd@10.1.0: + resolution: {integrity: sha512-mMdWTT9XKN7yNth/6N6g2GuKuJTsKMDHlQFUDacb/heQRRWOTIZ42t1rMHnQu4jYxU1ajdTeJM+9eEETlqToMA==} + engines: {node: '>=8.0.0'} + hasBin: true + + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + + environment@1.1.0: + resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} + engines: {node: '>=18'} + + err-code@2.0.3: + resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==} + + errno@0.1.8: + resolution: {integrity: sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==} + hasBin: true + + error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + + es-abstract@1.24.0: + resolution: {integrity: sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-iterator-helpers@1.2.1: + resolution: {integrity: sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==} + engines: {node: '>= 0.4'} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es-shim-unscopables@1.1.0: + resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} + engines: {node: '>= 0.4'} + + es-to-primitive@1.3.0: + resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} + engines: {node: '>= 0.4'} + + es6-promise@3.3.1: + resolution: {integrity: sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==} + + esast-util-from-estree@2.0.0: + resolution: {integrity: sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==} + + esast-util-from-js@2.0.1: + resolution: {integrity: sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==} + + esbuild-wasm@0.18.17: + resolution: {integrity: sha512-9OHGcuRzy+I8ziF9FzjfKLWAPbvi0e/metACVg9k6bK+SI4FFxeV6PcZsz8RIVaMD4YNehw+qj6UMR3+qj/EuQ==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.18.17: + resolution: {integrity: sha512-1GJtYnUxsJreHYA0Y+iQz2UEykonY66HNWOb0yXYZi9/kNrORUEHVg87eQsCtqh59PEJ5YVZJO98JHznMJSWjg==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.25.8: + resolution: {integrity: sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-goat@4.0.0: + resolution: {integrity: sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg==} + engines: {node: '>=12'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + + escodegen@2.1.0: + resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} + engines: {node: '>=6.0'} + hasBin: true + + eslint-config-next@15.4.0-canary.86: + resolution: {integrity: sha512-nMQzamY2GWhvScnfkfOVeq38tCt/TfyJyHMIzVYarpfyRj286Jk8ZkpgzQT8JtyeQ39kxTDZNBrB4CrWODYg4g==} + peerDependencies: + eslint: ^7.23.0 || ^8.0.0 || ^9.0.0 + typescript: '>=3.3.1' + peerDependenciesMeta: + typescript: + optional: true + + eslint-config-prettier@9.1.2: + resolution: {integrity: sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-import-resolver-node@0.3.9: + resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} + + eslint-import-resolver-typescript@3.10.1: + resolution: {integrity: sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + eslint: '*' + eslint-plugin-import: '*' + eslint-plugin-import-x: '*' + peerDependenciesMeta: + eslint-plugin-import: + optional: true + eslint-plugin-import-x: + optional: true + + eslint-module-utils@2.12.1: + resolution: {integrity: sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + + eslint-plugin-import@2.32.0: + resolution: {integrity: sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + + eslint-plugin-jsx-a11y@6.10.2: + resolution: {integrity: sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==} + engines: {node: '>=4.0'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9 + + eslint-plugin-react-hooks@5.2.0: + resolution: {integrity: sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==} + engines: {node: '>=10'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 + + eslint-plugin-react@7.37.5: + resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==} + engines: {node: '>=4'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 + + eslint-scope@5.1.1: + resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} + engines: {node: '>=8.0.0'} + + eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@2.1.0: + resolution: {integrity: sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==} + engines: {node: '>=10'} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@8.57.1: + resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. + hasBin: true + + espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@4.3.0: + resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-util-attach-comments@2.1.1: + resolution: {integrity: sha512-+5Ba/xGGS6mnwFbXIuQiDPTbuTxuMCooq3arVv7gPZtYpjp+VXH/NkHAP35OOefPhNG/UGqU3vt/LTABwcHX0w==} + + estree-util-attach-comments@3.0.0: + resolution: {integrity: sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==} + + estree-util-build-jsx@3.0.1: + resolution: {integrity: sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==} + + estree-util-is-identifier-name@2.1.0: + resolution: {integrity: sha512-bEN9VHRyXAUOjkKVQVvArFym08BTWB0aJPppZZr0UNyAqWsLaVfAqP7hbaTJjzHifmB5ebnR8Wm7r7yGN/HonQ==} + + estree-util-is-identifier-name@3.0.0: + resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} + + estree-util-scope@1.0.0: + resolution: {integrity: sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==} + + estree-util-to-js@1.2.0: + resolution: {integrity: sha512-IzU74r1PK5IMMGZXUVZbmiu4A1uhiPgW5hm1GjcOfr4ZzHaMPpLNJjR7HjXiIOzi25nZDrgFTobHTkV5Q6ITjA==} + + estree-util-to-js@2.0.0: + resolution: {integrity: sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==} + + estree-util-value-to-estree@3.4.0: + resolution: {integrity: sha512-Zlp+gxis+gCfK12d3Srl2PdX2ybsEA8ZYy6vQGVQTNNYLEGRQQ56XB64bjemN8kxIKXP1nC9ip4Z+ILy9LGzvQ==} + + estree-util-visit@1.2.1: + resolution: {integrity: sha512-xbgqcrkIVbIG+lI/gzbvd9SGTJL4zqJKBFttUl5pP27KhAjtMKbX/mQXJ7qgyXpMgVy/zvpm0xoQQaGL8OloOw==} + + estree-util-visit@2.0.0: + resolution: {integrity: sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + eta@2.2.0: + resolution: {integrity: sha512-UVQ72Rqjy/ZKQalzV5dCCJP80GrmPrMxh6NlNf+erV6ObL0ZFkhCstWRawS85z3smdr3d2wXPsZEY7rDPfGd2g==} + engines: {node: '>=6.0.0'} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + eval@0.1.8: + resolution: {integrity: sha512-EzV94NYKoO09GLXGjXj9JIlXijVck4ONSr5wiCWDvhsvj5jxSrzTmRU/9C1DyB6uToszLs8aifA6NQ7lEQdvFw==} + engines: {node: '>= 0.8'} + + event-stream@3.3.4: + resolution: {integrity: sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g==} + + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + + eventemitter-asyncresource@1.0.0: + resolution: {integrity: sha512-39F7TBIV0G7gTelxwbEqnwhp90eqCPON1k0NwNfwhgKn4Co4ybUbj2pECcXT0B3ztRKZ7Pw1JujUUgmQJHcVAQ==} + + eventemitter2@6.4.7: + resolution: {integrity: sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==} + + eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + + eventemitter3@5.0.1: + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + + execa@4.1.0: + resolution: {integrity: sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==} + engines: {node: '>=10'} + + execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + + execa@8.0.1: + resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} + engines: {node: '>=16.17'} + + executable@4.1.1: + resolution: {integrity: sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==} + engines: {node: '>=4'} + + exenv@1.2.2: + resolution: {integrity: sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw==} + + expect-type@1.2.2: + resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} + engines: {node: '>=12.0.0'} + + exponential-backoff@3.1.2: + resolution: {integrity: sha512-8QxYTVXUkuy7fIIoitQkPwGonB8F3Zj8eEO8Sqg9Zv/bkI7RJAzowee4gr81Hak/dUTpA2Z7VfQgoijjPNlUZA==} + + express@4.21.2: + resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} + engines: {node: '>= 0.10.0'} + + exsolve@1.0.7: + resolution: {integrity: sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==} + + extend-shallow@2.0.1: + resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} + engines: {node: '>=0.10.0'} + + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + extendable-error@0.1.7: + resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} + + external-editor@3.1.0: + resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} + engines: {node: '>=4'} + + extract-zip@2.0.1: + resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} + engines: {node: '>= 10.17.0'} + hasBin: true + + extsprintf@1.3.0: + resolution: {integrity: sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==} + engines: {'0': node >=0.6.0} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-fifo@1.3.2: + resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + + fast-glob@3.2.7: + resolution: {integrity: sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==} + engines: {node: '>=8'} + + fast-glob@3.3.1: + resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} + engines: {node: '>=8.6.0'} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-safe-stringify@2.1.1: + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + + fast-uri@3.0.6: + resolution: {integrity: sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==} + + fastq@1.19.1: + resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + + fault@2.0.1: + resolution: {integrity: sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==} + + faye-websocket@0.11.4: + resolution: {integrity: sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==} + engines: {node: '>=0.8.0'} + + fd-package-json@2.0.0: + resolution: {integrity: sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ==} + + fd-slicer@1.1.0: + resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + + fdir@6.4.6: + resolution: {integrity: sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + feed@4.2.2: + resolution: {integrity: sha512-u5/sxGfiMfZNtJ3OvQpXcvotFpYkL0n9u9mM2vkui2nGo8b4wvDkJ8gAkYqbA8QpGyFCv3RK0Z+Iv+9veCS9bQ==} + engines: {node: '>=0.4.0'} + + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + + fflate@0.4.8: + resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==} + + fflate@0.8.2: + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + + figures@3.2.0: + resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} + engines: {node: '>=8'} + + file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + + file-loader@6.2.0: + resolution: {integrity: sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==} + engines: {node: '>= 10.13.0'} + peerDependencies: + webpack: ^4.0.0 || ^5.0.0 + + file-saver@2.0.5: + resolution: {integrity: sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==} + + file-type@3.9.0: + resolution: {integrity: sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==} + engines: {node: '>=0.10.0'} + + filelist@1.0.4: + resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + filter-obj@5.1.0: + resolution: {integrity: sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng==} + engines: {node: '>=14.16'} + + finalhandler@1.1.2: + resolution: {integrity: sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==} + engines: {node: '>= 0.8'} + + finalhandler@1.3.1: + resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} + engines: {node: '>= 0.8'} + + find-cache-dir@4.0.0: + resolution: {integrity: sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==} + engines: {node: '>=14.16'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + find-up@6.3.0: + resolution: {integrity: sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + fix-dts-default-cjs-exports@1.0.1: + resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} + + flag-icons@7.5.0: + resolution: {integrity: sha512-kd+MNXviFIg5hijH766tt+3x76ele1AXlo4zDdCxIvqWZhKt4T83bOtxUOOMlTx/EcFdUMH5yvQgYlFh1EqqFg==} + + flat-cache@3.2.0: + resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} + engines: {node: ^10.12.0 || >=12.0.0} + + flat@5.0.2: + resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} + hasBin: true + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + + follow-redirects@1.15.9: + resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + + foreach@2.0.6: + resolution: {integrity: sha512-k6GAGDyqLe9JaebCsFCoudPPWfihKu8pylYXRlqP1J7ms39iPoTtk2fviNglIeQEwdh0bQeKJ01ZPyuyQvKzwg==} + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + forever-agent@0.6.1: + resolution: {integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==} + + form-data-encoder@1.7.2: + resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==} + + form-data-encoder@2.1.4: + resolution: {integrity: sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==} + engines: {node: '>= 14.17'} + + form-data@3.0.4: + resolution: {integrity: sha512-f0cRzm6dkyVYV3nPoooP8XlccPQukegwhAnpoLcXy+X+A8KfpGOoXwDr9FLZd3wzgLaBGQBE3lY93Zm/i1JvIQ==} + engines: {node: '>= 6'} + + form-data@4.0.4: + resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} + engines: {node: '>= 6'} + + format@0.2.2: + resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} + engines: {node: '>=0.4.x'} + + formatly@0.2.4: + resolution: {integrity: sha512-lIN7GpcvX/l/i24r/L9bnJ0I8Qn01qijWpQpDDvTLL29nKqSaJJu4h20+7VJ6m2CAhQ2/En/GbxDiHCzq/0MyA==} + engines: {node: '>=18.3.0'} + hasBin: true + + formdata-node@4.4.1: + resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==} + engines: {node: '>= 12.20'} + + formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fraction.js@4.3.7: + resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} + + fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + + from@0.1.7: + resolution: {integrity: sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==} + + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + + fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + + fs-extra@11.3.0: + resolution: {integrity: sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==} + engines: {node: '>=14.14'} + + fs-extra@7.0.1: + resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} + engines: {node: '>=6 <7 || >=8'} + + fs-extra@8.1.0: + resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} + engines: {node: '>=6 <7 || >=8'} + + fs-extra@9.1.0: + resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} + engines: {node: '>=10'} + + fs-minipass@2.1.0: + resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} + engines: {node: '>= 8'} + + fs-minipass@3.0.3: + resolution: {integrity: sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + fs-monkey@1.1.0: + resolution: {integrity: sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + fsu@1.1.1: + resolution: {integrity: sha512-xQVsnjJ/5pQtcKh+KjUoZGzVWn4uNkchxTF6Lwjr4Gf7nQr8fmUfhKJ62zE77+xQg9xnxi5KUps7XGs+VC986A==} + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + function.prototype.name@1.1.8: + resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} + engines: {node: '>= 0.4'} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + gauge@3.0.2: + resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==} + engines: {node: '>=10'} + deprecated: This package is no longer supported. + + gauge@4.0.4: + resolution: {integrity: sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + deprecated: This package is no longer supported. + + gaxios@7.1.1: + resolution: {integrity: sha512-Odju3uBUJyVCkW64nLD4wKLhbh93bh6vIg/ZIXkWiLPBrdgtc65+tls/qml+un3pr6JqYVFDZbbmLDQT68rTOQ==} + engines: {node: '>=18'} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-east-asian-width@1.3.0: + resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==} + engines: {node: '>=18'} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-nonce@1.0.1: + resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} + engines: {node: '>=6'} + + get-own-enumerable-property-symbols@3.0.2: + resolution: {integrity: sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==} + + get-package-type@0.1.0: + resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} + engines: {node: '>=8.0.0'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-stream@5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + + get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + + get-stream@8.0.1: + resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} + engines: {node: '>=16'} + + get-symbol-description@1.1.0: + resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} + engines: {node: '>= 0.4'} + + get-tsconfig@4.10.1: + resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} + + get-uri@6.0.5: + resolution: {integrity: sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==} + engines: {node: '>= 14'} + + getos@3.2.1: + resolution: {integrity: sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q==} + + getpass@0.1.7: + resolution: {integrity: sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==} + + github-slugger@1.5.0: + resolution: {integrity: sha512-wIh+gKBI9Nshz2o46B0B3f5k/W+WI9ZAv6y5Dn5WJ5SK1t0TnDimB4WE5rmTD05ZAIn8HALCZVmCsvj0w0v0lw==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob-to-regexp@0.4.1: + resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + + glob@10.4.5: + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + hasBin: true + + glob@11.0.3: + resolution: {integrity: sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==} + engines: {node: 20 || >=22} + hasBin: true + + glob@7.1.4: + resolution: {integrity: sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==} + deprecated: Glob versions prior to v9 are no longer supported + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + + glob@8.1.0: + resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} + engines: {node: '>=12'} + deprecated: Glob versions prior to v9 are no longer supported + + global-dirs@3.0.1: + resolution: {integrity: sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==} + engines: {node: '>=10'} + + globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} + + globals@15.15.0: + resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==} + engines: {node: '>=18'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + + globby@13.2.2: + resolution: {integrity: sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + globrex@0.1.2: + resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} + + google-protobuf@3.21.4: + resolution: {integrity: sha512-MnG7N936zcKTco4Jd2PX2U96Kf9PxygAPKBug+74LHzmHXmceN16MmRcdgZv+DGef/S9YvQAfRsNCn4cjf9yyQ==} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + got@12.6.1: + resolution: {integrity: sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ==} + engines: {node: '>=14.16'} + + graceful-fs@4.2.10: + resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + graphlib@2.1.8: + resolution: {integrity: sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==} + + gray-matter@4.0.3: + resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} + engines: {node: '>=6.0'} + + grpc-tools@1.13.0: + resolution: {integrity: sha512-7CbkJ1yWPfX0nHjbYG58BQThNhbICXBZynzCUxCb3LzX5X9B3hQbRY2STiRgIEiLILlK9fgl0z0QVGwPCdXf5g==} + hasBin: true + + grpc-web@1.5.0: + resolution: {integrity: sha512-y1tS3BBIoiVSzKTDF3Hm7E8hV2n7YY7pO0Uo7depfWJqKzWE+SKr0jvHNIJsJJYILQlpYShpi/DRJJMbosgDMQ==} + + guess-parser@0.4.22: + resolution: {integrity: sha512-KcUWZ5ACGaBM69SbqwVIuWGoSAgD+9iJnchR9j/IarVI1jHVeXv+bUXBIMeqVMSKt3zrn0Dgf9UpcOEpPBLbSg==} + peerDependencies: + typescript: '>=3.7.5' + + gzip-size@6.0.0: + resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==} + engines: {node: '>=10'} + + hachure-fill@0.5.2: + resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==} + + handle-thing@2.0.1: + resolution: {integrity: sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==} + + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + + has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.2.0: + resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + has-unicode@2.0.1: + resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} + + has-yarn@3.0.0: + resolution: {integrity: sha512-IrsVwUHhEULx3R8f/aA8AHuEzAorplsab/v8HBzEiIukwq5i/EC+xmOW+HfP1OaDP+2JkgT1yILHN2O3UFIbcA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + hasha@5.2.2: + resolution: {integrity: sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==} + engines: {node: '>=8'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + hast-util-embedded@3.0.0: + resolution: {integrity: sha512-naH8sld4Pe2ep03qqULEtvYr7EjrLK2QHY8KJR6RJkTUjPGObe1vnx585uzem2hGra+s1q08DZZpfgDVYRbaXA==} + + hast-util-from-html@1.0.2: + resolution: {integrity: sha512-LhrTA2gfCbLOGJq2u/asp4kwuG0y6NhWTXiPKP+n0qNukKy7hc10whqqCFfyvIA1Q5U5d0sp9HhNim9gglEH4A==} + + hast-util-from-html@2.0.3: + resolution: {integrity: sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==} + + hast-util-from-parse5@7.1.2: + resolution: {integrity: sha512-Nz7FfPBuljzsN3tCQ4kCBKqdNhQE2l0Tn+X1ubgKBPRoiDIu1mL08Cfw4k7q71+Duyaw7DXDN+VTAp4Vh3oCOw==} + + hast-util-from-parse5@8.0.3: + resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==} + + hast-util-has-property@3.0.0: + resolution: {integrity: sha512-MNilsvEKLFpV604hwfhVStK0usFY/QmM5zX16bo7EjnAEGofr5YyI37kzopBlZJkHD4t887i+q/C8/tr5Q94cA==} + + hast-util-is-body-ok-link@3.0.1: + resolution: {integrity: sha512-0qpnzOBLztXHbHQenVB8uNuxTnm/QBFUOmdOSsEn7GnBtyY07+ENTWVFBAnXd/zEgd9/SUG3lRY7hSIBWRgGpQ==} + + hast-util-is-element@3.0.0: + resolution: {integrity: sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==} + + hast-util-minify-whitespace@1.0.1: + resolution: {integrity: sha512-L96fPOVpnclQE0xzdWb/D12VT5FabA7SnZOUMtL1DbXmYiHJMXZvFkIZfiMmTCNJHUeO2K9UYNXoVyfz+QHuOw==} + + hast-util-parse-selector@3.1.1: + resolution: {integrity: sha512-jdlwBjEexy1oGz0aJ2f4GKMaVKkA9jwjr4MjAAI22E5fM/TXVZHuS5OpONtdeIkRKqAaryQ2E9xNQxijoThSZA==} + + hast-util-parse-selector@4.0.0: + resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==} + + hast-util-phrasing@3.0.1: + resolution: {integrity: sha512-6h60VfI3uBQUxHqTyMymMZnEbNl1XmEGtOxxKYL7stY2o601COo62AWAYBQR9lZbYXYSBoxag8UpPRXK+9fqSQ==} + + hast-util-raw@7.2.3: + resolution: {integrity: sha512-RujVQfVsOrxzPOPSzZFiwofMArbQke6DJjnFfceiEbFh7S05CbPt0cYN+A5YeD3pso0JQk6O1aHBnx9+Pm2uqg==} + + hast-util-raw@9.1.0: + resolution: {integrity: sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==} + + hast-util-select@6.0.4: + resolution: {integrity: sha512-RqGS1ZgI0MwxLaKLDxjprynNzINEkRHY2i8ln4DDjgv9ZhcYVIHN9rlpiYsqtFwrgpYU361SyWDQcGNIBVu3lw==} + + hast-util-to-estree@2.3.3: + resolution: {integrity: sha512-ihhPIUPxN0v0w6M5+IiAZZrn0LH2uZomeWwhn7uP7avZC6TE7lIiEh2yBMPr5+zi1aUCXq6VoYRgs2Bw9xmycQ==} + + hast-util-to-estree@3.1.3: + resolution: {integrity: sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==} + + hast-util-to-html@9.0.5: + resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} + + hast-util-to-jsx-runtime@2.3.6: + resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} + + hast-util-to-mdast@10.1.2: + resolution: {integrity: sha512-FiCRI7NmOvM4y+f5w32jPRzcxDIz+PUqDwEqn1A+1q2cdp3B8Gx7aVrXORdOKjMNDQsD1ogOr896+0jJHW1EFQ==} + + hast-util-to-parse5@7.1.0: + resolution: {integrity: sha512-YNRgAJkH2Jky5ySkIqFXTQiaqcAtJyVE+D5lkN6CdtOqrnkLfGYYrEcKuHOJZlp+MwjSwuD3fZuawI+sic/RBw==} + + hast-util-to-parse5@8.0.0: + resolution: {integrity: sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==} + + hast-util-to-string@3.0.1: + resolution: {integrity: sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==} + + hast-util-to-text@4.0.2: + resolution: {integrity: sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==} + + hast-util-whitespace@2.0.1: + resolution: {integrity: sha512-nAxA0v8+vXSBDt3AnRUNjyRIQ0rD+ntpbAp4LnPkumc5M9yUbSMa4XDU9Q6etY4f1Wp4bNgvc1yjiZtsTTrSng==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + + hastscript@7.2.0: + resolution: {integrity: sha512-TtYPq24IldU8iKoJQqvZOuhi5CyCQRAbvDOX0x1eW6rsHSxa/1i2CCiptNTotGHJ3VoHRGmqiv6/D3q113ikkw==} + + hastscript@9.0.1: + resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==} + + hdr-histogram-js@2.0.3: + resolution: {integrity: sha512-Hkn78wwzWHNCp2uarhzQ2SGFLU3JY8SBDDd3TAABK4fc30wm+MuPOrg5QVFVfkKOQd6Bfz3ukJEI+q9sXEkK1g==} + + hdr-histogram-percentiles-obj@3.0.0: + resolution: {integrity: sha512-7kIufnBqdsBGcSZLPJwqHT3yhk1QTsSlFsVD3kx5ixH/AlgBs9yM1q6DPhXZ8f8gtdqgh7N7/5btRLpQsS2gHw==} + + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + + history@4.10.1: + resolution: {integrity: sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==} + + hoist-non-react-statics@3.3.2: + resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + + hosted-git-info@4.1.0: + resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} + engines: {node: '>=10'} + + hosted-git-info@6.1.3: + resolution: {integrity: sha512-HVJyzUrLIL1c0QmviVh5E8VGyUS7xCFPS6yydaVd1UegW+ibV/CohqTH9MkOLDp5o+rb82DMo77PTuc9F/8GKw==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + hpack.js@2.1.6: + resolution: {integrity: sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==} + + html-encoding-sniffer@2.0.1: + resolution: {integrity: sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==} + engines: {node: '>=10'} + + html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} + + html-entities@2.6.0: + resolution: {integrity: sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==} + + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + + html-minifier-terser@6.1.0: + resolution: {integrity: sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==} + engines: {node: '>=12'} + hasBin: true + + html-minifier-terser@7.2.0: + resolution: {integrity: sha512-tXgn3QfqPIpGl9o+K5tpcj3/MN4SfLtsx2GWwBC3SSd0tXQGyF3gsSqad8loJgKZGM3ZxbYDd5yhiBIdWpmvLA==} + engines: {node: ^14.13.1 || >=16.0.0} + hasBin: true + + html-tags@3.3.1: + resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==} + engines: {node: '>=8'} + + html-url-attributes@3.0.1: + resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} + + html-void-elements@2.0.1: + resolution: {integrity: sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==} + + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + + html-webpack-plugin@5.6.3: + resolution: {integrity: sha512-QSf1yjtSAsmf7rYBV7XX86uua4W/vkhIt0xNXKbsi2foEeW7vjJQz4bhnpL3xH+l1ryl1680uNv968Z+X6jSYg==} + engines: {node: '>=10.13.0'} + peerDependencies: + '@rspack/core': 0.x || 1.x + webpack: ^5.20.0 + peerDependenciesMeta: + '@rspack/core': + optional: true + webpack: + optional: true + + htmlparser2@6.1.0: + resolution: {integrity: sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==} + + htmlparser2@8.0.2: + resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} + + http-cache-semantics@4.2.0: + resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} + + http-deceiver@1.2.7: + resolution: {integrity: sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==} + + http-errors@1.6.3: + resolution: {integrity: sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==} + engines: {node: '>= 0.6'} + + http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + + http-parser-js@0.5.10: + resolution: {integrity: sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==} + + http-proxy-agent@4.0.1: + resolution: {integrity: sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==} + engines: {node: '>= 6'} + + http-proxy-agent@5.0.0: + resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} + engines: {node: '>= 6'} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + http-proxy-middleware@2.0.9: + resolution: {integrity: sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@types/express': ^4.17.13 + peerDependenciesMeta: + '@types/express': + optional: true + + http-proxy@1.18.1: + resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==} + engines: {node: '>=8.0.0'} + + http-reasons@0.1.0: + resolution: {integrity: sha512-P6kYh0lKZ+y29T2Gqz+RlC9WBLhKe8kDmcJ+A+611jFfxdPsbMRQ5aNmFRM3lENqFkK+HTTL+tlQviAiv0AbLQ==} + + http-signature@1.4.0: + resolution: {integrity: sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg==} + engines: {node: '>=0.10'} + + http2-client@1.3.5: + resolution: {integrity: sha512-EC2utToWl4RKfs5zd36Mxq7nzHHBuomZboI0yYL6Y0RmBgT7Sgkq4rQ0ezFTYoIsSs7Tm9SJe+o2FcAg6GBhGA==} + + http2-wrapper@2.2.1: + resolution: {integrity: sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==} + engines: {node: '>=10.19.0'} + + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + human-id@4.1.1: + resolution: {integrity: sha512-3gKm/gCSUipeLsRYZbbdA1BD83lBoWUkZ7G9VFrhWPAU76KwYo5KR8V28bpoPm/ygy0x5/GCbpRQdY7VLYCoIg==} + hasBin: true + + human-signals@1.1.1: + resolution: {integrity: sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==} + engines: {node: '>=8.12.0'} + + human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + + human-signals@5.0.0: + resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} + engines: {node: '>=16.17.0'} + + humanize-ms@1.2.1: + resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} + + humps@2.0.1: + resolution: {integrity: sha512-E0eIbrFWUhwfXJmsbdjRQFQPrl5pTEoKlz163j1mTqqUnU9PgR4AgB8AIITzuB3vLBdxZXyZ9TDIrwB2OASz4g==} + + i18n-iso-countries@7.14.0: + resolution: {integrity: sha512-nXHJZYtNrfsi1UQbyRqm3Gou431elgLjKl//CYlnBGt5aTWdRPH1PiS2T/p/n8Q8LnqYqzQJik3Q7mkwvLokeg==} + engines: {node: '>= 12'} + + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + icss-utils@5.1.0: + resolution: {integrity: sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + ignore-by-default@1.0.1: + resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==} + + ignore-walk@6.0.5: + resolution: {integrity: sha512-VuuG0wCnjhnylG1ABXT3dAuIpTNDs/G8jlpmwXY03fXoXy/8ZK8/T+hMzt8L4WnrLCJgdybqgPagnF/f97cg3A==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + ignore@5.2.4: + resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} + engines: {node: '>= 4'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + image-size@0.5.5: + resolution: {integrity: sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==} + engines: {node: '>=0.10.0'} + hasBin: true + + image-size@2.0.2: + resolution: {integrity: sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w==} + engines: {node: '>=16.x'} + hasBin: true + + immer@9.0.21: + resolution: {integrity: sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==} + + immutable@4.3.7: + resolution: {integrity: sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==} + + immutable@5.1.3: + resolution: {integrity: sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg==} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + import-lazy@4.0.0: + resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==} + engines: {node: '>=8'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + infer-owner@1.0.4: + resolution: {integrity: sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==} + + infima@0.2.0-alpha.45: + resolution: {integrity: sha512-uyH0zfr1erU1OohLk0fT4Rrb94AOhguWNOcD9uGrSpRvNB+6gZXUoJX5J0NtvzBO10YZ9PgvA4NFgt+fYg8ojw==} + engines: {node: '>=12'} + + inflight@1.0.6: + resolution: {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. + + inherits@2.0.3: + resolution: {integrity: sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + ini@2.0.0: + resolution: {integrity: sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==} + engines: {node: '>=10'} + + ini@4.1.1: + resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + inline-style-parser@0.1.1: + resolution: {integrity: sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==} + + inline-style-parser@0.2.4: + resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==} + + inquirer@8.2.4: + resolution: {integrity: sha512-nn4F01dxU8VeKfq192IjLsxu0/OmMZ4Lg3xKAns148rCaXP6ntAoEkVYZThWjwON8AlzdZZi6oqnhNbxUG9hVg==} + engines: {node: '>=12.0.0'} + + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + + internmap@1.0.1: + resolution: {integrity: sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==} + + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + + interpret@1.4.0: + resolution: {integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==} + engines: {node: '>= 0.10'} + + intl-messageformat@10.7.16: + resolution: {integrity: sha512-UmdmHUmp5CIKKjSoE10la5yfU+AYJAaiYLsodbjL4lji83JNvgOQUjGaGhGrpFCb0Uh7sl7qfP1IyILa8Z40ug==} + + invariant@2.2.4: + resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} + + ip-address@9.0.5: + resolution: {integrity: sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==} + engines: {node: '>= 12'} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + ipaddr.js@2.2.0: + resolution: {integrity: sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==} + engines: {node: '>= 10'} + + is-alphabetical@2.0.1: + resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} + + is-alphanumerical@2.0.1: + resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-arrayish@0.3.2: + resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + + is-async-function@2.1.1: + resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} + engines: {node: '>= 0.4'} + + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + + is-buffer@2.0.5: + resolution: {integrity: sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==} + engines: {node: '>=4'} + + is-bun-module@2.0.0: + resolution: {integrity: sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-ci@3.0.1: + resolution: {integrity: sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==} + hasBin: true + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-data-view@1.0.2: + resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} + engines: {node: '>= 0.4'} + + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + + is-decimal@2.0.1: + resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + + is-docker@2.2.1: + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} + engines: {node: '>=8'} + hasBin: true + + is-extendable@0.1.1: + resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} + engines: {node: '>=0.10.0'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-finalizationregistry@1.1.1: + resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} + engines: {node: '>= 0.4'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-fullwidth-code-point@4.0.0: + resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==} + engines: {node: '>=12'} + + is-fullwidth-code-point@5.0.0: + resolution: {integrity: sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==} + engines: {node: '>=18'} + + is-generator-function@1.1.0: + resolution: {integrity: sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==} + engines: {node: '>= 0.4'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-hexadecimal@2.0.1: + resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + + is-installed-globally@0.4.0: + resolution: {integrity: sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==} + engines: {node: '>=10'} + + is-interactive@1.0.0: + resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} + engines: {node: '>=8'} + + is-lambda@1.0.1: + resolution: {integrity: sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==} + + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + + is-npm@6.0.0: + resolution: {integrity: sha512-JEjxbSmtPSt1c8XTkVrlujcXdKV1/tvuQ7GwKcAlyiVLeYFQ2VHat8xfrDJsIkhCdF/tZ7CiIR3sy141c6+gPQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-obj@1.0.1: + resolution: {integrity: sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==} + engines: {node: '>=0.10.0'} + + is-obj@2.0.0: + resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} + engines: {node: '>=8'} + + is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + + is-plain-obj@2.1.0: + resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==} + engines: {node: '>=8'} + + is-plain-obj@3.0.0: + resolution: {integrity: sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==} + engines: {node: '>=10'} + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + is-plain-object@2.0.4: + resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} + engines: {node: '>=0.10.0'} + + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-regexp@1.0.0: + resolution: {integrity: sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==} + engines: {node: '>=0.10.0'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + is-stream@3.0.0: + resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + + is-subdir@1.2.0: + resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==} + engines: {node: '>=4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + is-typedarray@1.0.0: + resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} + + is-unicode-supported@0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakref@1.1.1: + resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + + is-what@3.14.1: + resolution: {integrity: sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==} + + is-what@4.1.16: + resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} + engines: {node: '>=12.13'} + + is-windows@1.0.2: + resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} + engines: {node: '>=0.10.0'} + + is-wsl@2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} + + is-yarn-global@0.4.1: + resolution: {integrity: sha512-/kppl+R+LO5VmhYSEWARUFjodS25D68gvj8W7z0I7OWhUla5xWu8KL6CtB2V0R6yqhnRgbcaREMr4EEM6htLPQ==} + engines: {node: '>=12'} + + isarray@0.0.1: + resolution: {integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + isbinaryfile@4.0.10: + resolution: {integrity: sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==} + engines: {node: '>= 8.0.0'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + isobject@3.0.1: + resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} + engines: {node: '>=0.10.0'} + + isstream@0.1.2: + resolution: {integrity: sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==} + + istanbul-lib-coverage@2.0.5: + resolution: {integrity: sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==} + engines: {node: '>=6'} + + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-instrument@5.2.1: + resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} + engines: {node: '>=8'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-lib-source-maps@3.0.6: + resolution: {integrity: sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw==} + engines: {node: '>=6'} + + istanbul-lib-source-maps@4.0.1: + resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} + engines: {node: '>=10'} + + istanbul-reports@3.1.7: + resolution: {integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==} + engines: {node: '>=8'} + + iterator.prototype@1.1.5: + resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} + engines: {node: '>= 0.4'} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + jackspeak@4.1.1: + resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==} + engines: {node: 20 || >=22} + + jake@10.9.2: + resolution: {integrity: sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==} + engines: {node: '>=10'} + hasBin: true + + jasmine-core@4.6.1: + resolution: {integrity: sha512-VYz/BjjmC3klLJlLwA4Kw8ytk0zDSmbbDLNs794VnWmkcCB7I9aAL/D48VNQtmITyPvea2C3jdUMfc3kAoy0PQ==} + + jasmine-core@5.6.0: + resolution: {integrity: sha512-niVlkeYVRwKFpmfWg6suo6H9CrNnydfBLEqefM5UjibYS+UoTjZdmvPJSiuyrRLGnFj1eYRhFd/ch+5hSlsFVA==} + + jasmine-spec-reporter@7.0.0: + resolution: {integrity: sha512-OtC7JRasiTcjsaCBPtMO0Tl8glCejM4J4/dNuOJdA8lBjz4PmWjYQ6pzb0uzpBNAWJMDudYuj9OdXJWqM2QTJg==} + + jest-util@29.7.0: + resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-worker@27.5.1: + resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} + engines: {node: '>= 10.13.0'} + + jest-worker@29.7.0: + resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jiti@1.21.7: + resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} + hasBin: true + + jiti@2.4.2: + resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} + hasBin: true + + joi@17.13.3: + resolution: {integrity: sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==} + + jose@5.10.0: + resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==} + + joycon@3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + + js-levenshtein@1.1.6: + resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==} + engines: {node: '>=0.10.0'} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@3.14.1: + resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + hasBin: true + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + jsbn@0.1.1: + resolution: {integrity: sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==} + + jsbn@1.1.0: + resolution: {integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==} + + jsdom@16.7.0: + resolution: {integrity: sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw==} + engines: {node: '>=10'} + peerDependencies: + canvas: ^2.5.0 + peerDependenciesMeta: + canvas: + optional: true + + jsdom@26.1.0: + resolution: {integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==} + engines: {node: '>=18'} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + + jsesc@2.5.2: + resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} + engines: {node: '>=4'} + hasBin: true + + jsesc@3.0.2: + resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==} + engines: {node: '>=6'} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-crawl@0.5.3: + resolution: {integrity: sha512-BEjjCw8c7SxzNK4orhlWD5cXQh8vCk2LqDr4WgQq4CV+5dvopeYwt1Tskg67SuSLKvoFH5g0yuYtg7rcfKV6YA==} + engines: {node: '>=14.0.0'} + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-parse-even-better-errors@3.0.2: + resolution: {integrity: sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + json-pointer@0.6.2: + resolution: {integrity: sha512-vLWcKbOaXlO+jvRy4qNd+TI1QUPZzfJj1tpJ3vAXDych5XJf93ftpUKe5pKCrzyIIwgBJcOcCVRUfqQP25afBw==} + + json-schema-compare@0.2.2: + resolution: {integrity: sha512-c4WYmDKyJXhs7WWvAWm3uIYnfyWFoIp+JEoX34rctVvEkMYCPGhXtvmFFXiffBbxfZsvQ0RNnV5H7GvDF5HCqQ==} + + json-schema-merge-allof@0.8.1: + resolution: {integrity: sha512-CTUKmIlPJbsWfzRRnOXz+0MjIqvnleIXwFTzz+t9T86HnYX/Rozria6ZVGLktAU9e+NygNljveP+yxqtQp/Q4w==} + engines: {node: '>=12.0.0'} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json-stringify-safe@5.0.1: + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + + json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonc-parser@3.2.0: + resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==} + + jsonfile@4.0.0: + resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + + jsonfile@6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + + jsonparse@1.3.1: + resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} + engines: {'0': node >= 0.2.0} + + jsonwebtoken@9.0.2: + resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} + engines: {node: '>=12', npm: '>=6'} + + jsprim@2.0.2: + resolution: {integrity: sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==} + engines: {'0': node >=0.6.0} + + jsx-ast-utils@3.3.5: + resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} + engines: {node: '>=4.0'} + + jwa@1.4.2: + resolution: {integrity: sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==} + + jws@3.2.2: + resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} + + karma-chrome-launcher@3.2.0: + resolution: {integrity: sha512-rE9RkUPI7I9mAxByQWkGJFXfFD6lE4gC5nPuZdobf/QdTEJI6EU4yIay/cfU/xV4ZxlM5JiTv7zWYgA64NpS5Q==} + + karma-coverage-istanbul-reporter@3.0.3: + resolution: {integrity: sha512-wE4VFhG/QZv2Y4CdAYWDbMmcAHeS926ZIji4z+FkB2aF/EposRb6DP6G5ncT/wXhqUfAb/d7kZrNKPonbvsATw==} + + karma-coverage@2.2.1: + resolution: {integrity: sha512-yj7hbequkQP2qOSb20GuNSIyE//PgJWHwC2IydLE6XRtsnaflv+/OSGNssPjobYUlhVVagy99TQpqUt3vAUG7A==} + engines: {node: '>=10.0.0'} + + karma-jasmine-html-reporter@2.1.0: + resolution: {integrity: sha512-sPQE1+nlsn6Hwb5t+HHwyy0A1FNCVKuL1192b+XNauMYWThz2kweiBVW1DqloRpVvZIJkIoHVB7XRpK78n1xbQ==} + peerDependencies: + jasmine-core: ^4.0.0 || ^5.0.0 + karma: ^6.0.0 + karma-jasmine: ^5.0.0 + + karma-jasmine@5.1.0: + resolution: {integrity: sha512-i/zQLFrfEpRyQoJF9fsCdTMOF5c2dK7C7OmsuKg2D0YSsuZSfQDiLuaiktbuio6F2wiCsZSnSnieIQ0ant/uzQ==} + engines: {node: '>=12'} + peerDependencies: + karma: ^6.0.0 + + karma-source-map-support@1.4.0: + resolution: {integrity: sha512-RsBECncGO17KAoJCYXjv+ckIz+Ii9NCi+9enk+rq6XC81ezYkb4/RHE6CTXdA7IOJqoF3wcaLfVG0CPmE5ca6A==} + + karma@6.4.4: + resolution: {integrity: sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==} + engines: {node: '>= 10'} + hasBin: true + + katex@0.16.22: + resolution: {integrity: sha512-XCHRdUw4lf3SKBaJe4EvgqIuWwkPSo9XoeO8GjQW94Bp7TWv9hNhzZjZ+OH9yf1UmLygb7DIT5GSFQiyt16zYg==} + hasBin: true + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + khroma@2.1.0: + resolution: {integrity: sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==} + + kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + + kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + + klona@2.0.6: + resolution: {integrity: sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==} + engines: {node: '>= 8'} + + knip@5.62.0: + resolution: {integrity: sha512-hfTUVzmrMNMT1khlZfAYmBABeehwWUUrizLQoLamoRhSFkygsGIXWx31kaWKBgEaIVL77T3Uz7IxGvSw+CvQ6A==} + engines: {node: '>=18.18.0'} + hasBin: true + peerDependencies: + '@types/node': '>=18' + typescript: '>=5.0.4' + + kolorist@1.8.0: + resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + + langium@3.3.1: + resolution: {integrity: sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w==} + engines: {node: '>=16.0.0'} + + language-subtag-registry@0.3.23: + resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==} + + language-tags@1.0.9: + resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==} + engines: {node: '>=0.10'} + + latest-version@7.0.0: + resolution: {integrity: sha512-KvNT4XqAMzdcL6ka6Tl3i2lYeFDgXNCuIX+xNx6ZMVR1dFq+idXd9FLKNMOIx0t9mJ9/HudyX4oZWXZQ0UJHeg==} + engines: {node: '>=14.16'} + + launch-editor@2.10.0: + resolution: {integrity: sha512-D7dBRJo/qcGX9xlvt/6wUYzQxjh5G1RvZPgPv8vi4KRU99DVQL/oW7tnVOCCTm2HGeo3C5HvGE5Yrh6UBoZ0vA==} + + layout-base@1.0.2: + resolution: {integrity: sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==} + + layout-base@2.0.1: + resolution: {integrity: sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==} + + lazy-ass@1.6.0: + resolution: {integrity: sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw==} + engines: {node: '> 0.8'} + + less-loader@11.1.0: + resolution: {integrity: sha512-C+uDBV7kS7W5fJlUjq5mPBeBVhYpTIm5gB09APT9o3n/ILeaXVsiSFTbZpTJCJwQ/Crczfn3DmfQFwxYusWFug==} + engines: {node: '>= 14.15.0'} + peerDependencies: + less: ^3.5.0 || ^4.0.0 + webpack: ^5.0.0 + + less@4.1.3: + resolution: {integrity: sha512-w16Xk/Ta9Hhyei0Gpz9m7VS8F28nieJaL/VyShID7cYvP6IL5oHeL6p4TXSDJqZE/lNv0oJ2pGVjJsRkfwm5FA==} + engines: {node: '>=6'} + hasBin: true + + leven@3.1.0: + resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} + engines: {node: '>=6'} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + libphonenumber-js@1.12.10: + resolution: {integrity: sha512-E91vHJD61jekHHR/RF/E83T/CMoaLXT7cwYA75T4gim4FZjnM6hbJjVIGg7chqlSqRsSvQ3izGmOjHy1SQzcGQ==} + + license-webpack-plugin@4.0.2: + resolution: {integrity: sha512-771TFWFD70G1wLTC4oU2Cw4qvtmNrIw+wRvBtn+okgHl7slJVi7zfNcdmqDL72BojM30VNJ2UHylr1o77U37Jw==} + peerDependencies: + webpack: '*' + peerDependenciesMeta: + webpack: + optional: true + + lightningcss-darwin-arm64@1.30.1: + resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.30.1: + resolution: {integrity: sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.30.1: + resolution: {integrity: sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.30.1: + resolution: {integrity: sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.30.1: + resolution: {integrity: sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.30.1: + resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.30.1: + resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.30.1: + resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.30.1: + resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.30.1: + resolution: {integrity: sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.30.1: + resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==} + engines: {node: '>= 12.0.0'} + + lilconfig@2.1.0: + resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} + engines: {node: '>=10'} + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + lines-and-columns@2.0.4: + resolution: {integrity: sha512-wM1+Z03eypVAVUCE7QdSqpVIvelbOakn1M0bPDoA4SGWPx3sNDVUiMo3L6To6WWGClB7VyXnhQ4Sn7gxiJbE6A==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + lint-staged@15.5.1: + resolution: {integrity: sha512-6m7u8mue4Xn6wK6gZvSCQwBvMBR36xfY24nF5bMTf2MHDYG6S3yhJuOgdYVw99hsjyDt2d4z168b3naI8+NWtQ==} + engines: {node: '>=18.12.0'} + hasBin: true + + liquid-json@0.3.1: + resolution: {integrity: sha512-wUayTU8MS827Dam6MxgD72Ui+KOSF+u/eIqpatOtjnvgJ0+mnDq33uC2M7J0tPK+upe/DpUAuK4JUU89iBoNKQ==} + engines: {node: '>=4'} + + listr2@3.14.0: + resolution: {integrity: sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g==} + engines: {node: '>=10.0.0'} + peerDependencies: + enquirer: '>= 2.3.0 < 3' + peerDependenciesMeta: + enquirer: + optional: true + + listr2@8.3.3: + resolution: {integrity: sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ==} + engines: {node: '>=18.0.0'} + + load-script@1.0.0: + resolution: {integrity: sha512-kPEjMFtZvwL9TaZo0uZ2ml+Ye9HUMmPwbYRJ324qF9tqMejwykJ5ggTyvzmrbBeapCAbk98BSbTeovHEEP1uCA==} + + load-tsconfig@0.2.5: + resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + loader-runner@4.3.0: + resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==} + engines: {node: '>=6.11.5'} + + loader-utils@2.0.4: + resolution: {integrity: sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==} + engines: {node: '>=8.9.0'} + + loader-utils@3.2.1: + resolution: {integrity: sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==} + engines: {node: '>= 12.13.0'} + + local-pkg@1.1.1: + resolution: {integrity: sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg==} + engines: {node: '>=14'} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + locate-path@7.2.0: + resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + lodash-es@4.17.21: + resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + + lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + + lodash.debounce@4.0.8: + resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + + lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + + lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + + lodash.isempty@4.4.0: + resolution: {integrity: sha512-oKMuF3xEeqDltrGMfDxAPGIVMSSRv8tbRSODbrs4KGsRRLEhrW8N8Rd4DRgB2+621hY8A8XwwrTVhXWpxFvMzg==} + + lodash.isfunction@3.0.9: + resolution: {integrity: sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==} + + lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + + lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + + lodash.isobject@3.0.2: + resolution: {integrity: sha512-3/Qptq2vr7WeJbB4KHUSKlq8Pl7ASXi3UG6CMbBm8WRtXi8+GHm7mKaU3urfpSEzWe2wCIChs6/sdocUsTKJiA==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + + lodash.memoize@4.1.2: + resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + + lodash.sortby@4.7.0: + resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} + + lodash.startcase@4.4.0: + resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} + + lodash.uniq@4.5.0: + resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + + log-update@4.0.0: + resolution: {integrity: sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==} + engines: {node: '>=10'} + + log-update@6.1.0: + resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} + engines: {node: '>=18'} + + log4js@6.9.1: + resolution: {integrity: sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==} + engines: {node: '>=8.0'} + + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + loupe@3.1.4: + resolution: {integrity: sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg==} + + lower-case@2.0.2: + resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} + + lowercase-keys@3.0.0: + resolution: {integrity: sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@11.1.0: + resolution: {integrity: sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==} + engines: {node: 20 || >=22} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + + lru-cache@7.18.3: + resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} + engines: {node: '>=12'} + + lucide-react@0.469.0: + resolution: {integrity: sha512-28vvUnnKQ/dBwiCQtwJw7QauYnE7yd2Cyp4tTTJpvglX4EMpbflcdBgrgToX2j71B3YvugK/NH3BGUk+E/p/Fw==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + lucide-react@0.503.0: + resolution: {integrity: sha512-HGGkdlPWQ0vTF8jJ5TdIqhQXZi6uh3LnNgfZ8MHiuxFfX3RZeA79r2MW2tHAZKlAVfoNE8esm3p+O6VkIvpj6w==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + + magic-string@0.30.1: + resolution: {integrity: sha512-mbVKXPmS0z0G4XqFDCTllmDQ6coZzn94aMlb0o/A4HEHJCKcanlDZwYJgwnkmgD3jyWhUgj9VsPrfd972yPffA==} + engines: {node: '>=12'} + + magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + + make-dir-cli@4.0.0: + resolution: {integrity: sha512-9BBC2CaGH0hUAx+tQthgxqYypwkTs+7oXmPdiWyDpHGo4mGB3kdudUKQGivK59C1aJroo4QLlXF7Chu/kdhYiw==} + engines: {node: '>=18'} + hasBin: true + + make-dir@2.1.0: + resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==} + engines: {node: '>=6'} + + make-dir@3.1.0: + resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} + engines: {node: '>=8'} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + + make-dir@5.0.0: + resolution: {integrity: sha512-G0yBotnlWVonPClw+tq+xi4K7DZC9n96HjGTBDdHkstAVsDkfZhi1sTvZypXLpyQTbISBkDtK0E5XlUqDsShQg==} + engines: {node: '>=18'} + + make-fetch-happen@10.2.1: + resolution: {integrity: sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + make-fetch-happen@11.1.1: + resolution: {integrity: sha512-rLWS7GCSTcEujjVBs2YqG7Y4643u8ucvCJeSRqiLYhesrDuzeuFIk37xREzAsfQaqzl8b9rNCE4m6J8tvX4Q8w==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + map-stream@0.1.0: + resolution: {integrity: sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==} + + markdown-extensions@2.0.0: + resolution: {integrity: sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==} + engines: {node: '>=16'} + + markdown-table@2.0.0: + resolution: {integrity: sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A==} + + markdown-table@3.0.4: + resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + + marked@15.0.12: + resolution: {integrity: sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==} + engines: {node: '>= 18'} + hasBin: true + + marked@16.1.1: + resolution: {integrity: sha512-ij/2lXfCRT71L6u0M29tJPhP0bM5shLL3u5BePhFwPELj2blMJ6GDtD7PfJhRLhJ/c2UwrK17ySVcDzy2YHjHQ==} + engines: {node: '>= 20'} + hasBin: true + + material-colors@1.2.6: + resolution: {integrity: sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==} + + material-design-icons-iconfont@6.7.0: + resolution: {integrity: sha512-lSj71DgVv20kO0kGbs42icDzbRot61gEDBLQACzkUuznRQBUYmbxzEkGU6dNBb5fRWHMaScYlAXX96HQ4/cJWA==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mdast-util-definitions@5.1.2: + resolution: {integrity: sha512-8SVPMuHqlPME/z3gqVwWY4zVXn8lqKv/pAhC57FuJ40ImXyBpmO5ukh98zB2v7Blql2FiHjHv9LVztSIqjY+MA==} + + mdast-util-directive@3.1.0: + resolution: {integrity: sha512-I3fNFt+DHmpWCYAT7quoM6lHf9wuqtI+oCOfvILnoicNIqjh5E3dEJWiXuYME2gNe8vl1iMQwyUHa7bgFmak6Q==} + + mdast-util-find-and-replace@2.2.2: + resolution: {integrity: sha512-MTtdFRz/eMDHXzeK6W3dO7mXUlF82Gom4y0oOgvHhh/HXZAGvIQDUvQ0SuUx+j2tv44b8xTHOm8K/9OoRFnXKw==} + + mdast-util-find-and-replace@3.0.2: + resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} + + mdast-util-from-markdown@1.3.1: + resolution: {integrity: sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==} + + mdast-util-from-markdown@2.0.2: + resolution: {integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==} + + mdast-util-frontmatter@2.0.1: + resolution: {integrity: sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA==} + + mdast-util-gfm-autolink-literal@1.0.3: + resolution: {integrity: sha512-My8KJ57FYEy2W2LyNom4n3E7hKTuQk/0SES0u16tjA9Z3oFkF4RrC/hPAPgjlSpezsOvI8ObcXcElo92wn5IGA==} + + mdast-util-gfm-autolink-literal@2.0.1: + resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} + + mdast-util-gfm-footnote@1.0.2: + resolution: {integrity: sha512-56D19KOGbE00uKVj3sgIykpwKL179QsVFwx/DCW0u/0+URsryacI4MAdNJl0dh+u2PSsD9FtxPFbHCzJ78qJFQ==} + + mdast-util-gfm-footnote@2.1.0: + resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==} + + mdast-util-gfm-strikethrough@1.0.3: + resolution: {integrity: sha512-DAPhYzTYrRcXdMjUtUjKvW9z/FNAMTdU0ORyMcbmkwYNbKocDpdk+PX1L1dQgOID/+vVs1uBQ7ElrBQfZ0cuiQ==} + + mdast-util-gfm-strikethrough@2.0.0: + resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} + + mdast-util-gfm-table@1.0.7: + resolution: {integrity: sha512-jjcpmNnQvrmN5Vx7y7lEc2iIOEytYv7rTvu+MeyAsSHTASGCCRA79Igg2uKssgOs1i1po8s3plW0sTu1wkkLGg==} + + mdast-util-gfm-table@2.0.0: + resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} + + mdast-util-gfm-task-list-item@1.0.2: + resolution: {integrity: sha512-PFTA1gzfp1B1UaiJVyhJZA1rm0+Tzn690frc/L8vNX1Jop4STZgOE6bxUhnzdVSB+vm2GU1tIsuQcA9bxTQpMQ==} + + mdast-util-gfm-task-list-item@2.0.0: + resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} + + mdast-util-gfm@2.0.2: + resolution: {integrity: sha512-qvZ608nBppZ4icQlhQQIAdc6S3Ffj9RGmzwUKUWuEICFnd1LVkN3EktF7ZHAgfcEdvZB5owU9tQgt99e2TlLjg==} + + mdast-util-gfm@3.1.0: + resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} + + mdast-util-mdx-expression@1.3.2: + resolution: {integrity: sha512-xIPmR5ReJDu/DHH1OoIT1HkuybIfRGYRywC+gJtI7qHjCJp/M9jrmBEJW22O8lskDWm562BX2W8TiAwRTb0rKA==} + + mdast-util-mdx-expression@2.0.1: + resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} + + mdast-util-mdx-jsx@2.1.4: + resolution: {integrity: sha512-DtMn9CmVhVzZx3f+optVDF8yFgQVt7FghCRNdlIaS3X5Bnym3hZwPbg/XW86vdpKjlc1PVj26SpnLGeJBXD3JA==} + + mdast-util-mdx-jsx@3.2.0: + resolution: {integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==} + + mdast-util-mdx@2.0.1: + resolution: {integrity: sha512-38w5y+r8nyKlGvNjSEqWrhG0w5PmnRA+wnBvm+ulYCct7nsGYhFVb0lljS9bQav4psDAS1eGkP2LMVcZBi/aqw==} + + mdast-util-mdx@3.0.0: + resolution: {integrity: sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==} + + mdast-util-mdxjs-esm@1.3.1: + resolution: {integrity: sha512-SXqglS0HrEvSdUEfoXFtcg7DRl7S2cwOXc7jkuusG472Mmjag34DUDeOJUZtl+BVnyeO1frIgVpHlNRWc2gk/w==} + + mdast-util-mdxjs-esm@2.0.1: + resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==} + + mdast-util-phrasing@3.0.1: + resolution: {integrity: sha512-WmI1gTXUBJo4/ZmSk79Wcb2HcjPJBzM1nlI/OUWA8yk2X9ik3ffNbBGsU+09BFmXaL1IBb9fiuvq6/KMiNycSg==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-hast@12.3.0: + resolution: {integrity: sha512-pits93r8PhnIoU4Vy9bjW39M2jJ6/tdHyja9rrot9uujkN7UTU9SDnE6WNJz/IGyQk3XHX6yNNtrBH6cQzm8Hw==} + + mdast-util-to-hast@13.2.0: + resolution: {integrity: sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==} + + mdast-util-to-markdown@1.5.0: + resolution: {integrity: sha512-bbv7TPv/WC49thZPg3jXuqzuvI45IL2EVAr/KxF0BSdHsU0ceFHOmwQn6evxAh1GaoK/6GQ1wp4R4oW2+LFL/A==} + + mdast-util-to-markdown@2.1.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + + mdast-util-to-string@3.2.0: + resolution: {integrity: sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + + mdn-data@2.0.28: + resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==} + + mdn-data@2.0.30: + resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} + + mdx-mermaid@2.0.3: + resolution: {integrity: sha512-aVLaaVbQD8KmqzEk2AdLFb02MMENWkq5QQPD25sdtiswTIWk684JoaCOmy8oV+w3pthkcy2lRp0xVKIq1sLsqg==} + peerDependencies: + mermaid: '>=8.11.0' + react: ^16.8.4 || ^17.0.0 || ^18.0.0 || ^19.0.0 + unist-util-visit: ^4.1.0 + + media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + + medium-zoom@1.1.0: + resolution: {integrity: sha512-ewyDsp7k4InCUp3jRmwHBRFGyjBimKps/AJLjRSox+2q/2H4p/PNpQf+pwONWlJiOudkBXtbdmVbFjqyybfTmQ==} + + memfs@3.5.3: + resolution: {integrity: sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==} + engines: {node: '>= 4.0.0'} + + memoize-one@5.2.1: + resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} + + meow@13.2.0: + resolution: {integrity: sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==} + engines: {node: '>=18'} + + merge-anything@5.1.7: + resolution: {integrity: sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ==} + engines: {node: '>=12.13'} + + merge-descriptors@1.0.3: + resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + mermaid@11.9.0: + resolution: {integrity: sha512-YdPXn9slEwO0omQfQIsW6vS84weVQftIyyTGAZCwM//MGhPzL1+l6vO6bkf0wnP4tHigH1alZ5Ooy3HXI2gOag==} + + methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + + micromark-core-commonmark@1.1.0: + resolution: {integrity: sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==} + + micromark-core-commonmark@2.0.3: + resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + + micromark-extension-directive@3.0.2: + resolution: {integrity: sha512-wjcXHgk+PPdmvR58Le9d7zQYWy+vKEU9Se44p2CrCDPiLr2FMyiT4Fyb5UFKFC66wGB3kPlgD7q3TnoqPS7SZA==} + + micromark-extension-frontmatter@2.0.0: + resolution: {integrity: sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg==} + + micromark-extension-gfm-autolink-literal@1.0.5: + resolution: {integrity: sha512-z3wJSLrDf8kRDOh2qBtoTRD53vJ+CWIyo7uyZuxf/JAbNJjiHsOpG1y5wxk8drtv3ETAHutCu6N3thkOOgueWg==} + + micromark-extension-gfm-autolink-literal@2.1.0: + resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} + + micromark-extension-gfm-footnote@1.1.2: + resolution: {integrity: sha512-Yxn7z7SxgyGWRNa4wzf8AhYYWNrwl5q1Z8ii+CSTTIqVkmGZF1CElX2JI8g5yGoM3GAman9/PVCUFUSJ0kB/8Q==} + + micromark-extension-gfm-footnote@2.1.0: + resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} + + micromark-extension-gfm-strikethrough@1.0.7: + resolution: {integrity: sha512-sX0FawVE1o3abGk3vRjOH50L5TTLr3b5XMqnP9YDRb34M0v5OoZhG+OHFz1OffZ9dlwgpTBKaT4XW/AsUVnSDw==} + + micromark-extension-gfm-strikethrough@2.1.0: + resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==} + + micromark-extension-gfm-table@1.0.7: + resolution: {integrity: sha512-3ZORTHtcSnMQEKtAOsBQ9/oHp9096pI/UvdPtN7ehKvrmZZ2+bbWhi0ln+I9drmwXMt5boocn6OlwQzNXeVeqw==} + + micromark-extension-gfm-table@2.1.1: + resolution: {integrity: sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==} + + micromark-extension-gfm-tagfilter@1.0.2: + resolution: {integrity: sha512-5XWB9GbAUSHTn8VPU8/1DBXMuKYT5uOgEjJb8gN3mW0PNW5OPHpSdojoqf+iq1xo7vWzw/P8bAHY0n6ijpXF7g==} + + micromark-extension-gfm-tagfilter@2.0.0: + resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} + + micromark-extension-gfm-task-list-item@1.0.5: + resolution: {integrity: sha512-RMFXl2uQ0pNQy6Lun2YBYT9g9INXtWJULgbt01D/x8/6yJ2qpKyzdZD3pi6UIkzF++Da49xAelVKUeUMqd5eIQ==} + + micromark-extension-gfm-task-list-item@2.1.0: + resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==} + + micromark-extension-gfm@2.0.3: + resolution: {integrity: sha512-vb9OoHqrhCmbRidQv/2+Bc6pkP0FrtlhurxZofvOEy5o8RtuuvTq+RQ1Vw5ZDNrVraQZu3HixESqbG+0iKk/MQ==} + + micromark-extension-gfm@3.0.0: + resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + + micromark-extension-mdx-expression@1.0.8: + resolution: {integrity: sha512-zZpeQtc5wfWKdzDsHRBY003H2Smg+PUi2REhqgIhdzAa5xonhP03FcXxqFSerFiNUr5AWmHpaNPQTBVOS4lrXw==} + + micromark-extension-mdx-expression@3.0.1: + resolution: {integrity: sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==} + + micromark-extension-mdx-jsx@1.0.5: + resolution: {integrity: sha512-gPH+9ZdmDflbu19Xkb8+gheqEDqkSpdCEubQyxuz/Hn8DOXiXvrXeikOoBA71+e8Pfi0/UYmU3wW3H58kr7akA==} + + micromark-extension-mdx-jsx@3.0.2: + resolution: {integrity: sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ==} + + micromark-extension-mdx-md@1.0.1: + resolution: {integrity: sha512-7MSuj2S7xjOQXAjjkbjBsHkMtb+mDGVW6uI2dBL9snOBCbZmoNgDAeZ0nSn9j3T42UE/g2xVNMn18PJxZvkBEA==} + + micromark-extension-mdx-md@2.0.0: + resolution: {integrity: sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==} + + micromark-extension-mdxjs-esm@1.0.5: + resolution: {integrity: sha512-xNRBw4aoURcyz/S69B19WnZAkWJMxHMT5hE36GtDAyhoyn/8TuAeqjFJQlwk+MKQsUD7b3l7kFX+vlfVWgcX1w==} + + micromark-extension-mdxjs-esm@3.0.0: + resolution: {integrity: sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==} + + micromark-extension-mdxjs@1.0.1: + resolution: {integrity: sha512-7YA7hF6i5eKOfFUzZ+0z6avRG52GpWR8DL+kN47y3f2KhxbBZMhmxe7auOeaTBrW2DenbbZTf1ea9tA2hDpC2Q==} + + micromark-extension-mdxjs@3.0.0: + resolution: {integrity: sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==} + + micromark-factory-destination@1.1.0: + resolution: {integrity: sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@1.1.0: + resolution: {integrity: sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-mdx-expression@1.0.9: + resolution: {integrity: sha512-jGIWzSmNfdnkJq05c7b0+Wv0Kfz3NJ3N4cBjnbO4zjXIlxJr+f8lk+5ZmwFvqdAbUy2q6B5rCY//g0QAAaXDWA==} + + micromark-factory-mdx-expression@2.0.3: + resolution: {integrity: sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==} + + micromark-factory-space@1.1.0: + resolution: {integrity: sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@1.1.0: + resolution: {integrity: sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@1.1.0: + resolution: {integrity: sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@1.2.0: + resolution: {integrity: sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@1.1.0: + resolution: {integrity: sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@1.1.0: + resolution: {integrity: sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@1.1.0: + resolution: {integrity: sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@1.1.0: + resolution: {integrity: sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@1.1.0: + resolution: {integrity: sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + + micromark-util-encode@1.1.0: + resolution: {integrity: sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-events-to-acorn@1.2.3: + resolution: {integrity: sha512-ij4X7Wuc4fED6UoLWkmo0xJQhsktfNh1J0m8g4PbIMPlx+ek/4YdW5mvbye8z/aZvAPUoxgXHrwVlXAPKMRp1w==} + + micromark-util-events-to-acorn@2.0.3: + resolution: {integrity: sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==} + + micromark-util-html-tag-name@1.2.0: + resolution: {integrity: sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@1.1.0: + resolution: {integrity: sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@1.1.0: + resolution: {integrity: sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@1.2.0: + resolution: {integrity: sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@1.1.0: + resolution: {integrity: sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==} + + micromark-util-subtokenize@2.1.0: + resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} + + micromark-util-symbol@1.1.0: + resolution: {integrity: sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@1.1.0: + resolution: {integrity: sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + micromark@3.2.0: + resolution: {integrity: sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==} + + micromark@4.0.2: + resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.33.0: + resolution: {integrity: sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==} + engines: {node: '>= 0.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-format@2.0.1: + resolution: {integrity: sha512-XxU3ngPbEnrYnNbIX+lYSaYg0M01v6p2ntd2YaFksTu0vayaw5OJvbdRyWs07EYRlLED5qadUZ+xo+XhOvFhwg==} + + mime-types@2.1.18: + resolution: {integrity: sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + + mime@2.6.0: + resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} + engines: {node: '>=4.0.0'} + hasBin: true + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + mimic-fn@4.0.0: + resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} + engines: {node: '>=12'} + + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + + mimic-response@4.0.0: + resolution: {integrity: sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + + mini-css-extract-plugin@2.7.6: + resolution: {integrity: sha512-Qk7HcgaPkGG6eD77mLvZS1nmxlao3j+9PkrT9Uc7HAE1id3F41+DdBRYRYkbyfNRGzm8/YWtzhw7nVPmwhqTQw==} + engines: {node: '>= 12.13.0'} + peerDependencies: + webpack: ^5.0.0 + + mini-css-extract-plugin@2.9.2: + resolution: {integrity: sha512-GJuACcS//jtq4kCtd5ii/M0SZf7OZRH+BxdqXZHaJfb8TJiVl+NgQRPwiYt2EuqeSkNydn/7vP+bcE27C5mb9w==} + engines: {node: '>= 12.13.0'} + peerDependencies: + webpack: ^5.0.0 + + mini-svg-data-uri@1.4.4: + resolution: {integrity: sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==} + hasBin: true + + minimalistic-assert@1.0.1: + resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} + + minimatch@10.0.3: + resolution: {integrity: sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==} + engines: {node: 20 || >=22} + + minimatch@3.0.5: + resolution: {integrity: sha512-tUpxzX0VAzJHjLu0xUfFv1gwVp9ba3IOuRAVH2EGuRW8a5emA2FlACLqiT/lDVtS1W+TGNwqz3sWaNyLgDJWuw==} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@5.1.6: + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + engines: {node: '>=10'} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass-collect@1.0.2: + resolution: {integrity: sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==} + engines: {node: '>= 8'} + + minipass-fetch@2.1.2: + resolution: {integrity: sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + minipass-fetch@3.0.5: + resolution: {integrity: sha512-2N8elDQAtSnFV0Dk7gt15KHsS0Fyz6CbYZ360h0WTYV1Ty46li3rAXVOQj1THMNLdmrD9Vt5pBPtWtVkpwGBqg==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + minipass-flush@1.0.5: + resolution: {integrity: sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==} + engines: {node: '>= 8'} + + minipass-json-stream@1.0.2: + resolution: {integrity: sha512-myxeeTm57lYs8pH2nxPzmEEg8DGIgW+9mv6D4JZD2pa81I/OBjeU7PtICXV6c9eRGTA5JMDsuIPUZRCyBMYNhg==} + + minipass-pipeline@1.2.4: + resolution: {integrity: sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==} + engines: {node: '>=8'} + + minipass-sized@1.0.3: + resolution: {integrity: sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==} + engines: {node: '>=8'} + + minipass@3.3.6: + resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} + engines: {node: '>=8'} + + minipass@5.0.0: + resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} + engines: {node: '>=8'} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + minizlib@2.1.2: + resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} + engines: {node: '>= 8'} + + mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + + mkdirp@0.5.6: + resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} + hasBin: true + + mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + + mlly@1.7.4: + resolution: {integrity: sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==} + + mocha@11.7.1: + resolution: {integrity: sha512-5EK+Cty6KheMS/YLPPMJC64g5V61gIR25KsRItHw6x4hEKT6Njp1n9LOlH4gpevuwMVS66SXaBBpg+RWZkza4A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + + mochawesome-report-generator@6.2.0: + resolution: {integrity: sha512-Ghw8JhQFizF0Vjbtp9B0i//+BOkV5OWcQCPpbO0NGOoxV33o+gKDYU0Pr2pGxkIHnqZ+g5mYiXF7GMNgAcDpSg==} + hasBin: true + + mochawesome@7.1.3: + resolution: {integrity: sha512-Vkb3jR5GZ1cXohMQQ73H3cZz7RoxGjjUo0G5hu0jLaW+0FdUxUwg3Cj29bqQdh0rFcnyV06pWmqmi5eBPnEuNQ==} + peerDependencies: + mocha: '>=7' + + moment@2.30.1: + resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==} + + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + + mrmime@1.0.1: + resolution: {integrity: sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==} + engines: {node: '>=10'} + + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + multicast-dns@7.2.5: + resolution: {integrity: sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==} + hasBin: true + + mustache@4.2.0: + resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} + hasBin: true + + mute-stream@0.0.8: + resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} + + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + napi-postinstall@0.3.2: + resolution: {integrity: sha512-tWVJxJHmBWLy69PvO96TZMZDrzmw5KeiZBz3RHmiM2XZ9grBJ2WgMAFVVg25nqp3ZjTFUs2Ftw1JhscL3Teliw==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + hasBin: true + + natural-compare-lite@1.4.0: + resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==} + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + needle@3.3.1: + resolution: {integrity: sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==} + engines: {node: '>= 4.4.x'} + hasBin: true + + negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + + negotiator@0.6.4: + resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} + engines: {node: '>= 0.6'} + + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + + neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + + neotraverse@0.6.15: + resolution: {integrity: sha512-HZpdkco+JeXq0G+WWpMJ4NsX3pqb5O7eR9uGz3FfoFt+LYzU8iRWp49nJtud6hsDoywM8tIrDo3gjgmOqJA8LA==} + engines: {node: '>= 10'} + + netmask@2.0.2: + resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==} + engines: {node: '>= 0.4.0'} + + next-intl@3.26.5: + resolution: {integrity: sha512-EQlCIfY0jOhRldiFxwSXG+ImwkQtDEfQeSOEQp6ieAGSLWGlgjdb/Ck/O7wMfC430ZHGeUKVKax8KGusTPKCgg==} + peerDependencies: + next: ^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0 + + next-themes@0.2.1: + resolution: {integrity: sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A==} + peerDependencies: + next: '*' + react: '*' + react-dom: '*' + + next@15.4.0-canary.86: + resolution: {integrity: sha512-lGeO0sOvPZ7oFIklqRA863YzRL1bW+kT/OqU3N6RBquHldiucZwnZKQceZdn6WcHEFmWIHzZV+SMG1JEK7hZLg==} + engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@playwright/test': ^1.51.1 + babel-plugin-react-compiler: '*' + react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@playwright/test': + optional: true + babel-plugin-react-compiler: + optional: true + sass: + optional: true + + ngx-color@9.0.0: + resolution: {integrity: sha512-zyAFux+FRI4cACZ7g8DQQsBbNMhqmFkhtUPaxhkiVHhPzWU1iqXP8MqWH6By3guNOCch5oYrYNBWlHToklbdDg==} + peerDependencies: + '@angular/common': '>=16.0.0-0' + '@angular/core': '>=16.0.0-0' + + nice-grpc-common@2.0.2: + resolution: {integrity: sha512-7RNWbls5kAL1QVUOXvBsv1uO0wPQK3lHv+cY1gwkTzirnG1Nop4cBJZubpgziNbaVc/bl9QJcyvsf/NQxa3rjQ==} + + nice-grpc@2.0.1: + resolution: {integrity: sha512-Q5CGXO08STsv+HAkXeFgRayANT62X1LnIDhNXdCf+LP0XaP7EiHM0Cr3QefnoFjDZAx/Kxq+qiQfY66BrtKcNQ==} + + nice-napi@1.0.2: + resolution: {integrity: sha512-px/KnJAJZf5RuBGcfD+Sp2pAKq0ytz8j+1NehvgIGFkvtvFrDM3T8E4x/JJODXK9WZow8RRGrbA9QQ3hs+pDhA==} + os: ['!win32'] + + no-case@3.0.4: + resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} + + node-addon-api@3.2.1: + resolution: {integrity: sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==} + + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + + node-emoji@2.2.0: + resolution: {integrity: sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==} + engines: {node: '>=18'} + + node-fetch-h2@2.3.0: + resolution: {integrity: sha512-ofRW94Ab0T4AOh5Fk8t0h8OBWrmjb0SSB20xh1H8YnPV9EJ+f5AMoYSUQ2zgJ4Iq2HAK0I2l5/Nequ8YzFS3Hg==} + engines: {node: 4.x || >=6.0.0} + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + node-forge@1.3.1: + resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==} + engines: {node: '>= 6.13.0'} + + node-gyp-build@4.8.4: + resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} + hasBin: true + + node-gyp@9.4.1: + resolution: {integrity: sha512-OQkWKbjQKbGkMf/xqI1jjy3oCTgMKJac58G2+bjZb3fza6gW2YrCSdMQYaoTb70crvE//Gngr4f0AgVHmqHvBQ==} + engines: {node: ^12.13 || ^14.13 || >=16} + hasBin: true + + node-readfiles@0.2.0: + resolution: {integrity: sha512-SU00ZarexNlE4Rjdm83vglt5Y9yiQ+XI1XpflWlb7q7UTN1JUItm69xMeiQCTxtTfnzt+83T8Cx+vI2ED++VDA==} + + node-releases@2.0.19: + resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + + nodemon@3.1.10: + resolution: {integrity: sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==} + engines: {node: '>=10'} + hasBin: true + + nopt@5.0.0: + resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==} + engines: {node: '>=6'} + hasBin: true + + nopt@6.0.0: + resolution: {integrity: sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + hasBin: true + + normalize-package-data@3.0.3: + resolution: {integrity: sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==} + engines: {node: '>=10'} + + normalize-package-data@5.0.0: + resolution: {integrity: sha512-h9iPVIfrVZ9wVYQnxFgtw1ugSvGEMOlyPWWtm8BMJhnwyEL/FLbYbTY3V3PpjI/BUK67n9PEWDu6eHzu1fB15Q==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + normalize-range@0.1.2: + resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} + engines: {node: '>=0.10.0'} + + normalize-url@8.0.2: + resolution: {integrity: sha512-Ee/R3SyN4BuynXcnTaekmaVdbDAEiNrHqjQIA37mHU8G9pf7aaAD4ZX3XjBLo6rsdcxA/gtkcNYZLt30ACgynw==} + engines: {node: '>=14.16'} + + npm-bundled@3.0.1: + resolution: {integrity: sha512-+AvaheE/ww1JEwRHOrn4WHNzOxGtVp+adrg2AeZS/7KuxGUYFuBta98wYpfHBbJp6Tg6j1NKSEVHNcfZzJHQwQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + npm-install-checks@6.3.0: + resolution: {integrity: sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + npm-normalize-package-bin@3.0.1: + resolution: {integrity: sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + npm-package-arg@10.1.0: + resolution: {integrity: sha512-uFyyCEmgBfZTtrKk/5xDfHp6+MdrqGotX/VoOyEEl3mBwiEE5FlBaePanazJSVMPT7vKepcjYBY2ztg9A3yPIA==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + npm-packlist@7.0.4: + resolution: {integrity: sha512-d6RGEuRrNS5/N84iglPivjaJPxhDbZmlbTwTDX2IbcRHG5bZCdtysYMhwiPvcF4GisXHGn7xsxv+GQ7T/02M5Q==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + npm-pick-manifest@8.0.1: + resolution: {integrity: sha512-mRtvlBjTsJvfCCdmPtiu2bdlx8d/KXtF7yNXNWe7G0Z36qWA9Ny5zXsI2PfBZEv7SXgoxTmNaTzGSbbzDZChoA==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + npm-registry-fetch@14.0.5: + resolution: {integrity: sha512-kIDMIo4aBm6xg7jOttupWZamsZRkAqMqwqqbVXnUqstY5+tapvv6bkH/qMR76jdgV+YljEUCyWx3hRYMrJiAgA==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + + npm-run-path@5.3.0: + resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + npmlog@5.0.1: + resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==} + deprecated: This package is no longer supported. + + npmlog@6.0.2: + resolution: {integrity: sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + deprecated: This package is no longer supported. + + nprogress@0.2.0: + resolution: {integrity: sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==} + + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + + null-loader@4.0.1: + resolution: {integrity: sha512-pxqVbi4U6N26lq+LmgIbB5XATP0VdZKOG25DhHi8btMmJJefGArFyDg1yc4U3hWCJbMqSrw0qyrz1UQX+qYXqg==} + engines: {node: '>= 10.13.0'} + peerDependencies: + webpack: ^4.0.0 || ^5.0.0 + + nwsapi@2.2.20: + resolution: {integrity: sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==} + + nx@16.5.1: + resolution: {integrity: sha512-I3hJRE4hG7JWAtncWwDEO3GVeGPpN0TtM8xH5ArZXyDuVeTth/i3TtJzdDzqXO1HHtIoAQN0xeq4n9cLuMil5g==} + hasBin: true + peerDependencies: + '@swc-node/register': ^1.4.2 + '@swc/core': ^1.2.173 + peerDependenciesMeta: + '@swc-node/register': + optional: true + '@swc/core': + optional: true + + oas-kit-common@1.0.8: + resolution: {integrity: sha512-pJTS2+T0oGIwgjGpw7sIRU8RQMcUoKCDWFLdBqKB2BNmGpbBMH2sdqAaOXUg8OzonZHU0L7vfJu1mJFEiYDWOQ==} + + oas-linter@3.2.2: + resolution: {integrity: sha512-KEGjPDVoU5K6swgo9hJVA/qYGlwfbFx+Kg2QB/kd7rzV5N8N5Mg6PlsoCMohVnQmo+pzJap/F610qTodKzecGQ==} + + oas-resolver-browser@2.5.6: + resolution: {integrity: sha512-Jw5elT/kwUJrnGaVuRWe1D7hmnYWB8rfDDjBnpQ+RYY/dzAewGXeTexXzt4fGEo6PUE4eqKqPWF79MZxxvMppA==} + hasBin: true + + oas-resolver@2.5.6: + resolution: {integrity: sha512-Yx5PWQNZomfEhPPOphFbZKi9W93CocQj18NlD2Pa4GWZzdZpSJvYwoiuurRI7m3SpcChrnO08hkuQDL3FGsVFQ==} + hasBin: true + + oas-schema-walker@1.1.5: + resolution: {integrity: sha512-2yucenq1a9YPmeNExoUa9Qwrt9RFkjqaMAA1X+U7sbb0AqBeTIdMHky9SQQ6iN94bO5NW0W4TRYXerG+BdAvAQ==} + + oas-validator@5.0.8: + resolution: {integrity: sha512-cu20/HE5N5HKqVygs3dt94eYJfBi0TsZvPVXDhbXQHiEityDN+RROTleefoKRKKJ9dFAF2JBkDHgvWj0sjKGmw==} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object-path@0.11.8: + resolution: {integrity: sha512-YJjNZrlXJFM42wTBn6zgOJVar9KFJvzx6sTWDte8sWZF//cnjl0BxHNpfZx+ZffXX63A9q0b1zsFiBX4g4X5KA==} + engines: {node: '>= 10.12.0'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + + object.entries@1.1.9: + resolution: {integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==} + engines: {node: '>= 0.4'} + + object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} + + object.groupby@1.0.3: + resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} + engines: {node: '>= 0.4'} + + object.values@1.2.1: + resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} + engines: {node: '>= 0.4'} + + obuf@1.1.2: + resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} + + on-finished@2.3.0: + resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==} + engines: {node: '>= 0.8'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + on-headers@1.1.0: + resolution: {integrity: sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + onetime@6.0.0: + resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} + engines: {node: '>=12'} + + onetime@7.0.0: + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} + + open@8.4.2: + resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} + engines: {node: '>=12'} + + openai@4.78.1: + resolution: {integrity: sha512-drt0lHZBd2lMyORckOXFPQTmnGLWSLt8VK0W9BhOKWpMFBEoHMoz5gxMPmVq5icp+sOrsbMnsmZTVHUlKvD1Ow==} + hasBin: true + peerDependencies: + zod: ^3.23.8 + peerDependenciesMeta: + zod: + optional: true + + openapi-to-postmanv2@4.25.0: + resolution: {integrity: sha512-sIymbkQby0gzxt2Yez8YKB6hoISEel05XwGwNrAhr6+vxJWXNxkmssQc/8UEtVkuJ9ZfUXLkip9PYACIpfPDWg==} + engines: {node: '>=8'} + hasBin: true + + opener@1.5.2: + resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==} + hasBin: true + + opentype.js@1.3.4: + resolution: {integrity: sha512-d2JE9RP/6uagpQAVtJoF0pJJA/fgai89Cc50Yp0EJHk+eLp6QQ7gBoblsnubRULNY132I0J1QKMJ+JTbMqz4sw==} + engines: {node: '>= 8.0.0'} + hasBin: true + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + ora@5.4.1: + resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} + engines: {node: '>=10'} + + os-tmpdir@1.0.2: + resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} + engines: {node: '>=0.10.0'} + + ospath@1.2.2: + resolution: {integrity: sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==} + + outdent@0.5.0: + resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} + + own-keys@1.0.1: + resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} + engines: {node: '>= 0.4'} + + oxc-resolver@11.6.0: + resolution: {integrity: sha512-Yj3Wy+zLljtFL8ByKOljaPhiXjJWVe875p5MHaT5VAHoEmzeg1BuswM8s/E7ErpJ3s0fsXJfUYJE4v1bl7N65g==} + + p-cancelable@3.0.0: + resolution: {integrity: sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==} + engines: {node: '>=12.20'} + + p-filter@2.1.0: + resolution: {integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==} + engines: {node: '>=8'} + + p-filter@4.1.0: + resolution: {integrity: sha512-37/tPdZ3oJwHaS3gNJdenCDB3Tz26i9sjhnguBtvN0vYlRIiDNnvTWkuh+0hETV9rLPdJ3rlL3yVOYPIAnM8rw==} + engines: {node: '>=18'} + + p-finally@1.0.0: + resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} + engines: {node: '>=4'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-limit@4.0.0: + resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + p-locate@6.0.0: + resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + p-map@2.1.0: + resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} + engines: {node: '>=6'} + + p-map@4.0.0: + resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} + engines: {node: '>=10'} + + p-map@7.0.3: + resolution: {integrity: sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA==} + engines: {node: '>=18'} + + p-queue@6.6.2: + resolution: {integrity: sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==} + engines: {node: '>=8'} + + p-retry@4.6.2: + resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==} + engines: {node: '>=8'} + + p-timeout@3.2.0: + resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==} + engines: {node: '>=8'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + pac-proxy-agent@7.2.0: + resolution: {integrity: sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==} + engines: {node: '>= 14'} + + pac-resolver@7.0.1: + resolution: {integrity: sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==} + engines: {node: '>= 14'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + package-json@8.1.1: + resolution: {integrity: sha512-cbH9IAIJHNj9uXi196JVsRlt7cHKak6u/e6AkL/bkRelZ7rlL3X1YKxsZwa36xipOEKAsdtmaG6aAJoM1fx2zA==} + engines: {node: '>=14.16'} + + package-manager-detector@0.2.11: + resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==} + + package-manager-detector@1.3.0: + resolution: {integrity: sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ==} + + pacote@15.2.0: + resolution: {integrity: sha512-rJVZeIwHTUta23sIZgEIM62WYwbmGbThdbnkt81ravBplQv+HjyroqnLRNH2+sLJHcGZmLRmhPwACqhfTcOmnA==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + hasBin: true + + pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + + pako@2.1.0: + resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==} + + param-case@3.0.4: + resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-entities@4.0.2: + resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + parse-node-version@1.0.1: + resolution: {integrity: sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==} + engines: {node: '>= 0.10'} + + parse-numeric-range@1.3.0: + resolution: {integrity: sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==} + + parse5-html-rewriting-stream@7.0.0: + resolution: {integrity: sha512-mazCyGWkmCRWDI15Zp+UiCqMp/0dgEmkZRvhlsqqKYr4SsVm/TvnSpD9fCvqCA2zoWJcfRym846ejWBBHRiYEg==} + + parse5-htmlparser2-tree-adapter@7.1.0: + resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==} + + parse5-sax-parser@7.0.0: + resolution: {integrity: sha512-5A+v2SNsq8T6/mG3ahcz8ZtQ0OUFTatxPbeidoMB7tkJSGDY3tdfl4MHovtLQHkEn5CGxijNWRQHhRQ6IRpXKg==} + + parse5@6.0.1: + resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==} + + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + pascal-case@3.1.2: + resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + + path-data-parser@0.1.0: + resolution: {integrity: sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-exists@5.0.0: + resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-is-inside@1.0.2: + resolution: {integrity: sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-root-regex@0.1.2: + resolution: {integrity: sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ==} + engines: {node: '>=0.10.0'} + + path-root@0.1.1: + resolution: {integrity: sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg==} + engines: {node: '>=0.10.0'} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + path-scurry@2.0.0: + resolution: {integrity: sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==} + engines: {node: 20 || >=22} + + path-to-regexp@0.1.12: + resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} + + path-to-regexp@1.9.0: + resolution: {integrity: sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==} + + path-to-regexp@3.3.0: + resolution: {integrity: sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + path@0.12.7: + resolution: {integrity: sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==} + + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + + pause-stream@0.0.11: + resolution: {integrity: sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==} + + pend@1.2.0: + resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + + performance-now@2.1.0: + resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} + + pg-cloudflare@1.2.7: + resolution: {integrity: sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==} + + pg-connection-string@2.9.1: + resolution: {integrity: sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==} + + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-pool@3.10.1: + resolution: {integrity: sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==} + peerDependencies: + pg: '>=8.0' + + pg-protocol@1.10.3: + resolution: {integrity: sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + + pg@8.16.3: + resolution: {integrity: sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==} + engines: {node: '>= 16.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + + pgpass@1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pidtree@0.6.0: + resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==} + engines: {node: '>=0.10'} + hasBin: true + + pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + + pify@4.0.1: + resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} + engines: {node: '>=6'} + + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + + piscina@4.0.0: + resolution: {integrity: sha512-641nAmJS4k4iqpNUqfggqUBUMmlw0ZoM5VZKdQkV2e970Inn3Tk9kroCc1wpsYLD07vCwpys5iY0d3xI/9WkTg==} + + pkg-dir@7.0.0: + resolution: {integrity: sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==} + engines: {node: '>=14.16'} + + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + pkg-types@2.2.0: + resolution: {integrity: sha512-2SM/GZGAEkPp3KWORxQZns4M+WSeXbC2HEvmOIJe3Cmiv6ieAJvdVhDldtHqM5J1Y7MrR1XhkBT/rMlhh9FdqQ==} + + playwright-core@1.54.1: + resolution: {integrity: sha512-Nbjs2zjj0htNhzgiy5wu+3w09YetDx5pkrpI/kZotDlDUaYk0HVA5xrBVPdow4SAUIlhgKcJeJg4GRKW6xHusA==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.54.1: + resolution: {integrity: sha512-peWpSwIBmSLi6aW2auvrUtf2DqY16YYcCMO8rTVx486jKmDTJg7UAhyrraP98GB8BoPURZP8+nxO7TSd4cPr5g==} + engines: {node: '>=18'} + hasBin: true + + pluralize@8.0.0: + resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} + engines: {node: '>=4'} + + pngjs@5.0.0: + resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} + engines: {node: '>=10.13.0'} + + points-on-curve@0.2.0: + resolution: {integrity: sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==} + + points-on-path@0.2.1: + resolution: {integrity: sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==} + + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + + postcss-attribute-case-insensitive@7.0.1: + resolution: {integrity: sha512-Uai+SupNSqzlschRyNx3kbCTWgY/2hcwtHEI/ej2LJWc9JJ77qKgGptd8DHwY1mXtZ7Aoh4z4yxfwMBue9eNgw==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + postcss-calc@9.0.1: + resolution: {integrity: sha512-TipgjGyzP5QzEhsOZUaIkeO5mKeMFpebWzRogWG/ysonUlnHcq5aJe0jOjpfzUU8PeSaBQnrE8ehR0QA5vs8PQ==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.2.2 + + postcss-clamp@4.1.0: + resolution: {integrity: sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow==} + engines: {node: '>=7.6.0'} + peerDependencies: + postcss: ^8.4.6 + + postcss-color-functional-notation@7.0.10: + resolution: {integrity: sha512-k9qX+aXHBiLTRrWoCJuUFI6F1iF6QJQUXNVWJVSbqZgj57jDhBlOvD8gNUGl35tgqDivbGLhZeW3Ongz4feuKA==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + postcss-color-hex-alpha@10.0.0: + resolution: {integrity: sha512-1kervM2cnlgPs2a8Vt/Qbe5cQ++N7rkYo/2rz2BkqJZIHQwaVuJgQH38REHrAi4uM0b1fqxMkWYmese94iMp3w==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + postcss-color-rebeccapurple@10.0.0: + resolution: {integrity: sha512-JFta737jSP+hdAIEhk1Vs0q0YF5P8fFcj+09pweS8ktuGuZ8pPlykHsk6mPxZ8awDl4TrcxUqJo9l1IhVr/OjQ==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + postcss-colormin@6.1.0: + resolution: {integrity: sha512-x9yX7DOxeMAR+BgGVnNSAxmAj98NX/YxEMNFP+SDCEeNLb2r3i6Hh1ksMsnW8Ub5SLCpbescQqn9YEbE9554Sw==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-convert-values@6.1.0: + resolution: {integrity: sha512-zx8IwP/ts9WvUM6NkVSkiU902QZL1bwPhaVaLynPtCsOTqp+ZKbNi+s6XJg3rfqpKGA/oc7Oxk5t8pOQJcwl/w==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-custom-media@11.0.6: + resolution: {integrity: sha512-C4lD4b7mUIw+RZhtY7qUbf4eADmb7Ey8BFA2px9jUbwg7pjTZDl4KY4bvlUV+/vXQvzQRfiGEVJyAbtOsCMInw==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + postcss-custom-properties@14.0.6: + resolution: {integrity: sha512-fTYSp3xuk4BUeVhxCSJdIPhDLpJfNakZKoiTDx7yRGCdlZrSJR7mWKVOBS4sBF+5poPQFMj2YdXx1VHItBGihQ==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + postcss-custom-selectors@8.0.5: + resolution: {integrity: sha512-9PGmckHQswiB2usSO6XMSswO2yFWVoCAuih1yl9FVcwkscLjRKjwsjM3t+NIWpSU2Jx3eOiK2+t4vVTQaoCHHg==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + postcss-dir-pseudo-class@9.0.1: + resolution: {integrity: sha512-tRBEK0MHYvcMUrAuYMEOa0zg9APqirBcgzi6P21OhxtJyJADo/SWBwY1CAwEohQ/6HDaa9jCjLRG7K3PVQYHEA==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + postcss-discard-comments@6.0.2: + resolution: {integrity: sha512-65w/uIqhSBBfQmYnG92FO1mWZjJ4GL5b8atm5Yw2UgrwD7HiNiSSNwJor1eCFGzUgYnN/iIknhNRVqjrrpuglw==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-discard-duplicates@6.0.3: + resolution: {integrity: sha512-+JA0DCvc5XvFAxwx6f/e68gQu/7Z9ud584VLmcgto28eB8FqSFZwtrLwB5Kcp70eIoWP/HXqz4wpo8rD8gpsTw==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-discard-empty@6.0.3: + resolution: {integrity: sha512-znyno9cHKQsK6PtxL5D19Fj9uwSzC2mB74cpT66fhgOadEUPyXFkbgwm5tvc3bt3NAy8ltE5MrghxovZRVnOjQ==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-discard-overridden@6.0.2: + resolution: {integrity: sha512-j87xzI4LUggC5zND7KdjsI25APtyMuynXZSujByMaav2roV6OZX+8AaCUcZSWqckZpjAjRyFDdpqybgjFO0HJQ==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-discard-unused@6.0.5: + resolution: {integrity: sha512-wHalBlRHkaNnNwfC8z+ppX57VhvS+HWgjW508esjdaEYr3Mx7Gnn2xA4R/CKf5+Z9S5qsqC+Uzh4ueENWwCVUA==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-double-position-gradients@6.0.2: + resolution: {integrity: sha512-7qTqnL7nfLRyJK/AHSVrrXOuvDDzettC+wGoienURV8v2svNbu6zJC52ruZtHaO6mfcagFmuTGFdzRsJKB3k5Q==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + postcss-focus-visible@10.0.1: + resolution: {integrity: sha512-U58wyjS/I1GZgjRok33aE8juW9qQgQUNwTSdxQGuShHzwuYdcklnvK/+qOWX1Q9kr7ysbraQ6ht6r+udansalA==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + postcss-focus-within@9.0.1: + resolution: {integrity: sha512-fzNUyS1yOYa7mOjpci/bR+u+ESvdar6hk8XNK/TRR0fiGTp2QT5N+ducP0n3rfH/m9I7H/EQU6lsa2BrgxkEjw==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + postcss-font-variant@5.0.0: + resolution: {integrity: sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA==} + peerDependencies: + postcss: ^8.1.0 + + postcss-gap-properties@6.0.0: + resolution: {integrity: sha512-Om0WPjEwiM9Ru+VhfEDPZJAKWUd0mV1HmNXqp2C29z80aQ2uP9UVhLc7e3aYMIor/S5cVhoPgYQ7RtfeZpYTRw==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + postcss-image-set-function@7.0.0: + resolution: {integrity: sha512-QL7W7QNlZuzOwBTeXEmbVckNt1FSmhQtbMRvGGqqU4Nf4xk6KUEQhAoWuMzwbSv5jxiRiSZ5Tv7eiDB9U87znA==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + postcss-import@15.1.0: + resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} + engines: {node: '>=14.0.0'} + peerDependencies: + postcss: ^8.0.0 + + postcss-js@4.0.1: + resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} + engines: {node: ^12 || ^14 || >= 16} + peerDependencies: + postcss: ^8.4.21 + + postcss-lab-function@7.0.10: + resolution: {integrity: sha512-tqs6TCEv9tC1Riq6fOzHuHcZyhg4k3gIAMB8GGY/zA1ssGdm6puHMVE7t75aOSoFg7UD2wyrFFhbldiCMyyFTQ==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + postcss-load-config@4.0.2: + resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} + engines: {node: '>= 14'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + + postcss-load-config@6.0.1: + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + engines: {node: '>= 18'} + peerDependencies: + jiti: '>=1.21.0' + postcss: '>=8.0.9' + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + jiti: + optional: true + postcss: + optional: true + tsx: + optional: true + yaml: + optional: true + + postcss-loader@7.3.3: + resolution: {integrity: sha512-YgO/yhtevGO/vJePCQmTxiaEwER94LABZN0ZMT4A0vsak9TpO+RvKRs7EmJ8peIlB9xfXCsS7M8LjqncsUZ5HA==} + engines: {node: '>= 14.15.0'} + peerDependencies: + postcss: ^7.0.0 || ^8.0.1 + webpack: ^5.0.0 + + postcss-loader@7.3.4: + resolution: {integrity: sha512-iW5WTTBSC5BfsBJ9daFMPVrLT36MrNiC6fqOZTTaHjBNX6Pfd5p+hSBqe/fEeNd7pc13QiAyGt7VdGMw4eRC4A==} + engines: {node: '>= 14.15.0'} + peerDependencies: + postcss: ^7.0.0 || ^8.0.1 + webpack: ^5.0.0 + + postcss-logical@8.1.0: + resolution: {integrity: sha512-pL1hXFQ2fEXNKiNiAgtfA005T9FBxky5zkX6s4GZM2D8RkVgRqz3f4g1JUoq925zXv495qk8UNldDwh8uGEDoA==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + postcss-merge-idents@6.0.3: + resolution: {integrity: sha512-1oIoAsODUs6IHQZkLQGO15uGEbK3EAl5wi9SS8hs45VgsxQfMnxvt+L+zIr7ifZFIH14cfAeVe2uCTa+SPRa3g==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-merge-longhand@6.0.5: + resolution: {integrity: sha512-5LOiordeTfi64QhICp07nzzuTDjNSO8g5Ksdibt44d+uvIIAE1oZdRn8y/W5ZtYgRH/lnLDlvi9F8btZcVzu3w==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-merge-rules@6.1.1: + resolution: {integrity: sha512-KOdWF0gju31AQPZiD+2Ar9Qjowz1LTChSjFFbS+e2sFgc4uHOp3ZvVX4sNeTlk0w2O31ecFGgrFzhO0RSWbWwQ==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-minify-font-values@6.1.0: + resolution: {integrity: sha512-gklfI/n+9rTh8nYaSJXlCo3nOKqMNkxuGpTn/Qm0gstL3ywTr9/WRKznE+oy6fvfolH6dF+QM4nCo8yPLdvGJg==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-minify-gradients@6.0.3: + resolution: {integrity: sha512-4KXAHrYlzF0Rr7uc4VrfwDJ2ajrtNEpNEuLxFgwkhFZ56/7gaE4Nr49nLsQDZyUe+ds+kEhf+YAUolJiYXF8+Q==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-minify-params@6.1.0: + resolution: {integrity: sha512-bmSKnDtyyE8ujHQK0RQJDIKhQ20Jq1LYiez54WiaOoBtcSuflfK3Nm596LvbtlFcpipMjgClQGyGr7GAs+H1uA==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-minify-selectors@6.0.4: + resolution: {integrity: sha512-L8dZSwNLgK7pjTto9PzWRoMbnLq5vsZSTu8+j1P/2GB8qdtGQfn+K1uSvFgYvgh83cbyxT5m43ZZhUMTJDSClQ==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-modules-extract-imports@3.1.0: + resolution: {integrity: sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + + postcss-modules-local-by-default@4.2.0: + resolution: {integrity: sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + + postcss-modules-scope@3.2.1: + resolution: {integrity: sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + + postcss-modules-values@4.0.0: + resolution: {integrity: sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + + postcss-nested@6.2.0: + resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.2.14 + + postcss-nesting@13.0.2: + resolution: {integrity: sha512-1YCI290TX+VP0U/K/aFxzHzQWHWURL+CtHMSbex1lCdpXD1SoR2sYuxDu5aNI9lPoXpKTCggFZiDJbwylU0LEQ==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + postcss-normalize-charset@6.0.2: + resolution: {integrity: sha512-a8N9czmdnrjPHa3DeFlwqst5eaL5W8jYu3EBbTTkI5FHkfMhFZh1EGbku6jhHhIzTA6tquI2P42NtZ59M/H/kQ==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-normalize-display-values@6.0.2: + resolution: {integrity: sha512-8H04Mxsb82ON/aAkPeq8kcBbAtI5Q2a64X/mnRRfPXBq7XeogoQvReqxEfc0B4WPq1KimjezNC8flUtC3Qz6jg==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-normalize-positions@6.0.2: + resolution: {integrity: sha512-/JFzI441OAB9O7VnLA+RtSNZvQ0NCFZDOtp6QPFo1iIyawyXg0YI3CYM9HBy1WvwCRHnPep/BvI1+dGPKoXx/Q==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-normalize-repeat-style@6.0.2: + resolution: {integrity: sha512-YdCgsfHkJ2jEXwR4RR3Tm/iOxSfdRt7jplS6XRh9Js9PyCR/aka/FCb6TuHT2U8gQubbm/mPmF6L7FY9d79VwQ==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-normalize-string@6.0.2: + resolution: {integrity: sha512-vQZIivlxlfqqMp4L9PZsFE4YUkWniziKjQWUtsxUiVsSSPelQydwS8Wwcuw0+83ZjPWNTl02oxlIvXsmmG+CiQ==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-normalize-timing-functions@6.0.2: + resolution: {integrity: sha512-a+YrtMox4TBtId/AEwbA03VcJgtyW4dGBizPl7e88cTFULYsprgHWTbfyjSLyHeBcK/Q9JhXkt2ZXiwaVHoMzA==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-normalize-unicode@6.1.0: + resolution: {integrity: sha512-QVC5TQHsVj33otj8/JD869Ndr5Xcc/+fwRh4HAsFsAeygQQXm+0PySrKbr/8tkDKzW+EVT3QkqZMfFrGiossDg==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-normalize-url@6.0.2: + resolution: {integrity: sha512-kVNcWhCeKAzZ8B4pv/DnrU1wNh458zBNp8dh4y5hhxih5RZQ12QWMuQrDgPRw3LRl8mN9vOVfHl7uhvHYMoXsQ==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-normalize-whitespace@6.0.2: + resolution: {integrity: sha512-sXZ2Nj1icbJOKmdjXVT9pnyHQKiSAyuNQHSgRCUgThn2388Y9cGVDR+E9J9iAYbSbLHI+UUwLVl1Wzco/zgv0Q==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-opacity-percentage@3.0.0: + resolution: {integrity: sha512-K6HGVzyxUxd/VgZdX04DCtdwWJ4NGLG212US4/LA1TLAbHgmAsTWVR86o+gGIbFtnTkfOpb9sCRBx8K7HO66qQ==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + postcss-ordered-values@6.0.2: + resolution: {integrity: sha512-VRZSOB+JU32RsEAQrO94QPkClGPKJEL/Z9PCBImXMhIeK5KAYo6slP/hBYlLgrCjFxyqvn5VC81tycFEDBLG1Q==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-overflow-shorthand@6.0.0: + resolution: {integrity: sha512-BdDl/AbVkDjoTofzDQnwDdm/Ym6oS9KgmO7Gr+LHYjNWJ6ExORe4+3pcLQsLA9gIROMkiGVjjwZNoL/mpXHd5Q==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + postcss-page-break@3.0.4: + resolution: {integrity: sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ==} + peerDependencies: + postcss: ^8 + + postcss-place@10.0.0: + resolution: {integrity: sha512-5EBrMzat2pPAxQNWYavwAfoKfYcTADJ8AXGVPcUZ2UkNloUTWzJQExgrzrDkh3EKzmAx1evfTAzF9I8NGcc+qw==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + postcss-preset-env@10.2.4: + resolution: {integrity: sha512-q+lXgqmTMdB0Ty+EQ31SuodhdfZetUlwCA/F0zRcd/XdxjzI+Rl2JhZNz5US2n/7t9ePsvuhCnEN4Bmu86zXlA==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + postcss-pseudo-class-any-link@10.0.1: + resolution: {integrity: sha512-3el9rXlBOqTFaMFkWDOkHUTQekFIYnaQY55Rsp8As8QQkpiSgIYEcF/6Ond93oHiDsGb4kad8zjt+NPlOC1H0Q==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + postcss-reduce-idents@6.0.3: + resolution: {integrity: sha512-G3yCqZDpsNPoQgbDUy3T0E6hqOQ5xigUtBQyrmq3tn2GxlyiL0yyl7H+T8ulQR6kOcHJ9t7/9H4/R2tv8tJbMA==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-reduce-initial@6.1.0: + resolution: {integrity: sha512-RarLgBK/CrL1qZags04oKbVbrrVK2wcxhvta3GCxrZO4zveibqbRPmm2VI8sSgCXwoUHEliRSbOfpR0b/VIoiw==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-reduce-transforms@6.0.2: + resolution: {integrity: sha512-sB+Ya++3Xj1WaT9+5LOOdirAxP7dJZms3GRcYheSPi1PiTMigsxHAdkrbItHxwYHr4kt1zL7mmcHstgMYT+aiA==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-replace-overflow-wrap@4.0.0: + resolution: {integrity: sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw==} + peerDependencies: + postcss: ^8.0.3 + + postcss-selector-not@8.0.1: + resolution: {integrity: sha512-kmVy/5PYVb2UOhy0+LqUYAhKj7DUGDpSWa5LZqlkWJaaAV+dxxsOG3+St0yNLu6vsKD7Dmqx+nWQt0iil89+WA==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + + postcss-selector-parser@6.1.2: + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} + engines: {node: '>=4'} + + postcss-selector-parser@7.1.0: + resolution: {integrity: sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==} + engines: {node: '>=4'} + + postcss-sort-media-queries@5.2.0: + resolution: {integrity: sha512-AZ5fDMLD8SldlAYlvi8NIqo0+Z8xnXU2ia0jxmuhxAU+Lqt9K+AlmLNJ/zWEnE9x+Zx3qL3+1K20ATgNOr3fAA==} + engines: {node: '>=14.0.0'} + peerDependencies: + postcss: ^8.4.23 + + postcss-svgo@6.0.3: + resolution: {integrity: sha512-dlrahRmxP22bX6iKEjOM+c8/1p+81asjKT+V5lrgOH944ryx/OHpclnIbGsKVd3uWOXFLYJwCVf0eEkJGvO96g==} + engines: {node: ^14 || ^16 || >= 18} + peerDependencies: + postcss: ^8.4.31 + + postcss-unique-selectors@6.0.4: + resolution: {integrity: sha512-K38OCaIrO8+PzpArzkLKB42dSARtC2tmG6PvD4b1o1Q2E9Os8jzfWFfSy/rixsHwohtsDdFtAWGjFVFUdwYaMg==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss-zindex@6.0.2: + resolution: {integrity: sha512-5BxW9l1evPB/4ZIc+2GobEBoKC+h8gPGCMi+jxsYvd2x0mjq7wazk6DrP71pStqxE9Foxh5TVnonbWpFZzXaYg==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + postcss@8.4.31: + resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} + engines: {node: ^10 || ^12 || >=14} + + postcss@8.5.3: + resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==} + engines: {node: ^10 || ^12 || >=14} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-bytea@1.0.0: + resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==} + engines: {node: '>=0.10.0'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + + posthog-js@1.257.1: + resolution: {integrity: sha512-29kk3IO/LkPQ8E1cds6a2sWr5iN4BovgL+EMzRK9hQXbI6D3FJnQ7zLU6EUpktt6pHnqGpfO3BTEcflcDYkHBg==} + peerDependencies: + '@rrweb/types': 2.0.0-alpha.17 + rrweb-snapshot: 2.0.0-alpha.17 + peerDependenciesMeta: + '@rrweb/types': + optional: true + rrweb-snapshot: + optional: true + + postman-code-generators@1.14.2: + resolution: {integrity: sha512-qZAyyowfQAFE4MSCu2KtMGGQE/+oG1JhMZMJNMdZHYCSfQiVVeKxgk3oI4+KJ3d1y5rrm2D6C6x+Z+7iyqm+fA==} + engines: {node: '>=12'} + + postman-collection@4.5.0: + resolution: {integrity: sha512-152JSW9pdbaoJihwjc7Q8lc3nPg/PC9lPTHdMk7SHnHhu/GBJB7b2yb9zG7Qua578+3PxkQ/HYBuXpDSvsf7GQ==} + engines: {node: '>=10'} + + postman-url-encoder@3.0.5: + resolution: {integrity: sha512-jOrdVvzUXBC7C+9gkIkpDJ3HIxOHTIqjpQ4C1EMt1ZGeMvSEpbFCKq23DEfgsj46vMnDgyQf+1ZLp2Wm+bKSsA==} + engines: {node: '>=10'} + + preact@10.26.9: + resolution: {integrity: sha512-SSjF9vcnF27mJK1XyFMNJzFd5u3pQiATFqoaDy03XuN00u4ziveVVEGt5RKJrDR8MHE/wJo9Nnad56RLzS2RMA==} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier-plugin-organize-imports@3.2.4: + resolution: {integrity: sha512-6m8WBhIp0dfwu0SkgfOxJqh+HpdyfqSSLfKKRZSFbDuEQXDDndb8fTpRWkUrX/uBenkex3MgnVk0J3b3Y5byog==} + peerDependencies: + '@volar/vue-language-plugin-pug': ^1.0.4 + '@volar/vue-typescript': ^1.0.4 + prettier: '>=2.0' + typescript: '>=2.9' + peerDependenciesMeta: + '@volar/vue-language-plugin-pug': + optional: true + '@volar/vue-typescript': + optional: true + + prettier-plugin-organize-imports@4.2.0: + resolution: {integrity: sha512-Zdy27UhlmyvATZi67BTnLcKTo8fm6Oik59Sz6H64PgZJVs6NJpPD1mT240mmJn62c98/QaL+r3kx9Q3gRpDajg==} + peerDependencies: + prettier: '>=2.0' + typescript: '>=2.9' + vue-tsc: ^2.1.0 || 3 + peerDependenciesMeta: + vue-tsc: + optional: true + + prettier-plugin-tailwindcss@0.6.11: + resolution: {integrity: sha512-YxaYSIvZPAqhrrEpRtonnrXdghZg1irNg4qrjboCXrpybLWVs55cW2N3juhspVJiO0JBvYJT8SYsJpc8OQSnsA==} + engines: {node: '>=14.21.3'} + peerDependencies: + '@ianvs/prettier-plugin-sort-imports': '*' + '@prettier/plugin-pug': '*' + '@shopify/prettier-plugin-liquid': '*' + '@trivago/prettier-plugin-sort-imports': '*' + '@zackad/prettier-plugin-twig': '*' + 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: '*' + peerDependenciesMeta: + '@ianvs/prettier-plugin-sort-imports': + optional: true + '@prettier/plugin-pug': + optional: true + '@shopify/prettier-plugin-liquid': + optional: true + '@trivago/prettier-plugin-sort-imports': + optional: true + '@zackad/prettier-plugin-twig': + optional: true + prettier-plugin-astro: + optional: true + prettier-plugin-css-order: + optional: true + prettier-plugin-import-sort: + optional: true + 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@2.8.8: + resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} + engines: {node: '>=10.13.0'} + hasBin: true + + prettier@3.6.2: + resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} + engines: {node: '>=14'} + hasBin: true + + pretty-bytes@5.6.0: + resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==} + engines: {node: '>=6'} + + pretty-error@4.0.0: + resolution: {integrity: sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==} + + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + pretty-time@1.1.0: + resolution: {integrity: sha512-28iF6xPQrP8Oa6uxE6a1biz+lWeTOAPKggvjB8HAs6nVMKZwf5bG++632Dx614hIWgUPkgivRfG+a8uAXGTIbA==} + engines: {node: '>=4'} + + prism-react-renderer@2.4.1: + resolution: {integrity: sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig==} + peerDependencies: + react: '>=16.0.0' + + prismjs@1.30.0: + resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} + engines: {node: '>=6'} + + proc-log@3.0.0: + resolution: {integrity: sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + + progress@2.0.3: + resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} + engines: {node: '>=0.4.0'} + + promise-inflight@1.0.1: + resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==} + peerDependencies: + bluebird: '*' + peerDependenciesMeta: + bluebird: + optional: true + + promise-retry@2.0.1: + resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} + engines: {node: '>=10'} + + prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + + property-information@6.5.0: + resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==} + + property-information@7.1.0: + resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + + proto-list@1.2.4: + resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} + + protobufjs@7.5.3: + resolution: {integrity: sha512-sildjKwVqOI2kmFDiXQ6aEB0fjYTafpEvIBs8tOR8qI4spuL9OPROLVu2qZqi/xgCfsHIwVqlaF8JBjWFHnKbw==} + engines: {node: '>=12.0.0'} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + proxy-agent@6.5.0: + resolution: {integrity: sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==} + engines: {node: '>= 14'} + + proxy-compare@3.0.1: + resolution: {integrity: sha512-V9plBAt3qjMlS1+nC8771KNf6oJ12gExvaxnNzN/9yVRLdTv/lc+oJlnSzrdYDAvBfTStPCoiaCOTmTs0adv7Q==} + + proxy-from-env@1.0.0: + resolution: {integrity: sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + prr@1.0.1: + resolution: {integrity: sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==} + + ps-tree@1.2.0: + resolution: {integrity: sha512-0VnamPPYHl4uaU/nSFeZZpR21QAWRz+sRv4iW9+v/GS/J5U5iZB5BNN6J0RMoOvdx2gWM2+ZFMIm58q24e4UYA==} + engines: {node: '>= 0.10'} + hasBin: true + + psl@1.15.0: + resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} + + pstree.remy@1.1.8: + resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==} + + pump@3.0.3: + resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} + + punycode@1.4.1: + resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + pupa@3.1.0: + resolution: {integrity: sha512-FLpr4flz5xZTSJxSeaheeMKN/EDzMdK7b8PTOC6a5PYFKTucWbdqjgqaEyH0shFiSJrVB1+Qqi4Tk19ccU6Aug==} + engines: {node: '>=12.20'} + + puppeteer-core@22.15.0: + resolution: {integrity: sha512-cHArnywCiAAVXa3t4GGL2vttNxh7GqXtIYGym99egkNJ3oG//wL9LkvO4WE8W1TJe95t1F1ocu9X4xWaGsOKOA==} + engines: {node: '>=18'} + + puppeteer@22.15.0: + resolution: {integrity: sha512-XjCY1SiSEi1T7iSYuxS82ft85kwDJUS7wj1Z0eGVXKdtr5g4xnVcbjwxhq5xBnpK/E7x1VZZoJDxpjAOasHT4Q==} + engines: {node: '>=18'} + hasBin: true + + qjobs@1.2.0: + resolution: {integrity: sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==} + engines: {node: '>=0.9'} + + qrcode.react@3.2.0: + resolution: {integrity: sha512-YietHHltOHA4+l5na1srdaMx4sVSOjV9tamHs+mwiLWAMr6QVACRUw1Neax5CptFILcNoITctJY0Ipyn5enQ8g==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + + qrcode@1.5.3: + resolution: {integrity: sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg==} + engines: {node: '>=10.13.0'} + hasBin: true + + qs@6.13.0: + resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} + engines: {node: '>=0.6'} + + qs@6.14.0: + resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} + engines: {node: '>=0.6'} + + quansync@0.2.10: + resolution: {integrity: sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A==} + + querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + quick-lru@5.1.1: + resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} + engines: {node: '>=10'} + + randombytes@2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + + range-parser@1.2.0: + resolution: {integrity: sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==} + engines: {node: '>= 0.6'} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@2.5.2: + resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} + engines: {node: '>= 0.8'} + + raw-loader@4.0.2: + resolution: {integrity: sha512-ZnScIV3ag9A4wPX/ZayxL/jZH+euYb6FcUinPcgiQW0+UBtEv0O6Q3lGd3cqJ+GHH+rksEv3Pj99oxJ3u3VIKA==} + engines: {node: '>= 10.13.0'} + peerDependencies: + webpack: ^4.0.0 || ^5.0.0 + + rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + + react-copy-to-clipboard@5.1.0: + resolution: {integrity: sha512-k61RsNgAayIJNoy9yDsYzDe/yAZAzEbEgcz3DZMhF686LEyukcE1hzurxe85JandPUG+yTfGVFzuEw3xt8WP/A==} + peerDependencies: + react: ^15.3.0 || 16 || 17 || 18 + + react-dom@18.3.1: + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + + react-dom@19.1.0: + resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==} + peerDependencies: + react: ^19.1.0 + + react-error-boundary@6.0.0: + resolution: {integrity: sha512-gdlJjD7NWr0IfkPlaREN2d9uUZUlksrfOx7SX62VRerwXbMY6ftGCIZua1VG1aXFNOimhISsTq+Owp725b9SiA==} + peerDependencies: + react: '>=16.13.1' + + react-fast-compare@3.2.2: + resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} + + react-google-charts@5.2.1: + resolution: {integrity: sha512-mCbPiObP8yWM5A9ogej7Qp3/HX4EzOwuEzUYvcfHtL98Xt4V/brD14KgfDzSNNtyD48MNXCpq5oVaYKt0ykQUQ==} + peerDependencies: + react: '>=16.3.0' + react-dom: '>=16.3.0' + + react-hook-form@7.39.5: + resolution: {integrity: sha512-OE0HKyz5IPc6svN2wd+e+evidZrw4O4WZWAWYzQVZuHi+hYnHFSLnxOq0ddjbdmaLIsLHut/ab7j72y2QT3+KA==} + engines: {node: '>=12.22.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 + + react-hook-form@7.54.2: + resolution: {integrity: sha512-eHpAUgUjWbZocoQYUHposymRb4ZP6d0uwUnooL2uOybA9/3tPUvoAKqEWK1WaSiTxxOfTpffNZP7QwlnM3/gEg==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 || ^19 + + react-hook-form@7.60.0: + resolution: {integrity: sha512-SBrYOvMbDB7cV8ZfNpaiLcgjH/a1c7aK0lK+aNigpf4xWLO8q+o4tcvVurv3c4EOyzn/3dCsYt4GKD42VvJ/+A==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 || ^19 + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + + react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + + react-json-view-lite@2.4.1: + resolution: {integrity: sha512-fwFYknRIBxjbFm0kBDrzgBy1xa5tDg2LyXXBepC5f1b+MY3BUClMCsvanMPn089JbV1Eg3nZcrp0VCuH43aXnA==} + engines: {node: '>=18'} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + + react-lifecycles-compat@3.0.4: + resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==} + + react-live@4.1.8: + resolution: {integrity: sha512-B2SgNqwPuS2ekqj4lcxi5TibEcjWkdVyYykBEUBshPAPDQ527x2zPEZg560n8egNtAjUpwXFQm7pcXV65aAYmg==} + engines: {node: '>= 0.12.0', npm: '>= 2.0.0'} + peerDependencies: + react: '>=18.0.0' + react-dom: '>=18.0.0' + + react-loadable-ssr-addon-v5-slorber@1.0.1: + resolution: {integrity: sha512-lq3Lyw1lGku8zUEJPDxsNm1AfYHBrO9Y1+olAYwpUJ2IGFBskM0DMKok97A6LWUpHm+o7IvQBOWu9MLenp9Z+A==} + engines: {node: '>=10.13.0'} + peerDependencies: + react-loadable: '*' + webpack: '>=4.41.1 || 5.x' + + react-magic-dropzone@1.0.1: + resolution: {integrity: sha512-0BIROPARmXHpk4AS3eWBOsewxoM5ndk2psYP/JmbCq8tz3uR2LIV1XiroZ9PKrmDRMctpW+TvsBCtWasuS8vFA==} + + react-markdown@8.0.7: + resolution: {integrity: sha512-bvWbzG4MtOU62XqBx3Xx+zB2raaFFsq4mYiAzfjXJMEz2sixgeAfraA3tvzULF02ZdOMUOKTBFFaZJDDrq+BJQ==} + peerDependencies: + '@types/react': '>=16' + react: '>=16' + + react-markdown@9.0.3: + resolution: {integrity: sha512-Yk7Z94dbgYTOrdk41Z74GoKA7rThnsbbqBTRYuxoe08qvfQ9tJVhmAKw6BJS/ZORG7kTy/s1QvYzSuaoBA1qfw==} + peerDependencies: + '@types/react': '>=18' + react: '>=18' + + react-modal@3.16.3: + resolution: {integrity: sha512-yCYRJB5YkeQDQlTt17WGAgFJ7jr2QYcWa1SHqZ3PluDmnKJ/7+tVU+E6uKyZ0nODaeEj+xCpK4LcSnKXLMC0Nw==} + peerDependencies: + react: ^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18 || ^19 + react-dom: ^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18 || ^19 + + react-player@2.16.1: + resolution: {integrity: sha512-mxP6CqjSWjidtyDoMOSHVPdhX0pY16aSvw5fVr44EMaT7X5Xz46uQ4b/YBm1v2x+3hHkB9PmjEEkmbHb9PXQ4w==} + peerDependencies: + react: '>=16.6.0' + + react-redux@7.2.9: + resolution: {integrity: sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==} + peerDependencies: + react: ^16.8.3 || ^17 || ^18 + react-dom: '*' + react-native: '*' + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + + react-refresh@0.17.0: + resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} + engines: {node: '>=0.10.0'} + + react-remove-scroll-bar@2.3.8: + resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-remove-scroll@2.7.1: + resolution: {integrity: sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react-router-config@5.1.1: + resolution: {integrity: sha512-DuanZjaD8mQp1ppHjgnnUnyOlqYXZVjnov/JzFhjLEwd3Z4dYjMSnqrEzzGThH47vpCOqPPwJM2FtthLeJ8Pbg==} + peerDependencies: + react: '>=15' + react-router: '>=5' + + react-router-dom@5.3.4: + resolution: {integrity: sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==} + peerDependencies: + react: '>=15' + + react-router@5.3.4: + resolution: {integrity: sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==} + peerDependencies: + react: '>=15' + + react-style-singleton@2.2.3: + resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react-svg@16.3.0: + resolution: {integrity: sha512-MvoQbITgkmpPJYwDTNdiUyoncJFfoa0D86WzoZuMQ9c/ORJURPR6rPMnXDsLOWDCAyXuV9nKZhQhGyP0HZ0MVQ==} + peerDependencies: + react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + react-textarea-autosize@8.5.7: + resolution: {integrity: sha512-2MqJ3p0Jh69yt9ktFIaZmORHXw4c4bxSIhCeWiFwmJ9EYKgLmuNII3e9c9b2UO+ijl4StnpZdqpxNIhTdHvqtQ==} + engines: {node: '>=10'} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + + react@19.1.0: + resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==} + engines: {node: '>=0.10.0'} + + read-cache@1.0.0: + resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + + read-package-json-fast@3.0.2: + resolution: {integrity: sha512-0J+Msgym3vrLOUB3hzQCuZHII0xkNGCtz/HJH9xZshwv9DbDwkw1KaE3gx/e2J5rpEY5rtOy6cyhKOPrkP7FZw==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + read-package-json@6.0.4: + resolution: {integrity: sha512-AEtWXYfopBj2z5N5PbkAOeNHRPUg5q+Nen7QLxV8M2zJq1ym6/lCz3fYNTCXe19puu2d06jfHhrP7v/S2PtMMw==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + deprecated: This package is no longer supported. Please use @npmcli/package-json instead. + + read-pkg-up@9.1.0: + resolution: {integrity: sha512-vaMRR1AC1nrd5CQM0PhlRsO5oc2AAigqr7cCrZ/MW/Rsaflz4RlgzkpL4qoU/z1F6wrbd85iFv1OQj/y5RdGvg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + read-pkg@7.1.0: + resolution: {integrity: sha512-5iOehe+WF75IccPc30bWTbpdDQLOCc3Uu8bi3Dte3Eueij81yx1Mrufk8qBx/YAbR4uL1FdUr+7BKXDwEtisXg==} + engines: {node: '>=12.20'} + + read-yaml-file@1.1.0: + resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} + engines: {node: '>=6'} + + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + rechoir@0.6.2: + resolution: {integrity: sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==} + engines: {node: '>= 0.10'} + + recma-build-jsx@1.0.0: + resolution: {integrity: sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==} + + recma-jsx@1.0.0: + resolution: {integrity: sha512-5vwkv65qWwYxg+Atz95acp8DMu1JDSqdGkA2Of1j6rCreyFUE/gp15fC8MnGEuG1W68UKjM6x6+YTWIh7hZM/Q==} + + recma-parse@1.0.0: + resolution: {integrity: sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==} + + recma-stringify@1.0.0: + resolution: {integrity: sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==} + + redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + + redux-thunk@2.4.2: + resolution: {integrity: sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==} + peerDependencies: + redux: ^4 + + redux@4.2.1: + resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==} + + reflect-metadata@0.1.14: + resolution: {integrity: sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==} + + reflect.getprototypeof@1.0.10: + resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} + engines: {node: '>= 0.4'} + + reftools@1.1.9: + resolution: {integrity: sha512-OVede/NQE13xBQ+ob5CKd5KyeJYU2YInb1bmV4nRoOfquZPkAkxuOXicSe1PvqIuZZ4kD13sPKBbR7UFDmli6w==} + + regenerate-unicode-properties@10.2.0: + resolution: {integrity: sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==} + engines: {node: '>=4'} + + regenerate@1.4.2: + resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} + + regenerator-runtime@0.13.11: + resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} + + regex-parser@2.3.1: + resolution: {integrity: sha512-yXLRqatcCuKtVHsWrNg0JL3l1zGfdXeEvDa0bdu4tCDQw0RpMDZsqbkyRTUnKMR0tXF627V2oEWjBEaEdqTwtQ==} + + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + + regexpu-core@6.2.0: + resolution: {integrity: sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==} + engines: {node: '>=4'} + + registry-auth-token@5.1.0: + resolution: {integrity: sha512-GdekYuwLXLxMuFTwAPg5UKGLW/UXzQrZvH/Zj791BQif5T05T0RsaLfHc9q3ZOKi7n+BoprPD9mJ0O0k4xzUlw==} + engines: {node: '>=14'} + + registry-url@6.0.1: + resolution: {integrity: sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q==} + engines: {node: '>=12'} + + regjsgen@0.8.0: + resolution: {integrity: sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==} + + regjsparser@0.12.0: + resolution: {integrity: sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==} + hasBin: true + + rehype-minify-whitespace@6.0.2: + resolution: {integrity: sha512-Zk0pyQ06A3Lyxhe9vGtOtzz3Z0+qZ5+7icZ/PL/2x1SHPbKao5oB/g/rlc6BCTajqBb33JcOe71Ye1oFsuYbnw==} + + rehype-parse@9.0.1: + resolution: {integrity: sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==} + + rehype-raw@6.1.1: + resolution: {integrity: sha512-d6AKtisSRtDRX4aSPsJGTfnzrX2ZkHQLE5kiUuGOeEoLpbEulFF4hj0mLPbsa+7vmguDKOVVEQdHKDSwoaIDsQ==} + + rehype-raw@7.0.0: + resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==} + + rehype-recma@1.0.0: + resolution: {integrity: sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==} + + rehype-remark@10.0.1: + resolution: {integrity: sha512-EmDndlb5NVwXGfUa4c9GPK+lXeItTilLhE6ADSaQuHr4JUlKw9MidzGzx4HpqZrNCt6vnHmEifXQiiA+CEnjYQ==} + + relateurl@0.2.7: + resolution: {integrity: sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==} + engines: {node: '>= 0.10'} + + remark-directive@3.0.1: + resolution: {integrity: sha512-gwglrEQEZcZYgVyG1tQuA+h58EZfq5CSULw7J90AFuCTyib1thgHPoqQ+h9iFvU6R+vnZ5oNFQR5QKgGpk741A==} + + remark-emoji@4.0.1: + resolution: {integrity: sha512-fHdvsTR1dHkWKev9eNyhTo4EFwbUvJ8ka9SgeWkMPYFX4WoI7ViVBms3PjlQYgw5TLvNQso3GUB/b/8t3yo+dg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + remark-frontmatter@5.0.0: + resolution: {integrity: sha512-XTFYvNASMe5iPN0719nPrdItC9aU0ssC4v14mH1BCi1u0n1gAocqcujWUrByftZTbLhRtiKRyjYTSIOcr69UVQ==} + + remark-gfm@3.0.1: + resolution: {integrity: sha512-lEFDoi2PICJyNrACFOfDD3JlLkuSbOa5Wd8EPt06HUdptv8Gn0bxYTdbU/XXQ3swAPkEaGxxPN9cbnMHvVu1Ig==} + + remark-gfm@4.0.1: + resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} + + remark-mdx@3.1.0: + resolution: {integrity: sha512-Ngl/H3YXyBV9RcRNdlYsZujAmhsxwzxpDzpDEhFBVAGthS4GDgnctpDjgFl/ULx5UEDzqtW1cyBSNKqYYrqLBA==} + + remark-parse@10.0.2: + resolution: {integrity: sha512-3ydxgHa/ZQzG8LvC7jTXccARYDcRld3VfcgIIFs7bI6vbRSxJJmzgLEIIoYKyrfhaY+ujuWaf/PJiMZXoiCXgw==} + + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-rehype@10.1.0: + resolution: {integrity: sha512-EFmR5zppdBp0WQeDVZ/b66CWJipB2q2VLNFMabzDSGR66Z2fQii83G5gTBbgGEnEEA0QRussvrFHxk1HWGJskw==} + + remark-rehype@11.1.2: + resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} + + remark-stringify@11.0.0: + resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + + renderkid@3.0.0: + resolution: {integrity: sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==} + + repeat-string@1.6.1: + resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} + engines: {node: '>=0.10'} + + request-progress@3.0.0: + resolution: {integrity: sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg==} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + require-like@0.1.2: + resolution: {integrity: sha512-oyrU88skkMtDdauHDuKVrgR+zuItqr6/c//FXzvmxRGMexSDc6hNvJInGW3LL46n+8b50RykrvwSUIIQH2LQ5A==} + + require-main-filename@2.0.0: + resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + + reselect@4.1.8: + resolution: {integrity: sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==} + + resolve-alpn@1.2.1: + resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + resolve-package-path@4.0.3: + resolution: {integrity: sha512-SRpNAPW4kewOaNUt8VPqhJ0UMxawMwzJD8V7m1cJfdSTK9ieZwS6K7Dabsm4bmLFM96Z5Y/UznrpG5kt1im8yA==} + engines: {node: '>= 12'} + + resolve-pathname@3.0.0: + resolution: {integrity: sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + resolve-url-loader@5.0.0: + resolution: {integrity: sha512-uZtduh8/8srhBoMx//5bwqjQ+rfYOUq8zC9NrMUGtjBiGTtFJM42s58/36+hTqeqINcnYe08Nj3LkK9lW4N8Xg==} + engines: {node: '>=12'} + + resolve@1.22.10: + resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} + engines: {node: '>= 0.4'} + hasBin: true + + resolve@1.22.2: + resolution: {integrity: sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==} + hasBin: true + + resolve@2.0.0-next.5: + resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==} + hasBin: true + + responselike@3.0.0: + resolution: {integrity: sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==} + engines: {node: '>=14.16'} + + restore-cursor@3.1.0: + resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} + engines: {node: '>=8'} + + restore-cursor@5.1.0: + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + engines: {node: '>=18'} + + retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + + retry@0.13.1: + resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} + engines: {node: '>= 4'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + rimraf@2.7.1: + resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + robust-predicates@3.0.2: + resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==} + + rollup@3.29.5: + resolution: {integrity: sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==} + engines: {node: '>=14.18.0', npm: '>=8.0.0'} + hasBin: true + + rollup@4.45.1: + resolution: {integrity: sha512-4iya7Jb76fVpQyLoiVpzUrsjQ12r3dM7fIVz+4NwoYvZOShknRmiv+iu9CClZml5ZLGb0XMcYLutK6w9tgxHDw==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + roughjs@4.6.6: + resolution: {integrity: sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==} + + rrweb-cssom@0.8.0: + resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} + + rtlcss@4.3.0: + resolution: {integrity: sha512-FI+pHEn7Wc4NqKXMXFM+VAYKEj/mRIcW4h24YVwVtyjI+EqGrLc2Hx/Ny0lrZ21cBWU2goLy36eqMcNj3AQJig==} + engines: {node: '>=12.0.0'} + hasBin: true + + run-async@2.4.1: + resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} + engines: {node: '>=0.12.0'} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + rw@1.3.3: + resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} + + rxjs@7.8.1: + resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} + + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + + sade@1.8.1: + resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} + engines: {node: '>=6'} + + safe-array-concat@1.1.3: + resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} + engines: {node: '>=0.4'} + + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safe-push-apply@1.0.0: + resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} + engines: {node: '>= 0.4'} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + safevalues@0.3.4: + resolution: {integrity: sha512-LRneZZRXNgjzwG4bDQdOTSbze3fHm1EAKN/8bePxnlEZiBmkYEDggaHbuvHI9/hoqHbGfsEA7tWS9GhYHZBBsw==} + + sass-loader@13.3.2: + resolution: {integrity: sha512-CQbKl57kdEv+KDLquhC+gE3pXt74LEAzm+tzywcA0/aHZuub8wTErbjAoNI57rPUWRYRNC5WUnNl8eGJNbDdwg==} + engines: {node: '>= 14.15.0'} + 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 + + sass-loader@16.0.5: + resolution: {integrity: sha512-oL+CMBXrj6BZ/zOq4os+UECPL+bWqt6OAC6DWS8Ln8GZRcMDjlJ4JC3FBDuHJdYaFWIdKNIBYmtZtK2MaMkNIw==} + engines: {node: '>= 18.12.0'} + peerDependencies: + '@rspack/core': 0.x || 1.x + 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: + '@rspack/core': + optional: true + node-sass: + optional: true + sass: + optional: true + sass-embedded: + optional: true + webpack: + optional: true + + sass@1.64.1: + resolution: {integrity: sha512-16rRACSOFEE8VN7SCgBu1MpYCyN7urj9At898tyzdXFhC+a+yOX5dXwAR7L8/IdPJ1NB8OYoXmD55DM30B2kEQ==} + engines: {node: '>=14.0.0'} + hasBin: true + + sass@1.89.2: + resolution: {integrity: sha512-xCmtksBKd/jdJ9Bt9p7nPKiuqrlBMBuuGkQlkhZjjQk3Ty48lv93k5Dq6OPkKt4XwxDJ7tvlfrTa1MPA9bf+QA==} + engines: {node: '>=14.0.0'} + hasBin: true + + sax@1.4.1: + resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==} + + saxes@5.0.1: + resolution: {integrity: sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==} + engines: {node: '>=10'} + + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + + scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + + scheduler@0.26.0: + resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} + + schema-dts@1.1.5: + resolution: {integrity: sha512-RJr9EaCmsLzBX2NDiO5Z3ux2BVosNZN5jo0gWgsyKvxKIUL5R3swNvoorulAeL9kLB0iTSX7V6aokhla2m7xbg==} + + schema-utils@3.3.0: + resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==} + engines: {node: '>= 10.13.0'} + + schema-utils@4.3.2: + resolution: {integrity: sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==} + engines: {node: '>= 10.13.0'} + + search-insights@2.17.3: + resolution: {integrity: sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==} + + section-matter@1.0.0: + resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} + engines: {node: '>=4'} + + select-hose@2.0.0: + resolution: {integrity: sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==} + + selfsigned@2.4.1: + resolution: {integrity: sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==} + engines: {node: '>=10'} + + semver-diff@4.0.0: + resolution: {integrity: sha512-0Ju4+6A8iOnpL/Thra7dZsSlOHYAHIeMxfhWQRI1/VLcT3WDBZKKtQt/QkBOsiIN9ZpuvHE6cGZ0x4glCMmfiA==} + engines: {node: '>=12'} + + semver@5.7.2: + resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} + hasBin: true + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.5.3: + resolution: {integrity: sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==} + engines: {node: '>=10'} + hasBin: true + + semver@7.5.4: + resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} + engines: {node: '>=10'} + hasBin: true + + semver@7.6.3: + resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} + engines: {node: '>=10'} + hasBin: true + + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} + hasBin: true + + send@0.19.0: + resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} + engines: {node: '>= 0.8.0'} + + serialize-javascript@6.0.2: + resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} + + serve-handler@6.1.6: + resolution: {integrity: sha512-x5RL9Y2p5+Sh3D38Fh9i/iQ5ZK+e4xuXRd/pGbM4D13tgo/MGwbttUk8emytcr1YYzBYs+apnUngBDFYfpjPuQ==} + + serve-index@1.9.1: + resolution: {integrity: sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==} + engines: {node: '>= 0.8.0'} + + serve-static@1.16.2: + resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} + engines: {node: '>= 0.8.0'} + + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + set-proto@1.0.0: + resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} + engines: {node: '>= 0.4'} + + setprototypeof@1.1.0: + resolution: {integrity: sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + shallow-clone@3.0.1: + resolution: {integrity: sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==} + engines: {node: '>=8'} + + shallowequal@1.1.0: + resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==} + + sharp@0.34.3: + resolution: {integrity: sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + shell-quote@1.8.3: + resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} + engines: {node: '>= 0.4'} + + shelljs@0.8.5: + resolution: {integrity: sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==} + engines: {node: '>=4'} + hasBin: true + + should-equal@2.0.0: + resolution: {integrity: sha512-ZP36TMrK9euEuWQYBig9W55WPC7uo37qzAEmbjHz4gfyuXrEUgF8cUvQVO+w+d3OMfPvSRQJ22lSm8MQJ43LTA==} + + should-format@3.0.3: + resolution: {integrity: sha512-hZ58adtulAk0gKtua7QxevgUaXTTXxIi8t41L3zo9AHvjXO1/7sdLECuHeIN2SRtYXpNkmhoUP2pdeWgricQ+Q==} + + should-type-adaptors@1.1.0: + resolution: {integrity: sha512-JA4hdoLnN+kebEp2Vs8eBe9g7uy0zbRo+RMcU0EsNy+R+k049Ki+N5tT5Jagst2g7EAja+euFuoXFCa8vIklfA==} + + should-type@1.4.0: + resolution: {integrity: sha512-MdAsTu3n25yDbIe1NeN69G4n6mUnJGtSJHygX3+oN0ZbO3DTiATnf7XnYJdGT42JCXurTb1JI0qOBR65shvhPQ==} + + should-util@1.0.1: + resolution: {integrity: sha512-oXF8tfxx5cDk8r2kYqlkUJzZpDBqVY/II2WhvU0n9Y3XYvAYRmeaf1PvvIvTgPnv4KJ+ES5M0PyDq5Jp+Ygy2g==} + + should@13.2.3: + resolution: {integrity: sha512-ggLesLtu2xp+ZxI+ysJTmNjh2U0TsC+rQ/pfED9bUZZ4DKefP27D+7YJVVTvKsmjLpIi9jAa7itwDGkDDmt1GQ==} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + sigstore@1.9.0: + resolution: {integrity: sha512-0Zjz0oe37d08VeOtBIuB6cRriqXse2e8w+7yIy2XSXjshRKxbc2KkhXjL229jXSxEm7UbcjS76wcJDGQddVI9A==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + hasBin: true + + simple-swizzle@0.2.2: + resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + + simple-update-notifier@2.0.0: + resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} + engines: {node: '>=10'} + + sirv@2.0.4: + resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==} + engines: {node: '>= 10'} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + sitemap@7.1.2: + resolution: {integrity: sha512-ARCqzHJ0p4gWt+j7NlU5eDlIO9+Rkr/JhPFZKKQ1l5GCus7rJH4UdrlVAh0xC/gDS/Qir2UMxqYNHtsKr2rpCw==} + engines: {node: '>=12.0.0', npm: '>=5.6.0'} + hasBin: true + + skin-tone@2.0.0: + resolution: {integrity: sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==} + engines: {node: '>=8'} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + slash@4.0.0: + resolution: {integrity: sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==} + engines: {node: '>=12'} + + slice-ansi@3.0.0: + resolution: {integrity: sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==} + engines: {node: '>=8'} + + slice-ansi@4.0.0: + resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} + engines: {node: '>=10'} + + slice-ansi@5.0.0: + resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==} + engines: {node: '>=12'} + + slice-ansi@7.1.0: + resolution: {integrity: sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==} + engines: {node: '>=18'} + + slugify@1.6.6: + resolution: {integrity: sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==} + engines: {node: '>=8.0.0'} + + smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + + smol-toml@1.4.1: + resolution: {integrity: sha512-CxdwHXyYTONGHThDbq5XdwbFsuY4wlClRGejfE2NtwUtiHYsP1QtNsHb/hnj31jKYSchztJsaA8pSQoVzkfCFg==} + engines: {node: '>= 18'} + + snake-case@3.0.4: + resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} + + socket.io-adapter@2.5.5: + resolution: {integrity: sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==} + + socket.io-parser@4.2.4: + resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==} + engines: {node: '>=10.0.0'} + + socket.io@4.8.1: + resolution: {integrity: sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==} + engines: {node: '>=10.2.0'} + + sockjs@0.3.24: + resolution: {integrity: sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==} + + socks-proxy-agent@7.0.0: + resolution: {integrity: sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==} + engines: {node: '>= 10'} + + socks-proxy-agent@8.0.5: + resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==} + engines: {node: '>= 14'} + + socks@2.8.6: + resolution: {integrity: sha512-pe4Y2yzru68lXCb38aAqRf5gvN8YdjP1lok5o0J7BOHljkyCGKVz7H3vpVIXKD27rj2giOJ7DwVyk/GWrPHDWA==} + engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + + sort-css-media-queries@2.2.0: + resolution: {integrity: sha512-0xtkGhWCC9MGt/EzgnvbbbKhqWjl1+/rncmhTh5qCpbYguXh6S/qwePfv/JQ8jePXXmqingylxoC49pCkSPIbA==} + engines: {node: '>= 6.3.0'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map-loader@4.0.1: + resolution: {integrity: sha512-oqXpzDIByKONVY8g1NUPOTQhe0UTU5bWUl32GSkqK2LjJj0HmwTMVKxcUip0RgAYhY1mqgOxjbQM48a0mmeNfA==} + engines: {node: '>= 14.15.0'} + peerDependencies: + webpack: ^5.72.1 + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + source-map@0.7.4: + resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} + engines: {node: '>= 8'} + + source-map@0.8.0-beta.0: + resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} + engines: {node: '>= 8'} + + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + + spawndamnit@3.0.1: + resolution: {integrity: sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==} + + spdx-correct@3.2.0: + resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} + + spdx-exceptions@2.5.0: + resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==} + + spdx-expression-parse@3.0.1: + resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} + + spdx-license-ids@3.0.21: + resolution: {integrity: sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==} + + spdy-transport@3.0.0: + resolution: {integrity: sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==} + + spdy@4.0.2: + resolution: {integrity: sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==} + engines: {node: '>=6.0.0'} + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + split@0.3.3: + resolution: {integrity: sha512-wD2AeVmxXRBoX44wAycgjVpMhvbwdI2aZjCkvfNcH1YqHQvJVa1duWc73OyVGJUc05fhFaTZeQ/PYsrmyH0JVA==} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + sprintf-js@1.1.3: + resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} + + srcset@4.0.0: + resolution: {integrity: sha512-wvLeHgcVHKO8Sc/H/5lkGreJQVeYMm9rlmt8PuR1xE31rIuXhuzznUUqAt8MqLhB3MqJdFzlNAfpcWnxiFUcPw==} + engines: {node: '>=12'} + + sshpk@1.18.0: + resolution: {integrity: sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==} + engines: {node: '>=0.10.0'} + hasBin: true + + ssri@10.0.6: + resolution: {integrity: sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + ssri@9.0.1: + resolution: {integrity: sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + stable-hash@0.0.5: + resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + start-server-and-test@2.0.12: + resolution: {integrity: sha512-U6QiS5qsz+DN5RfJJrkAXdooxMDnLZ+n5nR8kaX//ZH19SilF6b58Z3zM9zTfrNIkJepzauHo4RceSgvgUSX9w==} + engines: {node: '>=16'} + hasBin: true + + statuses@1.5.0: + resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} + engines: {node: '>= 0.6'} + + statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + + std-env@3.9.0: + resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} + + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + + stream-combiner@0.0.4: + resolution: {integrity: sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==} + + streamroller@3.1.5: + resolution: {integrity: sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==} + engines: {node: '>=8.0'} + + streamx@2.22.1: + resolution: {integrity: sha512-znKXEBxfatz2GBNK02kRnCXjV+AA4kjZIUxeWSr3UGirZMJfTE9uiwKHobnbgxWyL/JWro8tTq+vOqAK1/qbSA==} + + string-argv@0.3.2: + resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} + engines: {node: '>=0.6.19'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + string.prototype.codepointat@0.2.1: + resolution: {integrity: sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==} + + string.prototype.includes@2.0.1: + resolution: {integrity: sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==} + engines: {node: '>= 0.4'} + + string.prototype.matchall@4.0.12: + resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==} + engines: {node: '>= 0.4'} + + string.prototype.repeat@1.0.0: + resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==} + + string.prototype.trim@1.2.10: + resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.9: + resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} + engines: {node: '>= 0.4'} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + + stringify-object@3.3.0: + resolution: {integrity: sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==} + engines: {node: '>=4'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + + strip-bom-string@1.0.0: + resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==} + engines: {node: '>=0.10.0'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + + strip-final-newline@3.0.0: + resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} + engines: {node: '>=12'} + + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + strip-json-comments@5.0.2: + resolution: {integrity: sha512-4X2FR3UwhNUE9G49aIsJW5hRRR3GXGTBTZRMfv568O60ojM8HcWjV/VxAxCDW3SUND33O6ZY66ZuRcdkj73q2g==} + engines: {node: '>=14.16'} + + strong-log-transformer@2.1.0: + resolution: {integrity: sha512-B3Hgul+z0L9a236FAUC9iZsL+nVHgoCJnqCbN588DjYxvGXaXaaFbfmQ/JhvKjZwsOukuR72XbHv71Qkug0HxA==} + engines: {node: '>=4'} + hasBin: true + + style-to-js@1.1.17: + resolution: {integrity: sha512-xQcBGDxJb6jjFCTzvQtfiPn6YvvP2O8U1MDIPNfJQlWMYfktPy+iGsHE7cssjs7y84d9fQaK4UF3RIJaAHSoYA==} + + style-to-object@0.4.4: + resolution: {integrity: sha512-HYNoHZa2GorYNyqiCaBgsxvcJIn7OHq6inEga+E6Ke3m5JkoqpQbnFssk4jwe+K7AhGa2fcha4wSOf1Kn01dMg==} + + style-to-object@1.0.9: + resolution: {integrity: sha512-G4qppLgKu/k6FwRpHiGiKPaPTFcG3g4wNVX/Qsfu+RqQM30E7Tyu/TEgxcL9PNLF5pdRLwQdE3YKKf+KF2Dzlw==} + + styled-jsx@5.1.6: + resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: + optional: true + + stylehacks@6.1.1: + resolution: {integrity: sha512-gSTTEQ670cJNoaeIp9KX6lZmm8LJ3jPB5yJmX8Zq/wQxOsAFXV3qjWzHas3YYk1qesuVIyYWWUpZ0vSE/dTSGg==} + engines: {node: ^14 || ^16 || >=18.0} + peerDependencies: + postcss: ^8.4.31 + + stylis@4.3.6: + resolution: {integrity: sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==} + + sucrase@3.35.0: + resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + svg-parser@2.0.4: + resolution: {integrity: sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==} + + svgo@3.3.2: + resolution: {integrity: sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==} + engines: {node: '>=14.0.0'} + hasBin: true + + swagger2openapi@7.0.8: + resolution: {integrity: sha512-upi/0ZGkYgEcLeGieoz8gT74oWHA0E7JivX7aN9mAf+Tc7BQoRBvnIGHoPDw+f9TXTW4s6kGYCZJtauP6OYp7g==} + hasBin: true + + swc-loader@0.2.6: + resolution: {integrity: sha512-9Zi9UP2YmDpgmQVbyOPJClY0dwf58JDyDMQ7uRc4krmc72twNI2fvlBWHLqVekBpPc7h5NJkGVT1zNDxFrqhvg==} + peerDependencies: + '@swc/core': ^1.2.147 + webpack: '>=2' + + symbol-observable@4.0.0: + resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==} + engines: {node: '>=0.10'} + + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + + tabbable@6.2.0: + resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==} + + tailwind-merge@2.6.0: + resolution: {integrity: sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==} + + tailwindcss@3.4.14: + resolution: {integrity: sha512-IcSvOcTRcUtQQ7ILQL5quRDg7Xs93PdJEk1ZLbhhvJc7uj/OAhYOnruEiwnGgBvUtaUAJ8/mhSw1o8L2jCiENA==} + engines: {node: '>=14.0.0'} + hasBin: true + + tapable@2.2.2: + resolution: {integrity: sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==} + engines: {node: '>=6'} + + tar-fs@3.1.0: + resolution: {integrity: sha512-5Mty5y/sOF1YWj1J6GiBodjlDc05CUR8PKXrsnFAiSG0xA+GHeWLovaZPYUDXkH/1iKRf2+M5+OrRgzC7O9b7w==} + + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + + tar-stream@3.1.7: + resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==} + + tar@6.2.1: + resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} + engines: {node: '>=10'} + + tcomb-validation@3.4.1: + resolution: {integrity: sha512-urVVMQOma4RXwiVCa2nM2eqrAomHROHvWPuj6UkDGz/eb5kcy0x6P0dVt6kzpUZtYMNoAqJLWmz1BPtxrtjtrA==} + + tcomb@3.2.29: + resolution: {integrity: sha512-di2Hd1DB2Zfw6StGv861JoAF5h/uQVu/QJp2g8KVbtfKnoHdBQl5M32YWq6mnSYBQ1vFFrns5B1haWJL7rKaOQ==} + + term-size@2.2.1: + resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} + engines: {node: '>=8'} + + terser-webpack-plugin@5.3.14: + resolution: {integrity: sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==} + engines: {node: '>= 10.13.0'} + peerDependencies: + '@swc/core': '*' + esbuild: '*' + uglify-js: '*' + webpack: ^5.1.0 + peerDependenciesMeta: + '@swc/core': + optional: true + esbuild: + optional: true + uglify-js: + optional: true + + terser@5.19.2: + resolution: {integrity: sha512-qC5+dmecKJA4cpYxRa5aVkKehYsQKc+AHeKl0Oe62aYjBL8ZA33tTljktDHJSaxxMnbI5ZYw+o/S2DxxLu8OfA==} + engines: {node: '>=10'} + hasBin: true + + terser@5.43.1: + resolution: {integrity: sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==} + engines: {node: '>=10'} + hasBin: true + + test-exclude@6.0.0: + resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} + engines: {node: '>=8'} + + text-decoder@1.2.3: + resolution: {integrity: sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==} + + text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + thirty-two@1.0.2: + resolution: {integrity: sha512-OEI0IWCe+Dw46019YLl6V10Us5bi574EvlJEOcAkB29IzQ/mYD1A6RyNHLjZPiHCmuodxvgF6U+vZO1L15lxVA==} + engines: {node: '>=0.2.6'} + + throttleit@1.0.1: + resolution: {integrity: sha512-vDZpf9Chs9mAdfY046mcPt8fg5QSZr37hEH4TXYBnDF+izxgrbRGUAAaBvIk/fJm9aOFCGFd1EsNg5AZCbnQCQ==} + + through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + + thunky@1.1.0: + resolution: {integrity: sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==} + + tiny-inflate@1.0.3: + resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} + + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + + tiny-warning@1.0.3: + resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinycolor2@1.4.2: + resolution: {integrity: sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA==} + + tinycolor2@1.6.0: + resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyexec@1.0.1: + resolution: {integrity: sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==} + + tinyglobby@0.2.14: + resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} + engines: {node: '>=12.0.0'} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@1.2.0: + resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} + engines: {node: '>=14.0.0'} + + tinyspy@3.0.2: + resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} + engines: {node: '>=14.0.0'} + + tldts-core@6.1.86: + resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} + + tldts@6.1.86: + resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} + hasBin: true + + tmp@0.0.33: + resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} + engines: {node: '>=0.6.0'} + + tmp@0.2.1: + resolution: {integrity: sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==} + engines: {node: '>=8.17.0'} + + tmp@0.2.3: + resolution: {integrity: sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==} + engines: {node: '>=14.14'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + toggle-selection@1.0.6: + resolution: {integrity: sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + + touch@3.1.1: + resolution: {integrity: sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==} + hasBin: true + + tough-cookie@4.1.4: + resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} + engines: {node: '>=6'} + + tough-cookie@5.1.2: + resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} + engines: {node: '>=16'} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + tr46@1.0.1: + resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} + + tr46@2.1.0: + resolution: {integrity: sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==} + engines: {node: '>=8'} + + tr46@5.1.1: + resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} + engines: {node: '>=18'} + + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + trim-trailing-lines@2.1.0: + resolution: {integrity: sha512-5UR5Biq4VlVOtzqkm2AZlgvSlDJtME46uV0br0gENbwN4l5+mMKT4b9gJKqWtuL2zAIqajGJGuvbCbcAJUZqBg==} + + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + + ts-api-utils@1.4.3: + resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==} + engines: {node: '>=16'} + peerDependencies: + typescript: '>=4.2.0' + + ts-api-utils@2.1.0: + resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + ts-dedent@2.2.0: + resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} + engines: {node: '>=6.10'} + + ts-error@1.0.6: + resolution: {integrity: sha512-tLJxacIQUM82IR7JO1UUkKlYuUTmoY9HBJAmNWFzheSlDS5SPMcNIepejHJa4BpPQLAcbRhRf3GDJzyj6rbKvA==} + + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + + ts-poet@6.12.0: + resolution: {integrity: sha512-xo+iRNMWqyvXpFTaOAvLPA5QAWO6TZrSUs5s4Odaya3epqofBu/fMLHEWl8jPmjhA0s9sgj9sNvF1BmaQlmQkA==} + + ts-proto-descriptors@2.0.0: + resolution: {integrity: sha512-wHcTH3xIv11jxgkX5OyCSFfw27agpInAd6yh89hKG6zqIXnjW9SYqSER2CVQxdPj4czeOhGagNvZBEbJPy7qkw==} + + ts-proto@2.7.5: + resolution: {integrity: sha512-FoRxSaNW+P3m+GiXIZjUjhaHXT67Ah4zMGKzn4yklbGRQTS+PqpUhKo5AJnwfUDUByjEUG7ch36byFUYWRH9Nw==} + hasBin: true + + tsconfck@3.1.6: + resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==} + engines: {node: ^18 || >=20} + hasBin: true + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + tsconfig-paths@3.15.0: + resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + + tsconfig-paths@4.2.0: + resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} + engines: {node: '>=6'} + + tslib@1.14.1: + resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + + tslib@2.6.1: + resolution: {integrity: sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsup@8.5.0: + resolution: {integrity: sha512-VmBp77lWNQq6PfuMqCHD3xWl22vEoWsKajkF8t+yMBawlUS8JzEI+vOVMeuNZIuMML8qXRizFKi9oD5glKQVcQ==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + '@microsoft/api-extractor': ^7.36.0 + '@swc/core': ^1 + postcss: ^8.4.12 + typescript: '>=4.5.0' + peerDependenciesMeta: + '@microsoft/api-extractor': + optional: true + '@swc/core': + optional: true + postcss: + optional: true + typescript: + optional: true + + tsutils@3.21.0: + resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} + engines: {node: '>= 6'} + 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' + + tuf-js@1.1.7: + resolution: {integrity: sha512-i3P9Kgw3ytjELUfpuKVDNBJvk4u5bXL6gskv572mcevPbSKCV3zt3djhmlEQ65yERjIbOSncy7U4cQJaB1CBCg==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + + turbo-darwin-64@2.5.5: + resolution: {integrity: sha512-RYnTz49u4F5tDD2SUwwtlynABNBAfbyT2uU/brJcyh5k6lDLyNfYKdKmqd3K2ls4AaiALWrFKVSBsiVwhdFNzQ==} + cpu: [x64] + os: [darwin] + + turbo-darwin-arm64@2.5.5: + resolution: {integrity: sha512-Tk+ZeSNdBobZiMw9aFypQt0DlLsWSFWu1ymqsAdJLuPoAH05qCfYtRxE1pJuYHcJB5pqI+/HOxtJoQ40726Btw==} + cpu: [arm64] + os: [darwin] + + turbo-linux-64@2.5.5: + resolution: {integrity: sha512-2/XvMGykD7VgsvWesZZYIIVXMlgBcQy+ZAryjugoTcvJv8TZzSU/B1nShcA7IAjZ0q7OsZ45uP2cOb8EgKT30w==} + cpu: [x64] + os: [linux] + + turbo-linux-arm64@2.5.5: + resolution: {integrity: sha512-DW+8CjCjybu0d7TFm9dovTTVg1VRnlkZ1rceO4zqsaLrit3DgHnN4to4uwyuf9s2V/BwS3IYcRy+HG9BL596Iw==} + cpu: [arm64] + os: [linux] + + turbo-windows-64@2.5.5: + resolution: {integrity: sha512-q5p1BOy8ChtSZfULuF1BhFMYIx6bevXu4fJ+TE/hyNfyHJIfjl90Z6jWdqAlyaFLmn99X/uw+7d6T/Y/dr5JwQ==} + cpu: [x64] + os: [win32] + + turbo-windows-arm64@2.5.5: + resolution: {integrity: sha512-AXbF1KmpHUq3PKQwddMGoKMYhHsy5t1YBQO8HZ04HLMR0rWv9adYlQ8kaeQJTko1Ay1anOBFTqaxfVOOsu7+1Q==} + cpu: [arm64] + os: [win32] + + turbo@2.5.5: + resolution: {integrity: sha512-eZ7wI6KjtT1eBqCnh2JPXWNUAxtoxxfi6VdBdZFvil0ychCOTxbm7YLRBi1JSt7U3c+u3CLxpoPxLdvr/Npr3A==} + hasBin: true + + tweetnacl@0.14.5: + resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + + type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + + type-fest@0.8.1: + resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} + engines: {node: '>=8'} + + type-fest@1.4.0: + resolution: {integrity: sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==} + engines: {node: '>=10'} + + type-fest@2.19.0: + resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} + engines: {node: '>=12.20'} + + type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.3: + resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.4: + resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.7: + resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} + engines: {node: '>= 0.4'} + + typed-assert@1.0.9: + resolution: {integrity: sha512-KNNZtayBCtmnNmbo5mG47p1XsCyrx6iVqomjcZnec/1Y5GGARaxPs6r49RnSPeUP3YjNYiU9sQHAtY4BBvnZwg==} + + typedarray-to-buffer@3.1.5: + resolution: {integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==} + + typescript@5.1.6: + resolution: {integrity: sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==} + engines: {node: '>=14.17'} + hasBin: true + + typescript@5.8.3: + resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} + engines: {node: '>=14.17'} + hasBin: true + + ua-parser-js@0.7.40: + resolution: {integrity: sha512-us1E3K+3jJppDBa3Tl0L3MOJiGhe1C6P0+nIvQAFYbxlMAx0h81eOwLmU57xgqToduDDPx3y5QsdjPfDu+FgOQ==} + hasBin: true + + ufo@1.6.1: + resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} + + unbox-primitive@1.1.0: + resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} + engines: {node: '>= 0.4'} + + unbzip2-stream@1.4.3: + resolution: {integrity: sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==} + + undefsafe@2.0.5: + resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==} + + undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + undici-types@7.8.0: + resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==} + + unicode-canonical-property-names-ecmascript@2.0.1: + resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==} + engines: {node: '>=4'} + + unicode-emoji-modifier-base@1.0.0: + resolution: {integrity: sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==} + engines: {node: '>=4'} + + unicode-match-property-ecmascript@2.0.0: + resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==} + engines: {node: '>=4'} + + unicode-match-property-value-ecmascript@2.2.0: + resolution: {integrity: sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==} + engines: {node: '>=4'} + + unicode-property-aliases-ecmascript@2.1.0: + resolution: {integrity: sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==} + engines: {node: '>=4'} + + unified@10.1.2: + resolution: {integrity: sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==} + + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + + unique-filename@2.0.1: + resolution: {integrity: sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + unique-filename@3.0.0: + resolution: {integrity: sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + unique-slug@3.0.0: + resolution: {integrity: sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + unique-slug@4.0.0: + resolution: {integrity: sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + unique-string@3.0.0: + resolution: {integrity: sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==} + engines: {node: '>=12'} + + unist-util-find-after@5.0.0: + resolution: {integrity: sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==} + + unist-util-generated@2.0.1: + resolution: {integrity: sha512-qF72kLmPxAw0oN2fwpWIqbXAVyEqUzDHMsbtPvOudIlUzXYFIeQIuxXQCRCFh22B7cixvU0MG7m3MW8FTq/S+A==} + + unist-util-is@5.2.1: + resolution: {integrity: sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==} + + unist-util-is@6.0.0: + resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==} + + unist-util-position-from-estree@1.1.2: + resolution: {integrity: sha512-poZa0eXpS+/XpoQwGwl79UUdea4ol2ZuCYguVaJS4qzIOMDzbqz8a3erUCOmubSZkaOuGamb3tX790iwOIROww==} + + unist-util-position-from-estree@2.0.0: + resolution: {integrity: sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==} + + unist-util-position@4.0.4: + resolution: {integrity: sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-remove-position@4.0.2: + resolution: {integrity: sha512-TkBb0HABNmxzAcfLf4qsIbFbaPDvMO6wa3b3j4VcEzFVaw1LBKwnW4/sRJ/atSLSzoIg41JWEdnE7N6DIhGDGQ==} + + unist-util-stringify-position@3.0.3: + resolution: {integrity: sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@5.1.3: + resolution: {integrity: sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==} + + unist-util-visit-parents@6.0.1: + resolution: {integrity: sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==} + + unist-util-visit@4.1.2: + resolution: {integrity: sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==} + + unist-util-visit@5.0.0: + resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + + universalify@0.1.2: + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} + + universalify@0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + unrs-resolver@1.11.1: + resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} + + untildify@4.0.0: + resolution: {integrity: sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==} + engines: {node: '>=8'} + + update-browserslist-db@1.1.3: + resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + update-notifier@6.0.2: + resolution: {integrity: sha512-EDxhTEVPZZRLWYcJ4ZXjGFN0oP7qYvbXWzEgRm/Yql4dHX5wDbvh89YHP6PK1lzZJYrMtXUuZZz8XGK+U6U1og==} + engines: {node: '>=14.16'} + + uri-js-replace@1.0.1: + resolution: {integrity: sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==} + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + url-loader@4.1.1: + resolution: {integrity: sha512-3BTV812+AVHHOJQO8O5MkWgZ5aosP7GnROJwvzLS9hWDj00lZ6Z0wNak423Lp9PBZN05N+Jk/N5Si8jRAlGyWA==} + engines: {node: '>= 10.13.0'} + peerDependencies: + file-loader: '*' + webpack: ^4.0.0 || ^5.0.0 + peerDependenciesMeta: + file-loader: + optional: true + + url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + + url@0.11.4: + resolution: {integrity: sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==} + engines: {node: '>= 0.4'} + + urlpattern-polyfill@10.0.0: + resolution: {integrity: sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==} + + use-callback-ref@1.3.3: + resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-composed-ref@1.4.0: + resolution: {integrity: sha512-djviaxuOOh7wkj0paeO1Q/4wMZ8Zrnag5H6yBvzN7AKKe8beOaED9SF5/ByLqsku8NP4zQqsvM2u3ew/tJK8/w==} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + use-editable@2.3.3: + resolution: {integrity: sha512-7wVD2JbfAFJ3DK0vITvXBdpd9JAz5BcKAAolsnLBuBn6UDDwBGuCIAGvR3yA2BNKm578vAMVHFCWaOcA+BhhiA==} + peerDependencies: + react: '>= 16.8.0' + + use-intl@3.26.5: + resolution: {integrity: sha512-OdsJnC/znPvHCHLQH/duvQNXnP1w0hPfS+tkSi3mAbfjYBGh4JnyfdwkQBfIVf7t8gs9eSX/CntxUMvtKdG2MQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0 + + use-isomorphic-layout-effect@1.2.1: + resolution: {integrity: sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + use-latest@1.3.0: + resolution: {integrity: sha512-mhg3xdm9NaM8q+gLT8KryJPnRFOz1/5XPBhmDEVZK1webPzDjrPk7f/mbpeLqTgB9msytYWANxgALOCJKnLvcQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + use-sidecar@1.1.3: + resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-sync-external-store@1.5.0: + resolution: {integrity: sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + util@0.10.4: + resolution: {integrity: sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==} + + utila@0.4.0: + resolution: {integrity: sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==} + + utility-types@3.11.0: + resolution: {integrity: sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==} + engines: {node: '>= 4'} + + utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + + uuid@10.0.0: + resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} + hasBin: true + + uuid@11.1.0: + resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} + hasBin: true + + uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + + uvu@0.5.6: + resolution: {integrity: sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==} + engines: {node: '>=8'} + hasBin: true + + v8-compile-cache@2.3.0: + resolution: {integrity: sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==} + + validate-npm-package-license@3.0.4: + resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + + validate-npm-package-name@5.0.1: + resolution: {integrity: sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + validate-peer-dependencies@2.2.0: + resolution: {integrity: sha512-8X1OWlERjiUY6P6tdeU9E0EwO8RA3bahoOVG7ulOZT5MqgNDUO/BQoVjYiHPcNe+v8glsboZRIw9iToMAA2zAA==} + engines: {node: '>= 12'} + + validate.io-array@1.0.6: + resolution: {integrity: sha512-DeOy7CnPEziggrOO5CZhVKJw6S3Yi7e9e65R1Nl/RTN1vTQKnzjfvks0/8kQ40FP/dsjRAOd4hxmJ7uLa6vxkg==} + + validate.io-function@1.0.2: + resolution: {integrity: sha512-LlFybRJEriSuBnUhQyG5bwglhh50EpTL2ul23MPIuR1odjO7XaMLFV8vHGwp7AZciFxtYOeiSCT5st+XSPONiQ==} + + validate.io-integer-array@1.0.0: + resolution: {integrity: sha512-mTrMk/1ytQHtCY0oNO3dztafHYyGU88KL+jRxWuzfOmQb+4qqnWmI+gykvGp8usKZOM0H7keJHEbRaFiYA0VrA==} + + validate.io-integer@1.0.5: + resolution: {integrity: sha512-22izsYSLojN/P6bppBqhgUDjCkr5RY2jd+N2a3DCAUey8ydvrZ/OkGvFPR7qfOpwR2LC5p4Ngzxz36g5Vgr/hQ==} + + validate.io-number@1.0.3: + resolution: {integrity: sha512-kRAyotcbNaSYoDnXvb4MHg/0a1egJdLwS6oJ38TJY7aw9n93Fl/3blIXdyYvPOp55CNxywooG/3BcrwNrBpcSg==} + + validator@13.15.15: + resolution: {integrity: sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==} + engines: {node: '>= 0.10'} + + value-equal@1.0.1: + resolution: {integrity: sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + verror@1.10.0: + resolution: {integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==} + engines: {'0': node >=0.6.0} + + vfile-location@4.1.0: + resolution: {integrity: sha512-YF23YMyASIIJXpktBa4vIGLJ5Gs88UB/XePgqPmTa7cDA+JeO3yclbpheQYCHjVHBn/yePzrXuygIL+xbvRYHw==} + + vfile-location@5.0.3: + resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} + + vfile-message@3.1.4: + resolution: {integrity: sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==} + + vfile-message@4.0.2: + resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==} + + vfile@5.3.7: + resolution: {integrity: sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + + vite-node@2.1.9: + resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + + vite-tsconfig-paths@5.1.4: + resolution: {integrity: sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==} + peerDependencies: + vite: '*' + peerDependenciesMeta: + vite: + optional: true + + vite@4.5.5: + resolution: {integrity: sha512-ifW3Lb2sMdX+WU91s3R0FyQlAyLxOzCSCP37ujw0+r5POeHPwe6udWVIElKQq8gk3t7b8rkmvqC6IHBpCff4GQ==} + engines: {node: ^14.18.0 || >=16.0.0} + hasBin: true + peerDependencies: + '@types/node': '>= 14' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vite@5.4.19: + resolution: {integrity: sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + 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 + + vitest@2.1.9: + resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 2.1.9 + '@vitest/ui': 2.1.9 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + void-elements@2.0.1: + resolution: {integrity: sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung==} + engines: {node: '>=0.10.0'} + + vscode-jsonrpc@8.2.0: + resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==} + engines: {node: '>=14.0.0'} + + vscode-languageserver-protocol@3.17.5: + resolution: {integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==} + + vscode-languageserver-textdocument@1.0.12: + resolution: {integrity: sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==} + + vscode-languageserver-types@3.17.5: + resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==} + + vscode-languageserver@9.0.1: + resolution: {integrity: sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==} + hasBin: true + + vscode-uri@3.0.8: + resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==} + + w3c-hr-time@1.0.2: + resolution: {integrity: sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==} + deprecated: Use your platform's native performance.now() and performance.timeOrigin. + + w3c-xmlserializer@2.0.0: + resolution: {integrity: sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==} + engines: {node: '>=10'} + + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + + wait-on@7.2.0: + resolution: {integrity: sha512-wCQcHkRazgjG5XoAq9jbTMLpNIjoSlZslrJ2+N9MxDsGEv1HnFoVjOCexL0ESva7Y9cu350j+DWADdk54s4AFQ==} + engines: {node: '>=12.0.0'} + hasBin: true + + wait-on@8.0.3: + resolution: {integrity: sha512-nQFqAFzZDeRxsu7S3C7LbuxslHhk+gnJZHyethuGKAn2IVleIbTB9I3vJSQiSR+DifUqmdzfPMoMPJfLqMF2vw==} + engines: {node: '>=12.0.0'} + hasBin: true + + walk-up-path@4.0.0: + resolution: {integrity: sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==} + engines: {node: 20 || >=22} + + warning@4.0.3: + resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==} + + watchpack@2.4.4: + resolution: {integrity: sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==} + engines: {node: '>=10.13.0'} + + wbuf@1.7.3: + resolution: {integrity: sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==} + + wcwidth@1.0.1: + resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + + web-namespaces@2.0.1: + resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} + + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + + web-streams-polyfill@4.0.0-beta.3: + resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==} + engines: {node: '>= 14'} + + web-vitals@4.2.4: + resolution: {integrity: sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==} + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + webidl-conversions@4.0.2: + resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} + + webidl-conversions@5.0.0: + resolution: {integrity: sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==} + engines: {node: '>=8'} + + webidl-conversions@6.1.0: + resolution: {integrity: sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==} + engines: {node: '>=10.4'} + + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + + webpack-bundle-analyzer@4.10.2: + resolution: {integrity: sha512-vJptkMm9pk5si4Bv922ZbKLV8UTT4zib4FPgXMhgzUny0bfDDkLXAVQs3ly3fS4/TN9ROFtb0NFrm04UXFE/Vw==} + engines: {node: '>= 10.13.0'} + hasBin: true + + webpack-dev-middleware@5.3.4: + resolution: {integrity: sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==} + engines: {node: '>= 12.13.0'} + peerDependencies: + webpack: ^4.0.0 || ^5.0.0 + + webpack-dev-middleware@6.1.2: + resolution: {integrity: sha512-Wu+EHmX326YPYUpQLKmKbTyZZJIB8/n6R09pTmB03kJmnMsVPTo9COzHZFr01txwaCAuZvfBJE4ZCHRcKs5JaQ==} + engines: {node: '>= 14.15.0'} + peerDependencies: + webpack: ^5.0.0 + peerDependenciesMeta: + webpack: + optional: true + + webpack-dev-server@4.15.1: + resolution: {integrity: sha512-5hbAst3h3C3L8w6W4P96L5vaV0PxSmJhxZvWKYIdgxOQm8pNZ5dEOmmSLBVpP85ReeyRt6AS1QJNyo/oFFPeVA==} + engines: {node: '>= 12.13.0'} + hasBin: true + peerDependencies: + webpack: ^4.37.0 || ^5.0.0 + webpack-cli: '*' + peerDependenciesMeta: + webpack: + optional: true + webpack-cli: + optional: true + + webpack-dev-server@4.15.2: + resolution: {integrity: sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g==} + engines: {node: '>= 12.13.0'} + hasBin: true + peerDependencies: + webpack: ^4.37.0 || ^5.0.0 + webpack-cli: '*' + peerDependenciesMeta: + webpack: + optional: true + webpack-cli: + optional: true + + webpack-merge@5.10.0: + resolution: {integrity: sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==} + engines: {node: '>=10.0.0'} + + webpack-merge@5.9.0: + resolution: {integrity: sha512-6NbRQw4+Sy50vYNTw7EyOn41OZItPiXB8GNv3INSoe3PSFaHJEz3SHTrYVaRm2LilNGnFUzh0FAwqPEmU/CwDg==} + engines: {node: '>=10.0.0'} + + webpack-merge@6.0.1: + resolution: {integrity: sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==} + engines: {node: '>=18.0.0'} + + webpack-sources@3.3.3: + resolution: {integrity: sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==} + engines: {node: '>=10.13.0'} + + webpack-subresource-integrity@5.1.0: + resolution: {integrity: sha512-sacXoX+xd8r4WKsy9MvH/q/vBtEHr86cpImXwyg74pFIpERKt6FmB8cXpeuh0ZLgclOlHI4Wcll7+R5L02xk9Q==} + engines: {node: '>= 12'} + peerDependencies: + html-webpack-plugin: '>= 5.0.0-beta.1 < 6' + webpack: ^5.12.0 + peerDependenciesMeta: + html-webpack-plugin: + optional: true + + webpack@5.100.2: + resolution: {integrity: sha512-QaNKAvGCDRh3wW1dsDjeMdDXwZm2vqq3zn6Pvq4rHOEOGSaUMgOOjG2Y9ZbIGzpfkJk9ZYTHpDqgDfeBDcnLaw==} + engines: {node: '>=10.13.0'} + hasBin: true + peerDependencies: + webpack-cli: '*' + peerDependenciesMeta: + webpack-cli: + optional: true + + webpack@5.94.0: + resolution: {integrity: sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==} + engines: {node: '>=10.13.0'} + hasBin: true + peerDependencies: + webpack-cli: '*' + peerDependenciesMeta: + webpack-cli: + optional: true + + webpackbar@6.0.1: + resolution: {integrity: sha512-TnErZpmuKdwWBdMoexjio3KKX6ZtoKHRVvLIU0A47R0VVBDtx3ZyOJDktgYixhoJokZTYTt1Z37OkO9pnGJa9Q==} + engines: {node: '>=14.21.3'} + peerDependencies: + webpack: 3 || 4 || 5 + + websocket-driver@0.7.4: + resolution: {integrity: sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==} + engines: {node: '>=0.8.0'} + + websocket-extensions@0.1.4: + resolution: {integrity: sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==} + engines: {node: '>=0.8.0'} + + whatwg-encoding@1.0.5: + resolution: {integrity: sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==} + + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + + whatwg-mimetype@2.3.0: + resolution: {integrity: sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==} + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + whatwg-url@14.2.0: + resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} + engines: {node: '>=18'} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + whatwg-url@7.1.0: + resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} + + whatwg-url@8.7.0: + resolution: {integrity: sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==} + engines: {node: '>=10'} + + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-builtin-type@1.2.1: + resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-module@2.0.1: + resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + + which-typed-array@1.1.19: + resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} + engines: {node: '>= 0.4'} + + which@1.3.1: + resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} + hasBin: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + which@3.0.1: + resolution: {integrity: sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + wide-align@1.1.5: + resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} + + widest-line@4.0.1: + resolution: {integrity: sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==} + engines: {node: '>=12'} + + wildcard@2.0.1: + resolution: {integrity: sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==} + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + workerpool@9.3.3: + resolution: {integrity: sha512-slxCaKbYjEdFT/o2rH9xS1hf4uRDch1w7Uo+apxhZ+sf/1d9e0ZVkn42kPNGP2dgjIx6YFvSevj0zHvbWe2jdw==} + + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + wrap-ansi@9.0.0: + resolution: {integrity: sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==} + engines: {node: '>=18'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + write-file-atomic@3.0.3: + resolution: {integrity: sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==} + + ws@7.5.10: + resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} + engines: {node: '>=8.3.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + ws@8.17.1: + resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==} + 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 + + ws@8.18.3: + resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} + 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 + + xdg-basedir@5.1.0: + resolution: {integrity: sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==} + engines: {node: '>=12'} + + xml-formatter@2.6.1: + resolution: {integrity: sha512-dOiGwoqm8y22QdTNI7A+N03tyVfBlQ0/oehAzxIZtwnFAHGeSlrfjF73YQvzSsa/Kt6+YZasKsrdu6OIpuBggw==} + engines: {node: '>= 10'} + + xml-js@1.6.11: + resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==} + hasBin: true + + xml-name-validator@3.0.0: + resolution: {integrity: sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==} + + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xml-parser-xo@3.2.0: + resolution: {integrity: sha512-8LRU6cq+d7mVsoDaMhnkkt3CTtAs4153p49fRo+HIB3I1FD1o5CeXRjRH29sQevIfVJIcPjKSsPU/+Ujhq09Rg==} + engines: {node: '>= 10'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + + y18n@4.0.3: + resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + + yaml-ast-parser@0.0.43: + resolution: {integrity: sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==} + + yaml@1.10.2: + resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} + engines: {node: '>= 6'} + + yaml@2.8.0: + resolution: {integrity: sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==} + engines: {node: '>= 14.6'} + hasBin: true + + yargs-parser@18.1.3: + resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} + engines: {node: '>=6'} + + yargs-parser@20.2.9: + resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} + engines: {node: '>=10'} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs-unparser@2.0.0: + resolution: {integrity: sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==} + engines: {node: '>=10'} + + yargs@15.4.1: + resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} + engines: {node: '>=8'} + + yargs@16.2.0: + resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} + engines: {node: '>=10'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yauzl@2.10.0: + resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + yocto-queue@1.2.1: + resolution: {integrity: sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==} + engines: {node: '>=12.20'} + + zod-validation-error@3.5.3: + resolution: {integrity: sha512-OT5Y8lbUadqVZCsnyFaTQ4/O2mys4tj7PqhdbBCp7McPwvIEKfPtdA6QfPeFQK2/Rz5LgwmAXRJTugBNBi0btw==} + engines: {node: '>=18.0.0'} + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + + zod@3.23.8: + resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + + zone.js@0.13.3: + resolution: {integrity: sha512-MKPbmZie6fASC/ps4dkmIhaT5eonHkEt6eAy80K42tAm0G2W+AahLJjbfi6X9NPdciOE9GRFTTM8u2IiF6O3ww==} + + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + +snapshots: + + '@adobe/css-tools@4.4.3': {} + + '@algolia/autocomplete-core@1.17.9(@algolia/client-search@5.34.0)(algoliasearch@5.34.0)(search-insights@2.17.3)': + dependencies: + '@algolia/autocomplete-plugin-algolia-insights': 1.17.9(@algolia/client-search@5.34.0)(algoliasearch@5.34.0)(search-insights@2.17.3) + '@algolia/autocomplete-shared': 1.17.9(@algolia/client-search@5.34.0)(algoliasearch@5.34.0) + transitivePeerDependencies: + - '@algolia/client-search' + - algoliasearch + - search-insights + + '@algolia/autocomplete-plugin-algolia-insights@1.17.9(@algolia/client-search@5.34.0)(algoliasearch@5.34.0)(search-insights@2.17.3)': + dependencies: + '@algolia/autocomplete-shared': 1.17.9(@algolia/client-search@5.34.0)(algoliasearch@5.34.0) + search-insights: 2.17.3 + transitivePeerDependencies: + - '@algolia/client-search' + - algoliasearch + + '@algolia/autocomplete-preset-algolia@1.17.9(@algolia/client-search@5.34.0)(algoliasearch@5.34.0)': + dependencies: + '@algolia/autocomplete-shared': 1.17.9(@algolia/client-search@5.34.0)(algoliasearch@5.34.0) + '@algolia/client-search': 5.34.0 + algoliasearch: 5.34.0 + + '@algolia/autocomplete-shared@1.17.9(@algolia/client-search@5.34.0)(algoliasearch@5.34.0)': + dependencies: + '@algolia/client-search': 5.34.0 + algoliasearch: 5.34.0 + + '@algolia/client-abtesting@5.34.0': + dependencies: + '@algolia/client-common': 5.34.0 + '@algolia/requester-browser-xhr': 5.34.0 + '@algolia/requester-fetch': 5.34.0 + '@algolia/requester-node-http': 5.34.0 + + '@algolia/client-analytics@5.34.0': + dependencies: + '@algolia/client-common': 5.34.0 + '@algolia/requester-browser-xhr': 5.34.0 + '@algolia/requester-fetch': 5.34.0 + '@algolia/requester-node-http': 5.34.0 + + '@algolia/client-common@5.34.0': {} + + '@algolia/client-insights@5.34.0': + dependencies: + '@algolia/client-common': 5.34.0 + '@algolia/requester-browser-xhr': 5.34.0 + '@algolia/requester-fetch': 5.34.0 + '@algolia/requester-node-http': 5.34.0 + + '@algolia/client-personalization@5.34.0': + dependencies: + '@algolia/client-common': 5.34.0 + '@algolia/requester-browser-xhr': 5.34.0 + '@algolia/requester-fetch': 5.34.0 + '@algolia/requester-node-http': 5.34.0 + + '@algolia/client-query-suggestions@5.34.0': + dependencies: + '@algolia/client-common': 5.34.0 + '@algolia/requester-browser-xhr': 5.34.0 + '@algolia/requester-fetch': 5.34.0 + '@algolia/requester-node-http': 5.34.0 + + '@algolia/client-search@5.34.0': + dependencies: + '@algolia/client-common': 5.34.0 + '@algolia/requester-browser-xhr': 5.34.0 + '@algolia/requester-fetch': 5.34.0 + '@algolia/requester-node-http': 5.34.0 + + '@algolia/events@4.0.1': {} + + '@algolia/ingestion@1.34.0': + dependencies: + '@algolia/client-common': 5.34.0 + '@algolia/requester-browser-xhr': 5.34.0 + '@algolia/requester-fetch': 5.34.0 + '@algolia/requester-node-http': 5.34.0 + + '@algolia/monitoring@1.34.0': + dependencies: + '@algolia/client-common': 5.34.0 + '@algolia/requester-browser-xhr': 5.34.0 + '@algolia/requester-fetch': 5.34.0 + '@algolia/requester-node-http': 5.34.0 + + '@algolia/recommend@5.34.0': + dependencies: + '@algolia/client-common': 5.34.0 + '@algolia/requester-browser-xhr': 5.34.0 + '@algolia/requester-fetch': 5.34.0 + '@algolia/requester-node-http': 5.34.0 + + '@algolia/requester-browser-xhr@5.34.0': + dependencies: + '@algolia/client-common': 5.34.0 + + '@algolia/requester-fetch@5.34.0': + dependencies: + '@algolia/client-common': 5.34.0 + + '@algolia/requester-node-http@5.34.0': + dependencies: + '@algolia/client-common': 5.34.0 + + '@alloc/quick-lru@5.2.0': {} + + '@ampproject/remapping@2.2.1': + dependencies: + '@jridgewell/gen-mapping': 0.3.12 + '@jridgewell/trace-mapping': 0.3.29 + + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.12 + '@jridgewell/trace-mapping': 0.3.29 + + '@angular-devkit/architect@0.1602.16(chokidar@3.5.3)': + dependencies: + '@angular-devkit/core': 16.2.16(chokidar@3.5.3) + rxjs: 7.8.1 + transitivePeerDependencies: + - chokidar + + '@angular-devkit/build-angular@16.2.16(@angular/compiler-cli@16.2.12(@angular/compiler@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(typescript@5.1.6))(@angular/service-worker@16.2.12(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(@swc/core@1.13.1)(@types/node@22.16.5)(html-webpack-plugin@5.6.3(webpack@5.94.0(@swc/core@1.13.1)(esbuild@0.18.17)))(karma@6.4.4)(lightningcss@1.30.1)(tailwindcss@3.4.14)(typescript@5.1.6)': + dependencies: + '@ampproject/remapping': 2.2.1 + '@angular-devkit/architect': 0.1602.16(chokidar@3.5.3) + '@angular-devkit/build-webpack': 0.1602.16(chokidar@3.5.3)(webpack-dev-server@4.15.1(webpack@5.94.0(@swc/core@1.13.1)(esbuild@0.18.17)))(webpack@5.94.0(@swc/core@1.13.1)(esbuild@0.18.17)) + '@angular-devkit/core': 16.2.16(chokidar@3.5.3) + '@angular/compiler-cli': 16.2.12(@angular/compiler@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(typescript@5.1.6) + '@babel/core': 7.22.9 + '@babel/generator': 7.22.9 + '@babel/helper-annotate-as-pure': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + '@babel/plugin-proposal-async-generator-functions': 7.20.7(@babel/core@7.22.9) + '@babel/plugin-transform-async-to-generator': 7.22.5(@babel/core@7.22.9) + '@babel/plugin-transform-runtime': 7.22.9(@babel/core@7.22.9) + '@babel/preset-env': 7.22.9(@babel/core@7.22.9) + '@babel/runtime': 7.22.6 + '@babel/template': 7.22.5 + '@discoveryjs/json-ext': 0.5.7 + '@ngtools/webpack': 16.2.16(@angular/compiler-cli@16.2.12(@angular/compiler@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(typescript@5.1.6))(typescript@5.1.6)(webpack@5.94.0(@swc/core@1.13.1)(esbuild@0.18.17)) + '@vitejs/plugin-basic-ssl': 1.0.1(vite@4.5.5(@types/node@22.16.5)(less@4.1.3)(lightningcss@1.30.1)(sass@1.64.1)(terser@5.19.2)) + ansi-colors: 4.1.3 + autoprefixer: 10.4.14(postcss@8.4.31) + babel-loader: 9.1.3(@babel/core@7.22.9)(webpack@5.94.0(@swc/core@1.13.1)(esbuild@0.18.17)) + babel-plugin-istanbul: 6.1.1 + browserslist: 4.25.1 + chokidar: 3.5.3 + copy-webpack-plugin: 11.0.0(webpack@5.94.0(@swc/core@1.13.1)(esbuild@0.18.17)) + critters: 0.0.20 + css-loader: 6.8.1(webpack@5.94.0(@swc/core@1.13.1)(esbuild@0.18.17)) + esbuild-wasm: 0.18.17 + fast-glob: 3.3.1 + guess-parser: 0.4.22(typescript@5.1.6) + https-proxy-agent: 5.0.1 + inquirer: 8.2.4 + jsonc-parser: 3.2.0 + karma-source-map-support: 1.4.0 + less: 4.1.3 + less-loader: 11.1.0(less@4.1.3)(webpack@5.94.0(@swc/core@1.13.1)(esbuild@0.18.17)) + license-webpack-plugin: 4.0.2(webpack@5.94.0(@swc/core@1.13.1)(esbuild@0.18.17)) + loader-utils: 3.2.1 + magic-string: 0.30.1 + mini-css-extract-plugin: 2.7.6(webpack@5.94.0(@swc/core@1.13.1)(esbuild@0.18.17)) + mrmime: 1.0.1 + open: 8.4.2 + ora: 5.4.1 + parse5-html-rewriting-stream: 7.0.0 + picomatch: 2.3.1 + piscina: 4.0.0 + postcss: 8.4.31 + postcss-loader: 7.3.3(postcss@8.4.31)(typescript@5.1.6)(webpack@5.94.0(@swc/core@1.13.1)(esbuild@0.18.17)) + resolve-url-loader: 5.0.0 + rxjs: 7.8.1 + sass: 1.64.1 + sass-loader: 13.3.2(sass@1.64.1)(webpack@5.94.0(@swc/core@1.13.1)(esbuild@0.18.17)) + semver: 7.5.4 + source-map-loader: 4.0.1(webpack@5.94.0(@swc/core@1.13.1)(esbuild@0.18.17)) + source-map-support: 0.5.21 + terser: 5.19.2 + text-table: 0.2.0 + tree-kill: 1.2.2 + tslib: 2.6.1 + typescript: 5.1.6 + vite: 4.5.5(@types/node@22.16.5)(less@4.1.3)(lightningcss@1.30.1)(sass@1.64.1)(terser@5.19.2) + webpack: 5.94.0(@swc/core@1.13.1)(esbuild@0.18.17) + webpack-dev-middleware: 6.1.2(webpack@5.94.0(@swc/core@1.13.1)(esbuild@0.18.17)) + webpack-dev-server: 4.15.1(webpack@5.94.0(@swc/core@1.13.1)(esbuild@0.18.17)) + webpack-merge: 5.9.0 + webpack-subresource-integrity: 5.1.0(html-webpack-plugin@5.6.3(webpack@5.94.0(@swc/core@1.13.1)(esbuild@0.18.17)))(webpack@5.94.0(@swc/core@1.13.1)(esbuild@0.18.17)) + optionalDependencies: + '@angular/service-worker': 16.2.12(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)) + esbuild: 0.18.17 + karma: 6.4.4 + tailwindcss: 3.4.14 + transitivePeerDependencies: + - '@swc/core' + - '@types/node' + - bufferutil + - canvas + - debug + - fibers + - html-webpack-plugin + - lightningcss + - node-sass + - sass-embedded + - stylus + - sugarss + - supports-color + - uglify-js + - utf-8-validate + - webpack-cli + + '@angular-devkit/build-webpack@0.1602.16(chokidar@3.5.3)(webpack-dev-server@4.15.1(webpack@5.94.0(@swc/core@1.13.1)(esbuild@0.18.17)))(webpack@5.94.0(@swc/core@1.13.1)(esbuild@0.18.17))': + dependencies: + '@angular-devkit/architect': 0.1602.16(chokidar@3.5.3) + rxjs: 7.8.1 + webpack: 5.94.0(@swc/core@1.13.1)(esbuild@0.18.17) + webpack-dev-server: 4.15.1(webpack@5.94.0(@swc/core@1.13.1)(esbuild@0.18.17)) + transitivePeerDependencies: + - chokidar + + '@angular-devkit/core@16.2.16(chokidar@3.5.3)': + dependencies: + ajv: 8.12.0 + ajv-formats: 2.1.1(ajv@8.12.0) + jsonc-parser: 3.2.0 + picomatch: 2.3.1 + rxjs: 7.8.1 + source-map: 0.7.4 + optionalDependencies: + chokidar: 3.5.3 + + '@angular-devkit/schematics@16.2.16(chokidar@3.5.3)': + dependencies: + '@angular-devkit/core': 16.2.16(chokidar@3.5.3) + jsonc-parser: 3.2.0 + magic-string: 0.30.1 + ora: 5.4.1 + rxjs: 7.8.1 + transitivePeerDependencies: + - chokidar + + '@angular-eslint/builder@18.3.0(eslint@8.57.1)(typescript@5.1.6)': + dependencies: + eslint: 8.57.1 + typescript: 5.1.6 + + '@angular-eslint/bundled-angular-compiler@16.2.0': {} + + '@angular-eslint/bundled-angular-compiler@18.0.0': {} + + '@angular-eslint/bundled-angular-compiler@18.3.0': {} + + '@angular-eslint/eslint-plugin-template@16.2.0(eslint@8.57.1)(typescript@5.1.6)': + dependencies: + '@angular-eslint/bundled-angular-compiler': 16.2.0 + '@angular-eslint/utils': 16.2.0(eslint@8.57.1)(typescript@5.1.6) + '@typescript-eslint/type-utils': 5.62.0(eslint@8.57.1)(typescript@5.1.6) + '@typescript-eslint/utils': 5.62.0(eslint@8.57.1)(typescript@5.1.6) + aria-query: 5.3.0 + axobject-query: 3.2.1 + eslint: 8.57.1 + typescript: 5.1.6 + transitivePeerDependencies: + - supports-color + + '@angular-eslint/eslint-plugin-template@18.0.0(eslint@8.57.1)(typescript@5.1.6)': + dependencies: + '@angular-eslint/bundled-angular-compiler': 18.0.0 + '@angular-eslint/utils': 18.0.0(eslint@8.57.1)(typescript@5.1.6) + '@typescript-eslint/utils': 8.0.0-alpha.20(eslint@8.57.1)(typescript@5.1.6) + aria-query: 5.3.0 + axobject-query: 4.0.0 + eslint: 8.57.1 + typescript: 5.1.6 + transitivePeerDependencies: + - supports-color + + '@angular-eslint/eslint-plugin@16.2.0(eslint@8.57.1)(typescript@5.1.6)': + dependencies: + '@angular-eslint/utils': 16.2.0(eslint@8.57.1)(typescript@5.1.6) + '@typescript-eslint/utils': 5.62.0(eslint@8.57.1)(typescript@5.1.6) + eslint: 8.57.1 + typescript: 5.1.6 + transitivePeerDependencies: + - supports-color + + '@angular-eslint/eslint-plugin@18.0.0(eslint@8.57.1)(typescript@5.1.6)': + dependencies: + '@angular-eslint/bundled-angular-compiler': 18.0.0 + '@angular-eslint/utils': 18.0.0(eslint@8.57.1)(typescript@5.1.6) + '@typescript-eslint/utils': 8.0.0-alpha.20(eslint@8.57.1)(typescript@5.1.6) + eslint: 8.57.1 + typescript: 5.1.6 + transitivePeerDependencies: + - supports-color + + '@angular-eslint/schematics@16.2.0(@angular/cli@16.2.16(chokidar@3.5.3))(@swc/core@1.13.1)(eslint@8.57.1)(typescript@5.1.6)': + dependencies: + '@angular-eslint/eslint-plugin': 16.2.0(eslint@8.57.1)(typescript@5.1.6) + '@angular-eslint/eslint-plugin-template': 16.2.0(eslint@8.57.1)(typescript@5.1.6) + '@angular/cli': 16.2.16(chokidar@3.5.3) + '@nx/devkit': 16.5.1(nx@16.5.1(@swc/core@1.13.1)) + ignore: 5.2.4 + nx: 16.5.1(@swc/core@1.13.1) + strip-json-comments: 3.1.1 + tmp: 0.2.1 + transitivePeerDependencies: + - '@swc-node/register' + - '@swc/core' + - debug + - eslint + - supports-color + - typescript + + '@angular-eslint/template-parser@18.3.0(eslint@8.57.1)(typescript@5.1.6)': + dependencies: + '@angular-eslint/bundled-angular-compiler': 18.3.0 + eslint: 8.57.1 + eslint-scope: 8.4.0 + typescript: 5.1.6 + + '@angular-eslint/utils@16.2.0(eslint@8.57.1)(typescript@5.1.6)': + dependencies: + '@angular-eslint/bundled-angular-compiler': 16.2.0 + '@typescript-eslint/utils': 5.62.0(eslint@8.57.1)(typescript@5.1.6) + eslint: 8.57.1 + typescript: 5.1.6 + transitivePeerDependencies: + - supports-color + + '@angular-eslint/utils@18.0.0(eslint@8.57.1)(typescript@5.1.6)': + dependencies: + '@angular-eslint/bundled-angular-compiler': 18.0.0 + '@typescript-eslint/utils': 8.0.0-alpha.20(eslint@8.57.1)(typescript@5.1.6) + eslint: 8.57.1 + typescript: 5.1.6 + transitivePeerDependencies: + - supports-color + + '@angular/animations@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))': + dependencies: + '@angular/core': 16.2.12(rxjs@7.8.2)(zone.js@0.13.3) + tslib: 2.8.1 + + '@angular/cdk@16.2.14(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2)': + dependencies: + '@angular/common': 16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2) + '@angular/core': 16.2.12(rxjs@7.8.2)(zone.js@0.13.3) + rxjs: 7.8.2 + tslib: 2.8.1 + optionalDependencies: + parse5: 7.3.0 + + '@angular/cli@16.2.16(chokidar@3.5.3)': + dependencies: + '@angular-devkit/architect': 0.1602.16(chokidar@3.5.3) + '@angular-devkit/core': 16.2.16(chokidar@3.5.3) + '@angular-devkit/schematics': 16.2.16(chokidar@3.5.3) + '@schematics/angular': 16.2.16(chokidar@3.5.3) + '@yarnpkg/lockfile': 1.1.0 + ansi-colors: 4.1.3 + ini: 4.1.1 + inquirer: 8.2.4 + jsonc-parser: 3.2.0 + npm-package-arg: 10.1.0 + npm-pick-manifest: 8.0.1 + open: 8.4.2 + ora: 5.4.1 + pacote: 15.2.0 + resolve: 1.22.2 + semver: 7.5.4 + symbol-observable: 4.0.0 + yargs: 17.7.2 + transitivePeerDependencies: + - bluebird + - chokidar + - supports-color + + '@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2)': + dependencies: + '@angular/core': 16.2.12(rxjs@7.8.2)(zone.js@0.13.3) + rxjs: 7.8.2 + tslib: 2.8.1 + + '@angular/compiler-cli@16.2.12(@angular/compiler@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(typescript@5.1.6)': + dependencies: + '@angular/compiler': 16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)) + '@babel/core': 7.23.2 + '@jridgewell/sourcemap-codec': 1.5.4 + chokidar: 3.6.0 + convert-source-map: 1.9.0 + reflect-metadata: 0.1.14 + semver: 7.7.2 + tslib: 2.8.1 + typescript: 5.1.6 + yargs: 17.7.2 + transitivePeerDependencies: + - supports-color + + '@angular/compiler@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))': + dependencies: + tslib: 2.8.1 + optionalDependencies: + '@angular/core': 16.2.12(rxjs@7.8.2)(zone.js@0.13.3) + + '@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)': + dependencies: + rxjs: 7.8.2 + tslib: 2.8.1 + zone.js: 0.13.3 + + '@angular/forms@16.2.12(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(@angular/platform-browser@16.2.12(@angular/animations@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(rxjs@7.8.2)': + dependencies: + '@angular/common': 16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2) + '@angular/core': 16.2.12(rxjs@7.8.2)(zone.js@0.13.3) + '@angular/platform-browser': 16.2.12(@angular/animations@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)) + rxjs: 7.8.2 + tslib: 2.8.1 + + '@angular/language-service@18.2.13': {} + + ? '@angular/material-moment-adapter@16.2.14(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(@angular/material@16.2.14(@angular/animations@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(@angular/cdk@16.2.14(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(@angular/forms@16.2.12(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(@angular/platform-browser@16.2.12(@angular/animations@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(rxjs@7.8.2))(@angular/platform-browser@16.2.12(@angular/animations@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(rxjs@7.8.2))(moment@2.30.1)' + : dependencies: + '@angular/core': 16.2.12(rxjs@7.8.2)(zone.js@0.13.3) + '@angular/material': 16.2.14(@angular/animations@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(@angular/cdk@16.2.14(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(@angular/forms@16.2.12(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(@angular/platform-browser@16.2.12(@angular/animations@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(rxjs@7.8.2))(@angular/platform-browser@16.2.12(@angular/animations@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(rxjs@7.8.2) + moment: 2.30.1 + tslib: 2.8.1 + + ? '@angular/material@16.2.14(@angular/animations@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(@angular/cdk@16.2.14(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(@angular/forms@16.2.12(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(@angular/platform-browser@16.2.12(@angular/animations@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(rxjs@7.8.2))(@angular/platform-browser@16.2.12(@angular/animations@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(rxjs@7.8.2)' + : dependencies: + '@angular/animations': 16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)) + '@angular/cdk': 16.2.14(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2) + '@angular/common': 16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2) + '@angular/core': 16.2.12(rxjs@7.8.2)(zone.js@0.13.3) + '@angular/forms': 16.2.12(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(@angular/platform-browser@16.2.12(@angular/animations@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(rxjs@7.8.2) + '@angular/platform-browser': 16.2.12(@angular/animations@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)) + '@material/animation': 15.0.0-canary.bc9ae6c9c.0 + '@material/auto-init': 15.0.0-canary.bc9ae6c9c.0 + '@material/banner': 15.0.0-canary.bc9ae6c9c.0 + '@material/base': 15.0.0-canary.bc9ae6c9c.0 + '@material/button': 15.0.0-canary.bc9ae6c9c.0 + '@material/card': 15.0.0-canary.bc9ae6c9c.0 + '@material/checkbox': 15.0.0-canary.bc9ae6c9c.0 + '@material/chips': 15.0.0-canary.bc9ae6c9c.0 + '@material/circular-progress': 15.0.0-canary.bc9ae6c9c.0 + '@material/data-table': 15.0.0-canary.bc9ae6c9c.0 + '@material/density': 15.0.0-canary.bc9ae6c9c.0 + '@material/dialog': 15.0.0-canary.bc9ae6c9c.0 + '@material/dom': 15.0.0-canary.bc9ae6c9c.0 + '@material/drawer': 15.0.0-canary.bc9ae6c9c.0 + '@material/elevation': 15.0.0-canary.bc9ae6c9c.0 + '@material/fab': 15.0.0-canary.bc9ae6c9c.0 + '@material/feature-targeting': 15.0.0-canary.bc9ae6c9c.0 + '@material/floating-label': 15.0.0-canary.bc9ae6c9c.0 + '@material/form-field': 15.0.0-canary.bc9ae6c9c.0 + '@material/icon-button': 15.0.0-canary.bc9ae6c9c.0 + '@material/image-list': 15.0.0-canary.bc9ae6c9c.0 + '@material/layout-grid': 15.0.0-canary.bc9ae6c9c.0 + '@material/line-ripple': 15.0.0-canary.bc9ae6c9c.0 + '@material/linear-progress': 15.0.0-canary.bc9ae6c9c.0 + '@material/list': 15.0.0-canary.bc9ae6c9c.0 + '@material/menu': 15.0.0-canary.bc9ae6c9c.0 + '@material/menu-surface': 15.0.0-canary.bc9ae6c9c.0 + '@material/notched-outline': 15.0.0-canary.bc9ae6c9c.0 + '@material/radio': 15.0.0-canary.bc9ae6c9c.0 + '@material/ripple': 15.0.0-canary.bc9ae6c9c.0 + '@material/rtl': 15.0.0-canary.bc9ae6c9c.0 + '@material/segmented-button': 15.0.0-canary.bc9ae6c9c.0 + '@material/select': 15.0.0-canary.bc9ae6c9c.0 + '@material/shape': 15.0.0-canary.bc9ae6c9c.0 + '@material/slider': 15.0.0-canary.bc9ae6c9c.0 + '@material/snackbar': 15.0.0-canary.bc9ae6c9c.0 + '@material/switch': 15.0.0-canary.bc9ae6c9c.0 + '@material/tab': 15.0.0-canary.bc9ae6c9c.0 + '@material/tab-bar': 15.0.0-canary.bc9ae6c9c.0 + '@material/tab-indicator': 15.0.0-canary.bc9ae6c9c.0 + '@material/tab-scroller': 15.0.0-canary.bc9ae6c9c.0 + '@material/textfield': 15.0.0-canary.bc9ae6c9c.0 + '@material/theme': 15.0.0-canary.bc9ae6c9c.0 + '@material/tooltip': 15.0.0-canary.bc9ae6c9c.0 + '@material/top-app-bar': 15.0.0-canary.bc9ae6c9c.0 + '@material/touch-target': 15.0.0-canary.bc9ae6c9c.0 + '@material/typography': 15.0.0-canary.bc9ae6c9c.0 + rxjs: 7.8.2 + tslib: 2.8.1 + + '@angular/platform-browser-dynamic@16.2.12(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/compiler@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(@angular/platform-browser@16.2.12(@angular/animations@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))': + dependencies: + '@angular/common': 16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2) + '@angular/compiler': 16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)) + '@angular/core': 16.2.12(rxjs@7.8.2)(zone.js@0.13.3) + '@angular/platform-browser': 16.2.12(@angular/animations@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)) + tslib: 2.8.1 + + '@angular/platform-browser@16.2.12(@angular/animations@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))': + dependencies: + '@angular/common': 16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2) + '@angular/core': 16.2.12(rxjs@7.8.2)(zone.js@0.13.3) + tslib: 2.8.1 + optionalDependencies: + '@angular/animations': 16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)) + + '@angular/router@16.2.12(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(@angular/platform-browser@16.2.12(@angular/animations@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(rxjs@7.8.2)': + dependencies: + '@angular/common': 16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2) + '@angular/core': 16.2.12(rxjs@7.8.2)(zone.js@0.13.3) + '@angular/platform-browser': 16.2.12(@angular/animations@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)) + rxjs: 7.8.2 + tslib: 2.8.1 + + '@angular/service-worker@16.2.12(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))': + dependencies: + '@angular/common': 16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2) + '@angular/core': 16.2.12(rxjs@7.8.2)(zone.js@0.13.3) + tslib: 2.8.1 + + '@antfu/install-pkg@1.1.0': + dependencies: + package-manager-detector: 1.3.0 + tinyexec: 1.0.1 + + '@antfu/utils@8.1.1': {} + + '@apidevtools/json-schema-ref-parser@11.9.3': + dependencies: + '@jsdevtools/ono': 7.1.3 + '@types/json-schema': 7.0.15 + js-yaml: 4.1.0 + + '@asamuzakjp/css-color@3.2.0': + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + lru-cache: 10.4.3 + + '@assemblyscript/loader@0.10.1': {} + + '@babel/code-frame@7.27.1': + dependencies: + '@babel/helper-validator-identifier': 7.27.1 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.28.0': {} + + '@babel/core@7.22.9': + dependencies: + '@ampproject/remapping': 2.2.1 + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.22.9 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-module-transforms': 7.27.3(@babel/core@7.22.9) + '@babel/helpers': 7.27.6 + '@babel/parser': 7.28.0 + '@babel/template': 7.22.5 + '@babel/traverse': 7.28.0 + '@babel/types': 7.28.1 + convert-source-map: 1.9.0 + debug: 4.4.1(supports-color@5.5.0) + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/core@7.23.2': + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.0 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-module-transforms': 7.27.3(@babel/core@7.23.2) + '@babel/helpers': 7.27.6 + '@babel/parser': 7.28.0 + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.0 + '@babel/types': 7.28.1 + convert-source-map: 2.0.0 + debug: 4.4.1(supports-color@5.5.0) + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/core@7.28.0': + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.0 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.0) + '@babel/helpers': 7.27.6 + '@babel/parser': 7.28.0 + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.0 + '@babel/types': 7.28.1 + convert-source-map: 2.0.0 + debug: 4.4.1(supports-color@5.5.0) + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/eslint-parser@7.28.0(@babel/core@7.28.0)(eslint@8.57.1)': + dependencies: + '@babel/core': 7.28.0 + '@nicolo-ribaudo/eslint-scope-5-internals': 5.1.1-v1 + eslint: 8.57.1 + eslint-visitor-keys: 2.1.0 + semver: 6.3.1 + + '@babel/generator@7.22.9': + dependencies: + '@babel/types': 7.28.1 + '@jridgewell/gen-mapping': 0.3.12 + '@jridgewell/trace-mapping': 0.3.29 + jsesc: 2.5.2 + + '@babel/generator@7.28.0': + dependencies: + '@babel/parser': 7.28.0 + '@babel/types': 7.28.1 + '@jridgewell/gen-mapping': 0.3.12 + '@jridgewell/trace-mapping': 0.3.29 + jsesc: 3.1.0 + + '@babel/helper-annotate-as-pure@7.22.5': + dependencies: + '@babel/types': 7.28.1 + + '@babel/helper-annotate-as-pure@7.27.3': + dependencies: + '@babel/types': 7.28.1 + + '@babel/helper-compilation-targets@7.27.2': + dependencies: + '@babel/compat-data': 7.28.0 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.25.1 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-create-class-features-plugin@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-member-expression-to-functions': 7.27.1 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.22.9) + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/traverse': 7.28.0 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-create-class-features-plugin@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-member-expression-to-functions': 7.27.1 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.0) + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/traverse': 7.28.0 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-create-regexp-features-plugin@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-annotate-as-pure': 7.27.3 + regexpu-core: 6.2.0 + semver: 6.3.1 + + '@babel/helper-create-regexp-features-plugin@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-annotate-as-pure': 7.27.3 + regexpu-core: 6.2.0 + semver: 6.3.1 + + '@babel/helper-define-polyfill-provider@0.4.4(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-plugin-utils': 7.27.1 + debug: 4.4.1(supports-color@5.5.0) + lodash.debounce: 4.0.8 + resolve: 1.22.10 + transitivePeerDependencies: + - supports-color + + '@babel/helper-define-polyfill-provider@0.5.0(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-plugin-utils': 7.27.1 + debug: 4.4.1(supports-color@5.5.0) + lodash.debounce: 4.0.8 + resolve: 1.22.10 + transitivePeerDependencies: + - supports-color + + '@babel/helper-define-polyfill-provider@0.6.5(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-plugin-utils': 7.27.1 + debug: 4.4.1(supports-color@5.5.0) + lodash.debounce: 4.0.8 + resolve: 1.22.10 + transitivePeerDependencies: + - supports-color + + '@babel/helper-define-polyfill-provider@0.6.5(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-plugin-utils': 7.27.1 + debug: 4.4.1(supports-color@5.5.0) + lodash.debounce: 4.0.8 + resolve: 1.22.10 + transitivePeerDependencies: + - supports-color + + '@babel/helper-environment-visitor@7.24.7': + dependencies: + '@babel/types': 7.28.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-member-expression-to-functions@7.27.1': + dependencies: + '@babel/traverse': 7.28.0 + '@babel/types': 7.28.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-imports@7.27.1': + dependencies: + '@babel/traverse': 7.28.0 + '@babel/types': 7.28.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.27.3(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@babel/traverse': 7.28.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.27.3(@babel/core@7.23.2)': + dependencies: + '@babel/core': 7.23.2 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@babel/traverse': 7.28.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.27.3(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@babel/traverse': 7.28.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-optimise-call-expression@7.27.1': + dependencies: + '@babel/types': 7.28.1 + + '@babel/helper-plugin-utils@7.27.1': {} + + '@babel/helper-remap-async-to-generator@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-wrap-function': 7.27.1 + '@babel/traverse': 7.28.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-remap-async-to-generator@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-wrap-function': 7.27.1 + '@babel/traverse': 7.28.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-replace-supers@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-member-expression-to-functions': 7.27.1 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/traverse': 7.28.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-replace-supers@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-member-expression-to-functions': 7.27.1 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/traverse': 7.28.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + dependencies: + '@babel/traverse': 7.28.0 + '@babel/types': 7.28.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-split-export-declaration@7.22.6': + dependencies: + '@babel/types': 7.28.1 + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.27.1': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helper-wrap-function@7.27.1': + dependencies: + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.0 + '@babel/types': 7.28.1 + transitivePeerDependencies: + - supports-color + + '@babel/helpers@7.27.6': + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.28.1 + + '@babel/parser@7.28.0': + dependencies: + '@babel/types': 7.28.1 + + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/traverse': 7.28.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/plugin-transform-optional-chaining': 7.27.1(@babel/core@7.22.9) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/plugin-transform-optional-chaining': 7.27.1(@babel/core@7.28.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/traverse': 7.28.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-proposal-async-generator-functions@7.20.7(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-environment-visitor': 7.24.7 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.22.9) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + + '@babel/plugin-proposal-unicode-property-regex@7.18.6(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.22.9) + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-export-namespace-from@7.8.3(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-import-assertions@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-import-assertions@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-import-attributes@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-import-attributes@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.22.9) + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.0) + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-arrow-functions@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-arrow-functions@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-async-generator-functions@7.28.0(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.22.9) + '@babel/traverse': 7.28.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-async-generator-functions@7.28.0(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.28.0) + '@babel/traverse': 7.28.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-async-to-generator@7.22.5(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.22.9) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-async-to-generator@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.28.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-block-scoped-functions@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-block-scoped-functions@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-block-scoping@7.28.0(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-block-scoping@7.28.0(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-class-properties@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.22.9) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-class-properties@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.28.0) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-class-static-block@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.22.9) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-class-static-block@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.28.0) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-classes@7.28.0(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-globals': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.22.9) + '@babel/traverse': 7.28.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-classes@7.28.0(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-globals': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.0) + '@babel/traverse': 7.28.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-computed-properties@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/template': 7.27.2 + + '@babel/plugin-transform-computed-properties@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/template': 7.27.2 + + '@babel/plugin-transform-destructuring@7.28.0(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/traverse': 7.28.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-destructuring@7.28.0(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/traverse': 7.28.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-dotall-regex@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.22.9) + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-dotall-regex@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.0) + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-duplicate-keys@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-duplicate-keys@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.0) + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-dynamic-import@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-dynamic-import@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-explicit-resource-management@7.28.0(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-destructuring': 7.28.0(@babel/core@7.28.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-exponentiation-operator@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-exponentiation-operator@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-export-namespace-from@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-export-namespace-from@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-for-of@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-for-of@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-function-name@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/traverse': 7.28.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-function-name@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/traverse': 7.28.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-json-strings@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-json-strings@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-literals@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-literals@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-logical-assignment-operators@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-logical-assignment-operators@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-member-expression-literals@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-member-expression-literals@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-modules-amd@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-module-transforms': 7.27.3(@babel/core@7.22.9) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-amd@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.0) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-commonjs@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-module-transforms': 7.27.3(@babel/core@7.22.9) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-commonjs@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.0) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-systemjs@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-module-transforms': 7.27.3(@babel/core@7.22.9) + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@babel/traverse': 7.28.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-systemjs@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.0) + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@babel/traverse': 7.28.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-umd@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-module-transforms': 7.27.3(@babel/core@7.22.9) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-umd@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.0) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-named-capturing-groups-regex@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.22.9) + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-named-capturing-groups-regex@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.0) + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-new-target@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-new-target@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-nullish-coalescing-operator@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-nullish-coalescing-operator@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-numeric-separator@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-numeric-separator@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-object-rest-spread@7.28.0(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-destructuring': 7.28.0(@babel/core@7.22.9) + '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.22.9) + '@babel/traverse': 7.28.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-object-rest-spread@7.28.0(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-destructuring': 7.28.0(@babel/core@7.28.0) + '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.28.0) + '@babel/traverse': 7.28.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-object-super@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.22.9) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-object-super@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-optional-catch-binding@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-optional-catch-binding@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-optional-chaining@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-optional-chaining@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-parameters@7.27.7(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-parameters@7.27.7(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-private-methods@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.22.9) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-private-methods@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.28.0) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-private-property-in-object@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.22.9) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-private-property-in-object@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.28.0) + '@babel/helper-plugin-utils': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-property-literals@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-property-literals@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-react-constant-elements@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-react-display-name@7.28.0(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-react-jsx-development@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/plugin-transform-react-jsx': 7.27.1(@babel/core@7.28.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-react-jsx@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.0) + '@babel/types': 7.28.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-react-pure-annotations@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-regenerator@7.28.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-regenerator@7.28.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-regexp-modifiers@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.0) + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-reserved-words@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-reserved-words@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-runtime@7.22.9(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + babel-plugin-polyfill-corejs2: 0.4.14(@babel/core@7.22.9) + babel-plugin-polyfill-corejs3: 0.8.7(@babel/core@7.22.9) + babel-plugin-polyfill-regenerator: 0.5.5(@babel/core@7.22.9) + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-runtime@7.28.0(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + babel-plugin-polyfill-corejs2: 0.4.14(@babel/core@7.28.0) + babel-plugin-polyfill-corejs3: 0.13.0(@babel/core@7.28.0) + babel-plugin-polyfill-regenerator: 0.6.5(@babel/core@7.28.0) + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-shorthand-properties@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-shorthand-properties@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-spread@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-spread@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-sticky-regex@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-sticky-regex@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-template-literals@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-template-literals@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-typeof-symbol@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-typeof-symbol@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-typescript@7.28.0(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-create-class-features-plugin': 7.27.1(@babel/core@7.28.0) + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-unicode-escapes@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-unicode-escapes@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-unicode-property-regex@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.22.9) + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-unicode-property-regex@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.0) + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-unicode-regex@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.22.9) + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-unicode-regex@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.0) + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-unicode-sets-regex@7.27.1(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.22.9) + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-unicode-sets-regex@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.0) + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/preset-env@7.22.9(@babel/core@7.22.9)': + dependencies: + '@babel/compat-data': 7.28.0 + '@babel/core': 7.22.9 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-validator-option': 7.27.1 + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.22.9) + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.22.9) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.22.9) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.22.9) + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.22.9) + '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.22.9) + '@babel/plugin-syntax-import-assertions': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-syntax-import-attributes': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.22.9) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.22.9) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.22.9) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.22.9) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.22.9) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.22.9) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.22.9) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.22.9) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.22.9) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.22.9) + '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.22.9) + '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-async-generator-functions': 7.28.0(@babel/core@7.22.9) + '@babel/plugin-transform-async-to-generator': 7.22.5(@babel/core@7.22.9) + '@babel/plugin-transform-block-scoped-functions': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-block-scoping': 7.28.0(@babel/core@7.22.9) + '@babel/plugin-transform-class-properties': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-class-static-block': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-classes': 7.28.0(@babel/core@7.22.9) + '@babel/plugin-transform-computed-properties': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-destructuring': 7.28.0(@babel/core@7.22.9) + '@babel/plugin-transform-dotall-regex': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-duplicate-keys': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-dynamic-import': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-exponentiation-operator': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-export-namespace-from': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-for-of': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-function-name': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-json-strings': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-literals': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-logical-assignment-operators': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-member-expression-literals': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-modules-amd': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-modules-systemjs': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-modules-umd': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-named-capturing-groups-regex': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-new-target': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-nullish-coalescing-operator': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-numeric-separator': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-object-rest-spread': 7.28.0(@babel/core@7.22.9) + '@babel/plugin-transform-object-super': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-optional-catch-binding': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-optional-chaining': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.22.9) + '@babel/plugin-transform-private-methods': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-private-property-in-object': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-property-literals': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-regenerator': 7.28.1(@babel/core@7.22.9) + '@babel/plugin-transform-reserved-words': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-spread': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-sticky-regex': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-template-literals': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-typeof-symbol': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-unicode-escapes': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-unicode-property-regex': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.22.9) + '@babel/plugin-transform-unicode-sets-regex': 7.27.1(@babel/core@7.22.9) + '@babel/preset-modules': 0.1.6(@babel/core@7.22.9) + '@babel/types': 7.28.1 + babel-plugin-polyfill-corejs2: 0.4.14(@babel/core@7.22.9) + babel-plugin-polyfill-corejs3: 0.8.7(@babel/core@7.22.9) + babel-plugin-polyfill-regenerator: 0.5.5(@babel/core@7.22.9) + core-js-compat: 3.44.0 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/preset-env@7.28.0(@babel/core@7.28.0)': + dependencies: + '@babel/compat-data': 7.28.0 + '@babel/core': 7.28.0 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-validator-option': 7.27.1 + '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.28.0) + '@babel/plugin-syntax-import-assertions': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-syntax-import-attributes': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.28.0) + '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-async-generator-functions': 7.28.0(@babel/core@7.28.0) + '@babel/plugin-transform-async-to-generator': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-block-scoped-functions': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-block-scoping': 7.28.0(@babel/core@7.28.0) + '@babel/plugin-transform-class-properties': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-class-static-block': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-classes': 7.28.0(@babel/core@7.28.0) + '@babel/plugin-transform-computed-properties': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-destructuring': 7.28.0(@babel/core@7.28.0) + '@babel/plugin-transform-dotall-regex': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-duplicate-keys': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-dynamic-import': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-explicit-resource-management': 7.28.0(@babel/core@7.28.0) + '@babel/plugin-transform-exponentiation-operator': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-export-namespace-from': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-for-of': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-function-name': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-json-strings': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-literals': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-logical-assignment-operators': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-member-expression-literals': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-modules-amd': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-modules-systemjs': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-modules-umd': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-named-capturing-groups-regex': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-new-target': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-nullish-coalescing-operator': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-numeric-separator': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-object-rest-spread': 7.28.0(@babel/core@7.28.0) + '@babel/plugin-transform-object-super': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-optional-catch-binding': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-optional-chaining': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.28.0) + '@babel/plugin-transform-private-methods': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-private-property-in-object': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-property-literals': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-regenerator': 7.28.1(@babel/core@7.28.0) + '@babel/plugin-transform-regexp-modifiers': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-reserved-words': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-spread': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-sticky-regex': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-template-literals': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-typeof-symbol': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-unicode-escapes': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-unicode-property-regex': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-unicode-sets-regex': 7.27.1(@babel/core@7.28.0) + '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.28.0) + babel-plugin-polyfill-corejs2: 0.4.14(@babel/core@7.28.0) + babel-plugin-polyfill-corejs3: 0.13.0(@babel/core@7.28.0) + babel-plugin-polyfill-regenerator: 0.6.5(@babel/core@7.28.0) + core-js-compat: 3.44.0 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/preset-modules@0.1.6(@babel/core@7.22.9)': + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-proposal-unicode-property-regex': 7.18.6(@babel/core@7.22.9) + '@babel/plugin-transform-dotall-regex': 7.27.1(@babel/core@7.22.9) + '@babel/types': 7.28.1 + esutils: 2.0.3 + + '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/types': 7.28.1 + esutils: 2.0.3 + + '@babel/preset-react@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-validator-option': 7.27.1 + '@babel/plugin-transform-react-display-name': 7.28.0(@babel/core@7.28.0) + '@babel/plugin-transform-react-jsx': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-react-jsx-development': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-react-pure-annotations': 7.27.1(@babel/core@7.28.0) + transitivePeerDependencies: + - supports-color + + '@babel/preset-typescript@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-validator-option': 7.27.1 + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-typescript': 7.28.0(@babel/core@7.28.0) + transitivePeerDependencies: + - supports-color + + '@babel/runtime-corejs3@7.28.0': + dependencies: + core-js-pure: 3.44.0 + + '@babel/runtime@7.22.6': + dependencies: + regenerator-runtime: 0.13.11 + + '@babel/runtime@7.27.6': {} + + '@babel/template@7.22.5': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/parser': 7.28.0 + '@babel/types': 7.28.1 + + '@babel/template@7.27.2': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/parser': 7.28.0 + '@babel/types': 7.28.1 + + '@babel/traverse@7.28.0': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.0 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.0 + '@babel/template': 7.27.2 + '@babel/types': 7.28.1 + debug: 4.4.1(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + + '@babel/types@7.28.1': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + + '@braintree/sanitize-url@7.1.1': {} + + '@bufbuild/buf-darwin-arm64@1.55.1': + optional: true + + '@bufbuild/buf-darwin-x64@1.55.1': + optional: true + + '@bufbuild/buf-linux-aarch64@1.55.1': + optional: true + + '@bufbuild/buf-linux-armv7@1.55.1': + optional: true + + '@bufbuild/buf-linux-x64@1.55.1': + optional: true + + '@bufbuild/buf-win32-arm64@1.55.1': + optional: true + + '@bufbuild/buf-win32-x64@1.55.1': + optional: true + + '@bufbuild/buf@1.55.1': + optionalDependencies: + '@bufbuild/buf-darwin-arm64': 1.55.1 + '@bufbuild/buf-darwin-x64': 1.55.1 + '@bufbuild/buf-linux-aarch64': 1.55.1 + '@bufbuild/buf-linux-armv7': 1.55.1 + '@bufbuild/buf-linux-x64': 1.55.1 + '@bufbuild/buf-win32-arm64': 1.55.1 + '@bufbuild/buf-win32-x64': 1.55.1 + + '@bufbuild/protobuf@2.6.1': {} + + '@bufbuild/protocompile@0.0.1(@bufbuild/buf@1.55.1)': + dependencies: + '@bufbuild/buf': 1.55.1 + '@bufbuild/protobuf': 2.6.1 + fflate: 0.8.2 + + '@changesets/apply-release-plan@7.0.12': + dependencies: + '@changesets/config': 3.1.1 + '@changesets/get-version-range-type': 0.4.0 + '@changesets/git': 3.0.4 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + detect-indent: 6.1.0 + fs-extra: 7.0.1 + lodash.startcase: 4.4.0 + outdent: 0.5.0 + prettier: 2.8.8 + resolve-from: 5.0.0 + semver: 7.7.2 + + '@changesets/assemble-release-plan@6.0.9': + dependencies: + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.3 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + semver: 7.7.2 + + '@changesets/changelog-git@0.2.1': + dependencies: + '@changesets/types': 6.1.0 + + '@changesets/cli@2.29.5': + dependencies: + '@changesets/apply-release-plan': 7.0.12 + '@changesets/assemble-release-plan': 6.0.9 + '@changesets/changelog-git': 0.2.1 + '@changesets/config': 3.1.1 + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.3 + '@changesets/get-release-plan': 4.0.13 + '@changesets/git': 3.0.4 + '@changesets/logger': 0.1.1 + '@changesets/pre': 2.0.2 + '@changesets/read': 0.6.5 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@changesets/write': 0.4.0 + '@manypkg/get-packages': 1.1.3 + ansi-colors: 4.1.3 + ci-info: 3.9.0 + enquirer: 2.4.1 + external-editor: 3.1.0 + fs-extra: 7.0.1 + mri: 1.2.0 + p-limit: 2.3.0 + package-manager-detector: 0.2.11 + picocolors: 1.1.1 + resolve-from: 5.0.0 + semver: 7.7.2 + spawndamnit: 3.0.1 + term-size: 2.2.1 + + '@changesets/config@3.1.1': + dependencies: + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.3 + '@changesets/logger': 0.1.1 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + fs-extra: 7.0.1 + micromatch: 4.0.8 + + '@changesets/errors@0.2.0': + dependencies: + extendable-error: 0.1.7 + + '@changesets/get-dependents-graph@2.1.3': + dependencies: + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + picocolors: 1.1.1 + semver: 7.7.2 + + '@changesets/get-release-plan@4.0.13': + dependencies: + '@changesets/assemble-release-plan': 6.0.9 + '@changesets/config': 3.1.1 + '@changesets/pre': 2.0.2 + '@changesets/read': 0.6.5 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + + '@changesets/get-version-range-type@0.4.0': {} + + '@changesets/git@3.0.4': + dependencies: + '@changesets/errors': 0.2.0 + '@manypkg/get-packages': 1.1.3 + is-subdir: 1.2.0 + micromatch: 4.0.8 + spawndamnit: 3.0.1 + + '@changesets/logger@0.1.1': + dependencies: + picocolors: 1.1.1 + + '@changesets/parse@0.4.1': + dependencies: + '@changesets/types': 6.1.0 + js-yaml: 3.14.1 + + '@changesets/pre@2.0.2': + dependencies: + '@changesets/errors': 0.2.0 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + fs-extra: 7.0.1 + + '@changesets/read@0.6.5': + dependencies: + '@changesets/git': 3.0.4 + '@changesets/logger': 0.1.1 + '@changesets/parse': 0.4.1 + '@changesets/types': 6.1.0 + fs-extra: 7.0.1 + p-filter: 2.1.0 + picocolors: 1.1.1 + + '@changesets/should-skip-package@0.1.2': + dependencies: + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + + '@changesets/types@4.1.0': {} + + '@changesets/types@6.1.0': {} + + '@changesets/write@0.4.0': + dependencies: + '@changesets/types': 6.1.0 + fs-extra: 7.0.1 + human-id: 4.1.1 + prettier: 2.8.8 + + '@chevrotain/cst-dts-gen@11.0.3': + dependencies: + '@chevrotain/gast': 11.0.3 + '@chevrotain/types': 11.0.3 + lodash-es: 4.17.21 + + '@chevrotain/gast@11.0.3': + dependencies: + '@chevrotain/types': 11.0.3 + lodash-es: 4.17.21 + + '@chevrotain/regexp-to-ast@11.0.3': {} + + '@chevrotain/types@11.0.3': {} + + '@chevrotain/utils@11.0.3': {} + + '@colors/colors@1.5.0': {} + + '@connectrpc/connect-node@2.0.2(@bufbuild/protobuf@2.6.1)(@connectrpc/connect@2.0.2(@bufbuild/protobuf@2.6.1))': + dependencies: + '@bufbuild/protobuf': 2.6.1 + '@connectrpc/connect': 2.0.2(@bufbuild/protobuf@2.6.1) + + '@connectrpc/connect-web@2.0.2(@bufbuild/protobuf@2.6.1)(@connectrpc/connect@2.0.2(@bufbuild/protobuf@2.6.1))': + dependencies: + '@bufbuild/protobuf': 2.6.1 + '@connectrpc/connect': 2.0.2(@bufbuild/protobuf@2.6.1) + + '@connectrpc/connect@2.0.2(@bufbuild/protobuf@2.6.1)': + dependencies: + '@bufbuild/protobuf': 2.6.1 + + '@csstools/cascade-layer-name-parser@2.0.5(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/color-helpers@5.0.2': {} + + '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-color-parser@3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/color-helpers': 5.0.2 + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-tokenizer@3.0.4': {} + + '@csstools/media-query-list-parser@4.0.3(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/postcss-cascade-layers@5.0.2(postcss@8.5.6)': + dependencies: + '@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.1.0) + postcss: 8.5.6 + postcss-selector-parser: 7.1.0 + + '@csstools/postcss-color-function@4.0.10(postcss@8.5.6)': + dependencies: + '@csstools/css-color-parser': 3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/postcss-progressive-custom-properties': 4.1.0(postcss@8.5.6) + '@csstools/utilities': 2.0.0(postcss@8.5.6) + postcss: 8.5.6 + + '@csstools/postcss-color-mix-function@3.0.10(postcss@8.5.6)': + dependencies: + '@csstools/css-color-parser': 3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/postcss-progressive-custom-properties': 4.1.0(postcss@8.5.6) + '@csstools/utilities': 2.0.0(postcss@8.5.6) + postcss: 8.5.6 + + '@csstools/postcss-color-mix-variadic-function-arguments@1.0.0(postcss@8.5.6)': + dependencies: + '@csstools/css-color-parser': 3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/postcss-progressive-custom-properties': 4.1.0(postcss@8.5.6) + '@csstools/utilities': 2.0.0(postcss@8.5.6) + postcss: 8.5.6 + + '@csstools/postcss-content-alt-text@2.0.6(postcss@8.5.6)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/postcss-progressive-custom-properties': 4.1.0(postcss@8.5.6) + '@csstools/utilities': 2.0.0(postcss@8.5.6) + postcss: 8.5.6 + + '@csstools/postcss-exponential-functions@2.0.9(postcss@8.5.6)': + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + postcss: 8.5.6 + + '@csstools/postcss-font-format-keywords@4.0.0(postcss@8.5.6)': + dependencies: + '@csstools/utilities': 2.0.0(postcss@8.5.6) + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + '@csstools/postcss-gamut-mapping@2.0.10(postcss@8.5.6)': + dependencies: + '@csstools/css-color-parser': 3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + postcss: 8.5.6 + + '@csstools/postcss-gradients-interpolation-method@5.0.10(postcss@8.5.6)': + dependencies: + '@csstools/css-color-parser': 3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/postcss-progressive-custom-properties': 4.1.0(postcss@8.5.6) + '@csstools/utilities': 2.0.0(postcss@8.5.6) + postcss: 8.5.6 + + '@csstools/postcss-hwb-function@4.0.10(postcss@8.5.6)': + dependencies: + '@csstools/css-color-parser': 3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/postcss-progressive-custom-properties': 4.1.0(postcss@8.5.6) + '@csstools/utilities': 2.0.0(postcss@8.5.6) + postcss: 8.5.6 + + '@csstools/postcss-ic-unit@4.0.2(postcss@8.5.6)': + dependencies: + '@csstools/postcss-progressive-custom-properties': 4.1.0(postcss@8.5.6) + '@csstools/utilities': 2.0.0(postcss@8.5.6) + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + '@csstools/postcss-initial@2.0.1(postcss@8.5.6)': + dependencies: + postcss: 8.5.6 + + '@csstools/postcss-is-pseudo-class@5.0.3(postcss@8.5.6)': + dependencies: + '@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.1.0) + postcss: 8.5.6 + postcss-selector-parser: 7.1.0 + + '@csstools/postcss-light-dark-function@2.0.9(postcss@8.5.6)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/postcss-progressive-custom-properties': 4.1.0(postcss@8.5.6) + '@csstools/utilities': 2.0.0(postcss@8.5.6) + postcss: 8.5.6 + + '@csstools/postcss-logical-float-and-clear@3.0.0(postcss@8.5.6)': + dependencies: + postcss: 8.5.6 + + '@csstools/postcss-logical-overflow@2.0.0(postcss@8.5.6)': + dependencies: + postcss: 8.5.6 + + '@csstools/postcss-logical-overscroll-behavior@2.0.0(postcss@8.5.6)': + dependencies: + postcss: 8.5.6 + + '@csstools/postcss-logical-resize@3.0.0(postcss@8.5.6)': + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + '@csstools/postcss-logical-viewport-units@3.0.4(postcss@8.5.6)': + dependencies: + '@csstools/css-tokenizer': 3.0.4 + '@csstools/utilities': 2.0.0(postcss@8.5.6) + postcss: 8.5.6 + + '@csstools/postcss-media-minmax@2.0.9(postcss@8.5.6)': + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/media-query-list-parser': 4.0.3(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + postcss: 8.5.6 + + '@csstools/postcss-media-queries-aspect-ratio-number-values@3.0.5(postcss@8.5.6)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/media-query-list-parser': 4.0.3(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + postcss: 8.5.6 + + '@csstools/postcss-nested-calc@4.0.0(postcss@8.5.6)': + dependencies: + '@csstools/utilities': 2.0.0(postcss@8.5.6) + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + '@csstools/postcss-normalize-display-values@4.0.0(postcss@8.5.6)': + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + '@csstools/postcss-oklab-function@4.0.10(postcss@8.5.6)': + dependencies: + '@csstools/css-color-parser': 3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/postcss-progressive-custom-properties': 4.1.0(postcss@8.5.6) + '@csstools/utilities': 2.0.0(postcss@8.5.6) + postcss: 8.5.6 + + '@csstools/postcss-progressive-custom-properties@4.1.0(postcss@8.5.6)': + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + '@csstools/postcss-random-function@2.0.1(postcss@8.5.6)': + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + postcss: 8.5.6 + + '@csstools/postcss-relative-color-syntax@3.0.10(postcss@8.5.6)': + dependencies: + '@csstools/css-color-parser': 3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/postcss-progressive-custom-properties': 4.1.0(postcss@8.5.6) + '@csstools/utilities': 2.0.0(postcss@8.5.6) + postcss: 8.5.6 + + '@csstools/postcss-scope-pseudo-class@4.0.1(postcss@8.5.6)': + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 7.1.0 + + '@csstools/postcss-sign-functions@1.1.4(postcss@8.5.6)': + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + postcss: 8.5.6 + + '@csstools/postcss-stepped-value-functions@4.0.9(postcss@8.5.6)': + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + postcss: 8.5.6 + + '@csstools/postcss-text-decoration-shorthand@4.0.2(postcss@8.5.6)': + dependencies: + '@csstools/color-helpers': 5.0.2 + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + '@csstools/postcss-trigonometric-functions@4.0.9(postcss@8.5.6)': + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + postcss: 8.5.6 + + '@csstools/postcss-unset-value@4.0.0(postcss@8.5.6)': + dependencies: + postcss: 8.5.6 + + '@csstools/selector-resolve-nested@3.1.0(postcss-selector-parser@7.1.0)': + dependencies: + postcss-selector-parser: 7.1.0 + + '@csstools/selector-specificity@5.0.0(postcss-selector-parser@7.1.0)': + dependencies: + postcss-selector-parser: 7.1.0 + + '@csstools/utilities@2.0.0(postcss@8.5.6)': + dependencies: + postcss: 8.5.6 + + '@ctrl/ngx-codemirror@6.1.0(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(@angular/forms@16.2.12(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(@angular/platform-browser@16.2.12(@angular/animations@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(rxjs@7.8.2))(codemirror@5.65.19)': + dependencies: + '@angular/core': 16.2.12(rxjs@7.8.2)(zone.js@0.13.3) + '@angular/forms': 16.2.12(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(@angular/platform-browser@16.2.12(@angular/animations@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(rxjs@7.8.2) + '@types/codemirror': 5.60.16 + codemirror: 5.65.19 + tslib: 2.8.1 + + '@ctrl/tinycolor@3.6.1': {} + + '@cypress/request@3.0.8': + dependencies: + aws-sign2: 0.7.0 + aws4: 1.13.2 + caseless: 0.12.0 + combined-stream: 1.0.8 + extend: 3.0.2 + forever-agent: 0.6.1 + form-data: 4.0.4 + http-signature: 1.4.0 + is-typedarray: 1.0.0 + isstream: 0.1.2 + json-stringify-safe: 5.0.1 + mime-types: 2.1.35 + performance-now: 2.1.0 + qs: 6.14.0 + safe-buffer: 5.2.1 + tough-cookie: 5.1.2 + tunnel-agent: 0.6.0 + uuid: 8.3.2 + + '@cypress/request@3.0.9': + dependencies: + aws-sign2: 0.7.0 + aws4: 1.13.2 + caseless: 0.12.0 + combined-stream: 1.0.8 + extend: 3.0.2 + forever-agent: 0.6.1 + form-data: 4.0.4 + http-signature: 1.4.0 + is-typedarray: 1.0.0 + isstream: 0.1.2 + json-stringify-safe: 5.0.1 + mime-types: 2.1.35 + performance-now: 2.1.0 + qs: 6.14.0 + safe-buffer: 5.2.1 + tough-cookie: 5.1.2 + tunnel-agent: 0.6.0 + uuid: 8.3.2 + + '@cypress/xvfb@1.2.4(supports-color@8.1.1)': + dependencies: + debug: 3.2.7(supports-color@8.1.1) + lodash.once: 4.1.1 + transitivePeerDependencies: + - supports-color + + '@devcontainers/cli@0.80.0': {} + + '@discoveryjs/json-ext@0.5.7': {} + + '@docsearch/css@3.9.0': {} + + '@docsearch/react@3.9.0(@algolia/client-search@5.34.0)(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)': + dependencies: + '@algolia/autocomplete-core': 1.17.9(@algolia/client-search@5.34.0)(algoliasearch@5.34.0)(search-insights@2.17.3) + '@algolia/autocomplete-preset-algolia': 1.17.9(@algolia/client-search@5.34.0)(algoliasearch@5.34.0) + '@docsearch/css': 3.9.0 + algoliasearch: 5.34.0 + optionalDependencies: + '@types/react': 19.1.2 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + search-insights: 2.17.3 + transitivePeerDependencies: + - '@algolia/client-search' + + '@docusaurus/babel@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/core': 7.28.0 + '@babel/generator': 7.28.0 + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.28.0) + '@babel/plugin-transform-runtime': 7.28.0(@babel/core@7.28.0) + '@babel/preset-env': 7.28.0(@babel/core@7.28.0) + '@babel/preset-react': 7.27.1(@babel/core@7.28.0) + '@babel/preset-typescript': 7.27.1(@babel/core@7.28.0) + '@babel/runtime': 7.27.6 + '@babel/runtime-corejs3': 7.28.0 + '@babel/traverse': 7.28.0 + '@docusaurus/logger': 3.8.1 + '@docusaurus/utils': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + babel-plugin-dynamic-import-node: 2.3.3 + fs-extra: 11.3.0 + tslib: 2.8.1 + transitivePeerDependencies: + - '@swc/core' + - acorn + - esbuild + - react + - react-dom + - supports-color + - uglify-js + - webpack-cli + + '@docusaurus/bundler@3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3)': + dependencies: + '@babel/core': 7.28.0 + '@docusaurus/babel': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/cssnano-preset': 3.8.1 + '@docusaurus/logger': 3.8.1 + '@docusaurus/types': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + babel-loader: 9.2.1(@babel/core@7.28.0)(webpack@5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17))) + clean-css: 5.3.3 + copy-webpack-plugin: 11.0.0(webpack@5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17))) + css-loader: 6.11.0(@rspack/core@1.4.9(@swc/helpers@0.5.17))(webpack@5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17))) + css-minimizer-webpack-plugin: 5.0.1(clean-css@5.3.3)(webpack@5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17))) + cssnano: 6.1.2(postcss@8.5.6) + file-loader: 6.2.0(webpack@5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17))) + html-minifier-terser: 7.2.0 + mini-css-extract-plugin: 2.9.2(webpack@5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17))) + null-loader: 4.0.1(webpack@5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17))) + postcss: 8.5.6 + postcss-loader: 7.3.4(postcss@8.5.6)(typescript@5.8.3)(webpack@5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17))) + postcss-preset-env: 10.2.4(postcss@8.5.6) + terser-webpack-plugin: 5.3.14(@swc/core@1.13.1(@swc/helpers@0.5.17))(webpack@5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17))) + tslib: 2.8.1 + url-loader: 4.1.1(file-loader@6.2.0(webpack@5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17))))(webpack@5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17))) + webpack: 5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17)) + webpackbar: 6.0.1(webpack@5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17))) + optionalDependencies: + '@docusaurus/faster': 3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17) + transitivePeerDependencies: + - '@parcel/css' + - '@rspack/core' + - '@swc/core' + - '@swc/css' + - acorn + - csso + - esbuild + - lightningcss + - react + - react-dom + - supports-color + - typescript + - uglify-js + - webpack-cli + + '@docusaurus/core@3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3)': + dependencies: + '@docusaurus/babel': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/bundler': 3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3) + '@docusaurus/logger': 3.8.1 + '@docusaurus/mdx-loader': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-common': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-validation': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@mdx-js/react': 3.1.0(@types/react@19.1.2)(react@18.3.1) + boxen: 6.2.1 + chalk: 4.1.2 + chokidar: 3.6.0 + cli-table3: 0.6.5 + combine-promises: 1.2.0 + commander: 5.1.0 + core-js: 3.44.0 + detect-port: 1.6.1 + escape-html: 1.0.3 + eta: 2.2.0 + eval: 0.1.8 + execa: 5.1.1 + fs-extra: 11.3.0 + html-tags: 3.3.1 + html-webpack-plugin: 5.6.3(@rspack/core@1.4.9(@swc/helpers@0.5.17))(webpack@5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17))) + leven: 3.1.0 + lodash: 4.17.21 + open: 8.4.2 + p-map: 4.0.0 + prompts: 2.4.2 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-helmet-async: '@slorber/react-helmet-async@1.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)' + react-loadable: '@docusaurus/react-loadable@6.0.0(react@18.3.1)' + react-loadable-ssr-addon-v5-slorber: 1.0.1(@docusaurus/react-loadable@6.0.0(react@18.3.1))(webpack@5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17))) + react-router: 5.3.4(react@18.3.1) + react-router-config: 5.1.1(react-router@5.3.4(react@18.3.1))(react@18.3.1) + react-router-dom: 5.3.4(react@18.3.1) + semver: 7.7.2 + serve-handler: 6.1.6 + tinypool: 1.1.1 + tslib: 2.8.1 + update-notifier: 6.0.2 + webpack: 5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17)) + webpack-bundle-analyzer: 4.10.2 + webpack-dev-server: 4.15.2(webpack@5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17))) + webpack-merge: 6.0.1 + transitivePeerDependencies: + - '@docusaurus/faster' + - '@parcel/css' + - '@rspack/core' + - '@swc/core' + - '@swc/css' + - acorn + - bufferutil + - csso + - debug + - esbuild + - lightningcss + - supports-color + - typescript + - uglify-js + - utf-8-validate + - webpack-cli + + '@docusaurus/cssnano-preset@3.8.1': + dependencies: + cssnano-preset-advanced: 6.1.2(postcss@8.5.6) + postcss: 8.5.6 + postcss-sort-media-queries: 5.2.0(postcss@8.5.6) + tslib: 2.8.1 + + '@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17)': + dependencies: + '@docusaurus/types': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@rspack/core': 1.4.9(@swc/helpers@0.5.17) + '@swc/core': 1.13.1(@swc/helpers@0.5.17) + '@swc/html': 1.13.1 + browserslist: 4.25.1 + lightningcss: 1.30.1 + swc-loader: 0.2.6(@swc/core@1.13.1(@swc/helpers@0.5.17))(webpack@5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17))) + tslib: 2.8.1 + webpack: 5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17)) + transitivePeerDependencies: + - '@swc/helpers' + - esbuild + - uglify-js + - webpack-cli + + '@docusaurus/logger@3.8.1': + dependencies: + chalk: 4.1.2 + tslib: 2.8.1 + + '@docusaurus/mdx-loader@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@docusaurus/logger': 3.8.1 + '@docusaurus/utils': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-validation': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@mdx-js/mdx': 3.1.0(acorn@8.15.0) + '@slorber/remark-comment': 1.0.0 + escape-html: 1.0.3 + estree-util-value-to-estree: 3.4.0 + file-loader: 6.2.0(webpack@5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17))) + fs-extra: 11.3.0 + image-size: 2.0.2 + mdast-util-mdx: 3.0.0 + mdast-util-to-string: 4.0.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + rehype-raw: 7.0.0 + remark-directive: 3.0.1 + remark-emoji: 4.0.1 + remark-frontmatter: 5.0.0 + remark-gfm: 4.0.1 + stringify-object: 3.3.0 + tslib: 2.8.1 + unified: 11.0.5 + unist-util-visit: 5.0.0 + url-loader: 4.1.1(file-loader@6.2.0(webpack@5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17))))(webpack@5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17))) + vfile: 6.0.3 + webpack: 5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17)) + transitivePeerDependencies: + - '@swc/core' + - acorn + - esbuild + - supports-color + - uglify-js + - webpack-cli + + '@docusaurus/module-type-aliases@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@docusaurus/types': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@types/history': 4.7.11 + '@types/react': 19.1.2 + '@types/react-router-config': 5.0.11 + '@types/react-router-dom': 5.3.3 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-helmet-async: '@slorber/react-helmet-async@1.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)' + react-loadable: '@docusaurus/react-loadable@6.0.0(react@18.3.1)' + transitivePeerDependencies: + - '@swc/core' + - acorn + - esbuild + - supports-color + - uglify-js + - webpack-cli + + '@docusaurus/plugin-content-blog@3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@docusaurus/plugin-content-docs@3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3)': + dependencies: + '@docusaurus/core': 3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3) + '@docusaurus/logger': 3.8.1 + '@docusaurus/mdx-loader': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/plugin-content-docs': 3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3) + '@docusaurus/theme-common': 3.8.1(@docusaurus/plugin-content-docs@3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/types': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-common': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-validation': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + cheerio: 1.0.0-rc.12 + feed: 4.2.2 + fs-extra: 11.3.0 + lodash: 4.17.21 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + schema-dts: 1.1.5 + srcset: 4.0.0 + tslib: 2.8.1 + unist-util-visit: 5.0.0 + utility-types: 3.11.0 + webpack: 5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17)) + transitivePeerDependencies: + - '@docusaurus/faster' + - '@mdx-js/react' + - '@parcel/css' + - '@rspack/core' + - '@swc/core' + - '@swc/css' + - acorn + - bufferutil + - csso + - debug + - esbuild + - lightningcss + - supports-color + - typescript + - uglify-js + - utf-8-validate + - webpack-cli + + '@docusaurus/plugin-content-docs@3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3)': + dependencies: + '@docusaurus/core': 3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3) + '@docusaurus/logger': 3.8.1 + '@docusaurus/mdx-loader': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/module-type-aliases': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/theme-common': 3.8.1(@docusaurus/plugin-content-docs@3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/types': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-common': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-validation': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@types/react-router-config': 5.0.11 + combine-promises: 1.2.0 + fs-extra: 11.3.0 + js-yaml: 4.1.0 + lodash: 4.17.21 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + schema-dts: 1.1.5 + tslib: 2.8.1 + utility-types: 3.11.0 + webpack: 5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17)) + transitivePeerDependencies: + - '@docusaurus/faster' + - '@mdx-js/react' + - '@parcel/css' + - '@rspack/core' + - '@swc/core' + - '@swc/css' + - acorn + - bufferutil + - csso + - debug + - esbuild + - lightningcss + - supports-color + - typescript + - uglify-js + - utf-8-validate + - webpack-cli + + '@docusaurus/plugin-content-pages@3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3)': + dependencies: + '@docusaurus/core': 3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3) + '@docusaurus/mdx-loader': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/types': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-validation': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + fs-extra: 11.3.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + tslib: 2.8.1 + webpack: 5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17)) + transitivePeerDependencies: + - '@docusaurus/faster' + - '@mdx-js/react' + - '@parcel/css' + - '@rspack/core' + - '@swc/core' + - '@swc/css' + - acorn + - bufferutil + - csso + - debug + - esbuild + - lightningcss + - supports-color + - typescript + - uglify-js + - utf-8-validate + - webpack-cli + + '@docusaurus/plugin-css-cascade-layers@3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3)': + dependencies: + '@docusaurus/core': 3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3) + '@docusaurus/types': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-validation': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + tslib: 2.8.1 + transitivePeerDependencies: + - '@docusaurus/faster' + - '@mdx-js/react' + - '@parcel/css' + - '@rspack/core' + - '@swc/core' + - '@swc/css' + - acorn + - bufferutil + - csso + - debug + - esbuild + - lightningcss + - react + - react-dom + - supports-color + - typescript + - uglify-js + - utf-8-validate + - webpack-cli + + '@docusaurus/plugin-debug@3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3)': + dependencies: + '@docusaurus/core': 3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3) + '@docusaurus/types': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + fs-extra: 11.3.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-json-view-lite: 2.4.1(react@18.3.1) + tslib: 2.8.1 + transitivePeerDependencies: + - '@docusaurus/faster' + - '@mdx-js/react' + - '@parcel/css' + - '@rspack/core' + - '@swc/core' + - '@swc/css' + - acorn + - bufferutil + - csso + - debug + - esbuild + - lightningcss + - supports-color + - typescript + - uglify-js + - utf-8-validate + - webpack-cli + + '@docusaurus/plugin-google-analytics@3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3)': + dependencies: + '@docusaurus/core': 3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3) + '@docusaurus/types': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-validation': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + tslib: 2.8.1 + transitivePeerDependencies: + - '@docusaurus/faster' + - '@mdx-js/react' + - '@parcel/css' + - '@rspack/core' + - '@swc/core' + - '@swc/css' + - acorn + - bufferutil + - csso + - debug + - esbuild + - lightningcss + - supports-color + - typescript + - uglify-js + - utf-8-validate + - webpack-cli + + '@docusaurus/plugin-google-gtag@3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3)': + dependencies: + '@docusaurus/core': 3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3) + '@docusaurus/types': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-validation': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@types/gtag.js': 0.0.12 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + tslib: 2.8.1 + transitivePeerDependencies: + - '@docusaurus/faster' + - '@mdx-js/react' + - '@parcel/css' + - '@rspack/core' + - '@swc/core' + - '@swc/css' + - acorn + - bufferutil + - csso + - debug + - esbuild + - lightningcss + - supports-color + - typescript + - uglify-js + - utf-8-validate + - webpack-cli + + '@docusaurus/plugin-google-tag-manager@3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3)': + dependencies: + '@docusaurus/core': 3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3) + '@docusaurus/types': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-validation': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + tslib: 2.8.1 + transitivePeerDependencies: + - '@docusaurus/faster' + - '@mdx-js/react' + - '@parcel/css' + - '@rspack/core' + - '@swc/core' + - '@swc/css' + - acorn + - bufferutil + - csso + - debug + - esbuild + - lightningcss + - supports-color + - typescript + - uglify-js + - utf-8-validate + - webpack-cli + + '@docusaurus/plugin-sitemap@3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3)': + dependencies: + '@docusaurus/core': 3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3) + '@docusaurus/logger': 3.8.1 + '@docusaurus/types': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-common': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-validation': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + fs-extra: 11.3.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + sitemap: 7.1.2 + tslib: 2.8.1 + transitivePeerDependencies: + - '@docusaurus/faster' + - '@mdx-js/react' + - '@parcel/css' + - '@rspack/core' + - '@swc/core' + - '@swc/css' + - acorn + - bufferutil + - csso + - debug + - esbuild + - lightningcss + - supports-color + - typescript + - uglify-js + - utf-8-validate + - webpack-cli + + '@docusaurus/plugin-svgr@3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3)': + dependencies: + '@docusaurus/core': 3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3) + '@docusaurus/types': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-validation': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@svgr/core': 8.1.0(typescript@5.8.3) + '@svgr/webpack': 8.1.0(typescript@5.8.3) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + tslib: 2.8.1 + webpack: 5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17)) + transitivePeerDependencies: + - '@docusaurus/faster' + - '@mdx-js/react' + - '@parcel/css' + - '@rspack/core' + - '@swc/core' + - '@swc/css' + - acorn + - bufferutil + - csso + - debug + - esbuild + - lightningcss + - supports-color + - typescript + - uglify-js + - utf-8-validate + - webpack-cli + + '@docusaurus/preset-classic@3.8.1(@algolia/client-search@5.34.0)(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(@types/react@19.1.2)(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@5.8.3)': + dependencies: + '@docusaurus/core': 3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3) + '@docusaurus/plugin-content-blog': 3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@docusaurus/plugin-content-docs@3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3) + '@docusaurus/plugin-content-docs': 3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3) + '@docusaurus/plugin-content-pages': 3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3) + '@docusaurus/plugin-css-cascade-layers': 3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3) + '@docusaurus/plugin-debug': 3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3) + '@docusaurus/plugin-google-analytics': 3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3) + '@docusaurus/plugin-google-gtag': 3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3) + '@docusaurus/plugin-google-tag-manager': 3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3) + '@docusaurus/plugin-sitemap': 3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3) + '@docusaurus/plugin-svgr': 3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3) + '@docusaurus/theme-classic': 3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(@types/react@19.1.2)(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3) + '@docusaurus/theme-common': 3.8.1(@docusaurus/plugin-content-docs@3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/theme-search-algolia': 3.8.1(@algolia/client-search@5.34.0)(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(@types/react@19.1.2)(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@5.8.3) + '@docusaurus/types': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + transitivePeerDependencies: + - '@algolia/client-search' + - '@docusaurus/faster' + - '@mdx-js/react' + - '@parcel/css' + - '@rspack/core' + - '@swc/core' + - '@swc/css' + - '@types/react' + - acorn + - bufferutil + - csso + - debug + - esbuild + - lightningcss + - search-insights + - supports-color + - typescript + - uglify-js + - utf-8-validate + - webpack-cli + + '@docusaurus/react-loadable@6.0.0(react@18.3.1)': + dependencies: + '@types/react': 19.1.2 + react: 18.3.1 + + '@docusaurus/theme-classic@3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(@types/react@19.1.2)(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3)': + dependencies: + '@docusaurus/core': 3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3) + '@docusaurus/logger': 3.8.1 + '@docusaurus/mdx-loader': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/module-type-aliases': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/plugin-content-blog': 3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@docusaurus/plugin-content-docs@3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3) + '@docusaurus/plugin-content-docs': 3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3) + '@docusaurus/plugin-content-pages': 3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3) + '@docusaurus/theme-common': 3.8.1(@docusaurus/plugin-content-docs@3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/theme-translations': 3.8.1 + '@docusaurus/types': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-common': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-validation': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@mdx-js/react': 3.1.0(@types/react@19.1.2)(react@18.3.1) + clsx: 2.1.1 + copy-text-to-clipboard: 3.2.0 + infima: 0.2.0-alpha.45 + lodash: 4.17.21 + nprogress: 0.2.0 + postcss: 8.5.6 + prism-react-renderer: 2.4.1(react@18.3.1) + prismjs: 1.30.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-router-dom: 5.3.4(react@18.3.1) + rtlcss: 4.3.0 + tslib: 2.8.1 + utility-types: 3.11.0 + transitivePeerDependencies: + - '@docusaurus/faster' + - '@parcel/css' + - '@rspack/core' + - '@swc/core' + - '@swc/css' + - '@types/react' + - acorn + - bufferutil + - csso + - debug + - esbuild + - lightningcss + - supports-color + - typescript + - uglify-js + - utf-8-validate + - webpack-cli + + '@docusaurus/theme-common@3.8.1(@docusaurus/plugin-content-docs@3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@docusaurus/mdx-loader': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/module-type-aliases': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/plugin-content-docs': 3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3) + '@docusaurus/utils': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-common': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@types/history': 4.7.11 + '@types/react': 19.1.2 + '@types/react-router-config': 5.0.11 + clsx: 2.1.1 + parse-numeric-range: 1.3.0 + prism-react-renderer: 2.4.1(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + tslib: 2.8.1 + utility-types: 3.11.0 + transitivePeerDependencies: + - '@swc/core' + - acorn + - esbuild + - supports-color + - uglify-js + - webpack-cli + + '@docusaurus/theme-mermaid@3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@docusaurus/plugin-content-docs@3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3)': + dependencies: + '@docusaurus/core': 3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3) + '@docusaurus/module-type-aliases': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/theme-common': 3.8.1(@docusaurus/plugin-content-docs@3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/types': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-validation': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + mermaid: 11.9.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + tslib: 2.8.1 + transitivePeerDependencies: + - '@docusaurus/faster' + - '@docusaurus/plugin-content-docs' + - '@mdx-js/react' + - '@parcel/css' + - '@rspack/core' + - '@swc/core' + - '@swc/css' + - acorn + - bufferutil + - csso + - debug + - esbuild + - lightningcss + - supports-color + - typescript + - uglify-js + - utf-8-validate + - webpack-cli + + '@docusaurus/theme-search-algolia@3.8.1(@algolia/client-search@5.34.0)(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(@types/react@19.1.2)(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@5.8.3)': + dependencies: + '@docsearch/react': 3.9.0(@algolia/client-search@5.34.0)(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3) + '@docusaurus/core': 3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3) + '@docusaurus/logger': 3.8.1 + '@docusaurus/plugin-content-docs': 3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3) + '@docusaurus/theme-common': 3.8.1(@docusaurus/plugin-content-docs@3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/theme-translations': 3.8.1 + '@docusaurus/utils': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-validation': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + algoliasearch: 5.34.0 + algoliasearch-helper: 3.26.0(algoliasearch@5.34.0) + clsx: 2.1.1 + eta: 2.2.0 + fs-extra: 11.3.0 + lodash: 4.17.21 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + tslib: 2.8.1 + utility-types: 3.11.0 + transitivePeerDependencies: + - '@algolia/client-search' + - '@docusaurus/faster' + - '@mdx-js/react' + - '@parcel/css' + - '@rspack/core' + - '@swc/core' + - '@swc/css' + - '@types/react' + - acorn + - bufferutil + - csso + - debug + - esbuild + - lightningcss + - search-insights + - supports-color + - typescript + - uglify-js + - utf-8-validate + - webpack-cli + + '@docusaurus/theme-translations@3.8.1': + dependencies: + fs-extra: 11.3.0 + tslib: 2.8.1 + + '@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@mdx-js/mdx': 3.1.0(acorn@8.15.0) + '@types/history': 4.7.11 + '@types/react': 19.1.2 + commander: 5.1.0 + joi: 17.13.3 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-helmet-async: '@slorber/react-helmet-async@1.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)' + utility-types: 3.11.0 + webpack: 5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17)) + webpack-merge: 5.10.0 + transitivePeerDependencies: + - '@swc/core' + - acorn + - esbuild + - supports-color + - uglify-js + - webpack-cli + + '@docusaurus/utils-common@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@docusaurus/types': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + tslib: 2.8.1 + transitivePeerDependencies: + - '@swc/core' + - acorn + - esbuild + - react + - react-dom + - supports-color + - uglify-js + - webpack-cli + + '@docusaurus/utils-validation@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@docusaurus/logger': 3.8.1 + '@docusaurus/utils': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-common': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + fs-extra: 11.3.0 + joi: 17.13.3 + js-yaml: 4.1.0 + lodash: 4.17.21 + tslib: 2.8.1 + transitivePeerDependencies: + - '@swc/core' + - acorn + - esbuild + - react + - react-dom + - supports-color + - uglify-js + - webpack-cli + + '@docusaurus/utils@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@docusaurus/logger': 3.8.1 + '@docusaurus/types': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-common': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + escape-string-regexp: 4.0.0 + execa: 5.1.1 + file-loader: 6.2.0(webpack@5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17))) + fs-extra: 11.3.0 + github-slugger: 1.5.0 + globby: 11.1.0 + gray-matter: 4.0.3 + jiti: 1.21.7 + js-yaml: 4.1.0 + lodash: 4.17.21 + micromatch: 4.0.8 + p-queue: 6.6.2 + prompts: 2.4.2 + resolve-pathname: 3.0.0 + tslib: 2.8.1 + url-loader: 4.1.1(file-loader@6.2.0(webpack@5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17))))(webpack@5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17))) + utility-types: 3.11.0 + webpack: 5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17)) + transitivePeerDependencies: + - '@swc/core' + - acorn + - esbuild + - react + - react-dom + - supports-color + - uglify-js + - webpack-cli + + '@emnapi/core@1.4.5': + dependencies: + '@emnapi/wasi-threads': 1.0.4 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.4.5': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.0.4': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/aix-ppc64@0.25.8': + optional: true + + '@esbuild/android-arm64@0.18.17': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.25.8': + optional: true + + '@esbuild/android-arm@0.18.17': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-arm@0.25.8': + optional: true + + '@esbuild/android-x64@0.18.17': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/android-x64@0.25.8': + optional: true + + '@esbuild/darwin-arm64@0.18.17': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.25.8': + optional: true + + '@esbuild/darwin-x64@0.18.17': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.25.8': + optional: true + + '@esbuild/freebsd-arm64@0.18.17': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.25.8': + optional: true + + '@esbuild/freebsd-x64@0.18.17': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.25.8': + optional: true + + '@esbuild/linux-arm64@0.18.17': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.25.8': + optional: true + + '@esbuild/linux-arm@0.18.17': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-arm@0.25.8': + optional: true + + '@esbuild/linux-ia32@0.18.17': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.25.8': + optional: true + + '@esbuild/linux-loong64@0.18.17': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.25.8': + optional: true + + '@esbuild/linux-mips64el@0.18.17': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.25.8': + optional: true + + '@esbuild/linux-ppc64@0.18.17': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.25.8': + optional: true + + '@esbuild/linux-riscv64@0.18.17': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.25.8': + optional: true + + '@esbuild/linux-s390x@0.18.17': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.25.8': + optional: true + + '@esbuild/linux-x64@0.18.17': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/linux-x64@0.25.8': + optional: true + + '@esbuild/netbsd-arm64@0.25.8': + optional: true + + '@esbuild/netbsd-x64@0.18.17': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.25.8': + optional: true + + '@esbuild/openbsd-arm64@0.25.8': + optional: true + + '@esbuild/openbsd-x64@0.18.17': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.25.8': + optional: true + + '@esbuild/openharmony-arm64@0.25.8': + optional: true + + '@esbuild/sunos-x64@0.18.17': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.25.8': + optional: true + + '@esbuild/win32-arm64@0.18.17': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.25.8': + optional: true + + '@esbuild/win32-ia32@0.18.17': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.25.8': + optional: true + + '@esbuild/win32-x64@0.18.17': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@esbuild/win32-x64@0.25.8': + optional: true + + '@eslint-community/eslint-utils@4.7.0(eslint@8.57.1)': + dependencies: + eslint: 8.57.1 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.1': {} + + '@eslint/eslintrc@2.1.4': + dependencies: + ajv: 6.12.6 + debug: 4.4.1(supports-color@5.5.0) + espree: 9.6.1 + globals: 13.24.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@8.57.1': {} + + '@exodus/schemasafe@1.3.0': {} + + '@faker-js/faker@5.5.3': {} + + '@faker-js/faker@9.9.0': {} + + '@floating-ui/core@1.7.2': + dependencies: + '@floating-ui/utils': 0.2.10 + + '@floating-ui/dom@1.7.2': + dependencies: + '@floating-ui/core': 1.7.2 + '@floating-ui/utils': 0.2.10 + + '@floating-ui/react-dom@2.1.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/dom': 1.7.2 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@floating-ui/react-dom@2.1.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@floating-ui/dom': 1.7.2 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + + '@floating-ui/react@0.26.28(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@floating-ui/react-dom': 2.1.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@floating-ui/utils': 0.2.10 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + tabbable: 6.2.0 + + '@floating-ui/utils@0.2.10': {} + + '@formatjs/ecma402-abstract@2.3.4': + dependencies: + '@formatjs/fast-memoize': 2.2.7 + '@formatjs/intl-localematcher': 0.6.1 + decimal.js: 10.6.0 + tslib: 2.8.1 + + '@formatjs/fast-memoize@2.2.7': + dependencies: + tslib: 2.8.1 + + '@formatjs/icu-messageformat-parser@2.11.2': + dependencies: + '@formatjs/ecma402-abstract': 2.3.4 + '@formatjs/icu-skeleton-parser': 1.8.14 + tslib: 2.8.1 + + '@formatjs/icu-skeleton-parser@1.8.14': + dependencies: + '@formatjs/ecma402-abstract': 2.3.4 + tslib: 2.8.1 + + '@formatjs/intl-localematcher@0.5.10': + dependencies: + tslib: 2.8.1 + + '@formatjs/intl-localematcher@0.6.1': + dependencies: + tslib: 2.8.1 + + '@fortawesome/angular-fontawesome@0.13.0(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(@fortawesome/fontawesome-svg-core@6.7.2)': + dependencies: + '@angular/core': 16.2.12(rxjs@7.8.2)(zone.js@0.13.3) + '@fortawesome/fontawesome-svg-core': 6.7.2 + tslib: 2.8.1 + + '@fortawesome/fontawesome-common-types@6.7.2': {} + + '@fortawesome/fontawesome-svg-core@6.7.2': + dependencies: + '@fortawesome/fontawesome-common-types': 6.7.2 + + '@fortawesome/free-brands-svg-icons@6.7.2': + dependencies: + '@fortawesome/fontawesome-common-types': 6.7.2 + + '@gar/promisify@1.1.3': {} + + '@grpc/grpc-js@1.13.4': + dependencies: + '@grpc/proto-loader': 0.7.15 + '@js-sdsl/ordered-map': 4.4.2 + + '@grpc/proto-loader@0.7.15': + dependencies: + lodash.camelcase: 4.3.0 + long: 5.3.2 + protobufjs: 7.5.3 + yargs: 17.7.2 + + '@hapi/hoek@9.3.0': {} + + '@hapi/topo@5.1.0': + dependencies: + '@hapi/hoek': 9.3.0 + + '@headlessui/react@1.7.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@tanstack/react-virtual': 3.13.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + client-only: 0.0.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@headlessui/react@2.2.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@floating-ui/react': 0.26.28(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@react-aria/focus': 3.20.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@react-aria/interactions': 3.25.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@tanstack/react-virtual': 3.13.12(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + use-sync-external-store: 1.5.0(react@19.1.0) + + '@heroicons/react@2.1.3(react@18.3.1)': + dependencies: + react: 18.3.1 + + '@heroicons/react@2.1.3(react@19.1.0)': + dependencies: + react: 19.1.0 + + '@hookform/error-message@2.0.1(react-dom@18.3.1(react@18.3.1))(react-hook-form@7.60.0(react@18.3.1))(react@18.3.1)': + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-hook-form: 7.60.0(react@18.3.1) + + '@humanwhocodes/config-array@0.13.0': + dependencies: + '@humanwhocodes/object-schema': 2.0.3 + debug: 4.4.1(supports-color@5.5.0) + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/object-schema@2.0.3': {} + + '@iconify/types@2.0.0': {} + + '@iconify/utils@2.3.0': + dependencies: + '@antfu/install-pkg': 1.1.0 + '@antfu/utils': 8.1.1 + '@iconify/types': 2.0.0 + debug: 4.4.1(supports-color@5.5.0) + globals: 15.15.0 + kolorist: 1.8.0 + local-pkg: 1.1.1 + mlly: 1.7.4 + transitivePeerDependencies: + - supports-color + + '@img/sharp-darwin-arm64@0.34.3': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.0 + optional: true + + '@img/sharp-darwin-x64@0.34.3': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.0 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.0': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.0': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.0': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.0': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.0': + optional: true + + '@img/sharp-libvips-linux-s390x@1.2.0': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.0': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.0': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.0': + optional: true + + '@img/sharp-linux-arm64@0.34.3': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.0 + optional: true + + '@img/sharp-linux-arm@0.34.3': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.0 + optional: true + + '@img/sharp-linux-ppc64@0.34.3': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.0 + optional: true + + '@img/sharp-linux-s390x@0.34.3': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.0 + optional: true + + '@img/sharp-linux-x64@0.34.3': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.0 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.3': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.0 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.3': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.0 + optional: true + + '@img/sharp-wasm32@0.34.3': + dependencies: + '@emnapi/runtime': 1.4.5 + optional: true + + '@img/sharp-win32-arm64@0.34.3': + optional: true + + '@img/sharp-win32-ia32@0.34.3': + optional: true + + '@img/sharp-win32-x64@0.34.3': + optional: true + + '@inkeep/cxkit-color-mode@0.5.95': {} + + '@inkeep/cxkit-docusaurus@0.5.95(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(zod@3.25.76)': + dependencies: + '@inkeep/cxkit-react': 0.5.95(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(zod@3.25.76) + merge-anything: 5.1.7 + path: 0.12.7 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + - encoding + - supports-color + - zod + + '@inkeep/cxkit-primitives@0.5.95(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(zod@3.25.76)': + dependencies: + '@inkeep/cxkit-color-mode': 0.5.95 + '@inkeep/cxkit-theme': 0.5.95 + '@inkeep/cxkit-types': 0.5.95 + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-avatar': 1.1.2(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-checkbox': 1.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.2(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-hover-card': 1.1.14(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/react-popover': 1.1.6(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-popper': 1.2.7(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-scroll-area': 1.2.2(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/react-tabs': 1.1.12(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-tooltip': 1.1.6(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.2)(react@18.3.1) + '@zag-js/focus-trap': 1.19.0 + '@zag-js/presence': 1.19.0 + '@zag-js/react': 1.19.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + altcha-lib: 1.3.0 + aria-hidden: 1.2.6 + dequal: 2.0.3 + humps: 2.0.1 + lucide-react: 0.503.0(react@18.3.1) + marked: 15.0.12 + merge-anything: 5.1.7 + openai: 4.78.1(encoding@0.1.13)(zod@3.25.76) + prism-react-renderer: 2.4.1(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-error-boundary: 6.0.0(react@18.3.1) + react-hook-form: 7.54.2(react@18.3.1) + react-markdown: 9.0.3(@types/react@19.1.2)(react@18.3.1) + react-remove-scroll: 2.7.1(@types/react@19.1.2)(react@18.3.1) + react-svg: 16.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-textarea-autosize: 8.5.7(@types/react@19.1.2)(react@18.3.1) + rehype-raw: 7.0.0 + remark-gfm: 4.0.1 + unist-util-visit: 5.0.0 + use-sync-external-store: 1.5.0(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + - encoding + - supports-color + - zod + + '@inkeep/cxkit-react@0.5.95(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(zod@3.25.76)': + dependencies: + '@inkeep/cxkit-styled': 0.5.95(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(zod@3.25.76) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.2)(react@18.3.1) + lucide-react: 0.503.0(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + - encoding + - react + - react-dom + - supports-color + - zod + + '@inkeep/cxkit-styled@0.5.95(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(zod@3.25.76)': + dependencies: + '@inkeep/cxkit-primitives': 0.5.95(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(zod@3.25.76) + class-variance-authority: 0.7.1 + clsx: 2.1.1 + merge-anything: 5.1.7 + tailwind-merge: 2.6.0 + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + - encoding + - react + - react-dom + - supports-color + - zod + + '@inkeep/cxkit-theme@0.5.95': + dependencies: + colorjs.io: 0.5.2 + + '@inkeep/cxkit-types@0.5.95': {} + + '@isaacs/balanced-match@4.0.1': {} + + '@isaacs/brace-expansion@5.0.0': + dependencies: + '@isaacs/balanced-match': 4.0.1 + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@istanbuljs/load-nyc-config@1.1.0': + dependencies: + camelcase: 5.3.1 + find-up: 4.1.0 + get-package-type: 0.1.0 + js-yaml: 3.14.1 + resolve-from: 5.0.0 + + '@istanbuljs/schema@0.1.3': {} + + '@jest/schemas@29.6.3': + dependencies: + '@sinclair/typebox': 0.27.8 + + '@jest/types@29.6.3': + dependencies: + '@jest/schemas': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 + '@types/node': 22.16.5 + '@types/yargs': 17.0.33 + chalk: 4.1.2 + + '@jridgewell/gen-mapping@0.3.12': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.4 + '@jridgewell/trace-mapping': 0.3.29 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/source-map@0.3.10': + dependencies: + '@jridgewell/gen-mapping': 0.3.12 + '@jridgewell/trace-mapping': 0.3.29 + + '@jridgewell/sourcemap-codec@1.5.4': {} + + '@jridgewell/trace-mapping@0.3.29': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.4 + + '@js-sdsl/ordered-map@4.4.2': {} + + '@jsdevtools/ono@7.1.3': {} + + '@leichtgewicht/ip-codec@2.0.5': {} + + '@manypkg/find-root@1.1.0': + dependencies: + '@babel/runtime': 7.27.6 + '@types/node': 12.20.55 + find-up: 4.1.0 + fs-extra: 8.1.0 + + '@manypkg/get-packages@1.1.3': + dependencies: + '@babel/runtime': 7.27.6 + '@changesets/types': 4.1.0 + '@manypkg/find-root': 1.1.0 + fs-extra: 8.1.0 + globby: 11.1.0 + read-yaml-file: 1.1.0 + + '@mapbox/node-pre-gyp@1.0.11(encoding@0.1.13)': + dependencies: + detect-libc: 2.0.4 + https-proxy-agent: 5.0.1 + make-dir: 3.1.0 + node-fetch: 2.7.0(encoding@0.1.13) + nopt: 5.0.0 + npmlog: 5.0.1 + rimraf: 3.0.2 + semver: 7.7.2 + tar: 6.2.1 + transitivePeerDependencies: + - encoding + - supports-color + + '@material/animation@15.0.0-canary.bc9ae6c9c.0': + dependencies: + tslib: 2.8.1 + + '@material/auto-init@15.0.0-canary.bc9ae6c9c.0': + dependencies: + '@material/base': 15.0.0-canary.bc9ae6c9c.0 + tslib: 2.8.1 + + '@material/banner@15.0.0-canary.bc9ae6c9c.0': + dependencies: + '@material/base': 15.0.0-canary.bc9ae6c9c.0 + '@material/button': 15.0.0-canary.bc9ae6c9c.0 + '@material/dom': 15.0.0-canary.bc9ae6c9c.0 + '@material/elevation': 15.0.0-canary.bc9ae6c9c.0 + '@material/feature-targeting': 15.0.0-canary.bc9ae6c9c.0 + '@material/ripple': 15.0.0-canary.bc9ae6c9c.0 + '@material/rtl': 15.0.0-canary.bc9ae6c9c.0 + '@material/shape': 15.0.0-canary.bc9ae6c9c.0 + '@material/theme': 15.0.0-canary.bc9ae6c9c.0 + '@material/tokens': 15.0.0-canary.bc9ae6c9c.0 + '@material/typography': 15.0.0-canary.bc9ae6c9c.0 + tslib: 2.8.1 + + '@material/base@15.0.0-canary.bc9ae6c9c.0': + dependencies: + tslib: 2.8.1 + + '@material/button@15.0.0-canary.bc9ae6c9c.0': + dependencies: + '@material/density': 15.0.0-canary.bc9ae6c9c.0 + '@material/dom': 15.0.0-canary.bc9ae6c9c.0 + '@material/elevation': 15.0.0-canary.bc9ae6c9c.0 + '@material/feature-targeting': 15.0.0-canary.bc9ae6c9c.0 + '@material/focus-ring': 15.0.0-canary.bc9ae6c9c.0 + '@material/ripple': 15.0.0-canary.bc9ae6c9c.0 + '@material/rtl': 15.0.0-canary.bc9ae6c9c.0 + '@material/shape': 15.0.0-canary.bc9ae6c9c.0 + '@material/theme': 15.0.0-canary.bc9ae6c9c.0 + '@material/tokens': 15.0.0-canary.bc9ae6c9c.0 + '@material/touch-target': 15.0.0-canary.bc9ae6c9c.0 + '@material/typography': 15.0.0-canary.bc9ae6c9c.0 + tslib: 2.8.1 + + '@material/card@15.0.0-canary.bc9ae6c9c.0': + dependencies: + '@material/dom': 15.0.0-canary.bc9ae6c9c.0 + '@material/elevation': 15.0.0-canary.bc9ae6c9c.0 + '@material/feature-targeting': 15.0.0-canary.bc9ae6c9c.0 + '@material/ripple': 15.0.0-canary.bc9ae6c9c.0 + '@material/rtl': 15.0.0-canary.bc9ae6c9c.0 + '@material/shape': 15.0.0-canary.bc9ae6c9c.0 + '@material/theme': 15.0.0-canary.bc9ae6c9c.0 + '@material/tokens': 15.0.0-canary.bc9ae6c9c.0 + tslib: 2.8.1 + + '@material/checkbox@15.0.0-canary.bc9ae6c9c.0': + dependencies: + '@material/animation': 15.0.0-canary.bc9ae6c9c.0 + '@material/base': 15.0.0-canary.bc9ae6c9c.0 + '@material/density': 15.0.0-canary.bc9ae6c9c.0 + '@material/dom': 15.0.0-canary.bc9ae6c9c.0 + '@material/feature-targeting': 15.0.0-canary.bc9ae6c9c.0 + '@material/focus-ring': 15.0.0-canary.bc9ae6c9c.0 + '@material/ripple': 15.0.0-canary.bc9ae6c9c.0 + '@material/rtl': 15.0.0-canary.bc9ae6c9c.0 + '@material/theme': 15.0.0-canary.bc9ae6c9c.0 + '@material/touch-target': 15.0.0-canary.bc9ae6c9c.0 + tslib: 2.8.1 + + '@material/chips@15.0.0-canary.bc9ae6c9c.0': + dependencies: + '@material/animation': 15.0.0-canary.bc9ae6c9c.0 + '@material/base': 15.0.0-canary.bc9ae6c9c.0 + '@material/checkbox': 15.0.0-canary.bc9ae6c9c.0 + '@material/density': 15.0.0-canary.bc9ae6c9c.0 + '@material/dom': 15.0.0-canary.bc9ae6c9c.0 + '@material/elevation': 15.0.0-canary.bc9ae6c9c.0 + '@material/feature-targeting': 15.0.0-canary.bc9ae6c9c.0 + '@material/focus-ring': 15.0.0-canary.bc9ae6c9c.0 + '@material/ripple': 15.0.0-canary.bc9ae6c9c.0 + '@material/rtl': 15.0.0-canary.bc9ae6c9c.0 + '@material/shape': 15.0.0-canary.bc9ae6c9c.0 + '@material/theme': 15.0.0-canary.bc9ae6c9c.0 + '@material/tokens': 15.0.0-canary.bc9ae6c9c.0 + '@material/touch-target': 15.0.0-canary.bc9ae6c9c.0 + '@material/typography': 15.0.0-canary.bc9ae6c9c.0 + safevalues: 0.3.4 + tslib: 2.8.1 + + '@material/circular-progress@15.0.0-canary.bc9ae6c9c.0': + dependencies: + '@material/animation': 15.0.0-canary.bc9ae6c9c.0 + '@material/base': 15.0.0-canary.bc9ae6c9c.0 + '@material/dom': 15.0.0-canary.bc9ae6c9c.0 + '@material/feature-targeting': 15.0.0-canary.bc9ae6c9c.0 + '@material/progress-indicator': 15.0.0-canary.bc9ae6c9c.0 + '@material/rtl': 15.0.0-canary.bc9ae6c9c.0 + '@material/theme': 15.0.0-canary.bc9ae6c9c.0 + tslib: 2.8.1 + + '@material/data-table@15.0.0-canary.bc9ae6c9c.0': + dependencies: + '@material/animation': 15.0.0-canary.bc9ae6c9c.0 + '@material/base': 15.0.0-canary.bc9ae6c9c.0 + '@material/checkbox': 15.0.0-canary.bc9ae6c9c.0 + '@material/density': 15.0.0-canary.bc9ae6c9c.0 + '@material/dom': 15.0.0-canary.bc9ae6c9c.0 + '@material/elevation': 15.0.0-canary.bc9ae6c9c.0 + '@material/feature-targeting': 15.0.0-canary.bc9ae6c9c.0 + '@material/icon-button': 15.0.0-canary.bc9ae6c9c.0 + '@material/linear-progress': 15.0.0-canary.bc9ae6c9c.0 + '@material/list': 15.0.0-canary.bc9ae6c9c.0 + '@material/menu': 15.0.0-canary.bc9ae6c9c.0 + '@material/rtl': 15.0.0-canary.bc9ae6c9c.0 + '@material/select': 15.0.0-canary.bc9ae6c9c.0 + '@material/shape': 15.0.0-canary.bc9ae6c9c.0 + '@material/theme': 15.0.0-canary.bc9ae6c9c.0 + '@material/tokens': 15.0.0-canary.bc9ae6c9c.0 + '@material/touch-target': 15.0.0-canary.bc9ae6c9c.0 + '@material/typography': 15.0.0-canary.bc9ae6c9c.0 + tslib: 2.8.1 + + '@material/density@15.0.0-canary.bc9ae6c9c.0': + dependencies: + tslib: 2.8.1 + + '@material/dialog@15.0.0-canary.bc9ae6c9c.0': + dependencies: + '@material/animation': 15.0.0-canary.bc9ae6c9c.0 + '@material/base': 15.0.0-canary.bc9ae6c9c.0 + '@material/button': 15.0.0-canary.bc9ae6c9c.0 + '@material/dom': 15.0.0-canary.bc9ae6c9c.0 + '@material/elevation': 15.0.0-canary.bc9ae6c9c.0 + '@material/feature-targeting': 15.0.0-canary.bc9ae6c9c.0 + '@material/icon-button': 15.0.0-canary.bc9ae6c9c.0 + '@material/ripple': 15.0.0-canary.bc9ae6c9c.0 + '@material/rtl': 15.0.0-canary.bc9ae6c9c.0 + '@material/shape': 15.0.0-canary.bc9ae6c9c.0 + '@material/theme': 15.0.0-canary.bc9ae6c9c.0 + '@material/tokens': 15.0.0-canary.bc9ae6c9c.0 + '@material/touch-target': 15.0.0-canary.bc9ae6c9c.0 + '@material/typography': 15.0.0-canary.bc9ae6c9c.0 + tslib: 2.8.1 + + '@material/dom@15.0.0-canary.bc9ae6c9c.0': + dependencies: + '@material/feature-targeting': 15.0.0-canary.bc9ae6c9c.0 + '@material/rtl': 15.0.0-canary.bc9ae6c9c.0 + tslib: 2.8.1 + + '@material/drawer@15.0.0-canary.bc9ae6c9c.0': + dependencies: + '@material/animation': 15.0.0-canary.bc9ae6c9c.0 + '@material/base': 15.0.0-canary.bc9ae6c9c.0 + '@material/dom': 15.0.0-canary.bc9ae6c9c.0 + '@material/elevation': 15.0.0-canary.bc9ae6c9c.0 + '@material/feature-targeting': 15.0.0-canary.bc9ae6c9c.0 + '@material/list': 15.0.0-canary.bc9ae6c9c.0 + '@material/ripple': 15.0.0-canary.bc9ae6c9c.0 + '@material/rtl': 15.0.0-canary.bc9ae6c9c.0 + '@material/shape': 15.0.0-canary.bc9ae6c9c.0 + '@material/theme': 15.0.0-canary.bc9ae6c9c.0 + '@material/typography': 15.0.0-canary.bc9ae6c9c.0 + tslib: 2.8.1 + + '@material/elevation@15.0.0-canary.bc9ae6c9c.0': + dependencies: + '@material/animation': 15.0.0-canary.bc9ae6c9c.0 + '@material/base': 15.0.0-canary.bc9ae6c9c.0 + '@material/feature-targeting': 15.0.0-canary.bc9ae6c9c.0 + '@material/rtl': 15.0.0-canary.bc9ae6c9c.0 + '@material/theme': 15.0.0-canary.bc9ae6c9c.0 + tslib: 2.8.1 + + '@material/fab@15.0.0-canary.bc9ae6c9c.0': + dependencies: + '@material/animation': 15.0.0-canary.bc9ae6c9c.0 + '@material/dom': 15.0.0-canary.bc9ae6c9c.0 + '@material/elevation': 15.0.0-canary.bc9ae6c9c.0 + '@material/feature-targeting': 15.0.0-canary.bc9ae6c9c.0 + '@material/focus-ring': 15.0.0-canary.bc9ae6c9c.0 + '@material/ripple': 15.0.0-canary.bc9ae6c9c.0 + '@material/rtl': 15.0.0-canary.bc9ae6c9c.0 + '@material/shape': 15.0.0-canary.bc9ae6c9c.0 + '@material/theme': 15.0.0-canary.bc9ae6c9c.0 + '@material/tokens': 15.0.0-canary.bc9ae6c9c.0 + '@material/touch-target': 15.0.0-canary.bc9ae6c9c.0 + '@material/typography': 15.0.0-canary.bc9ae6c9c.0 + tslib: 2.8.1 + + '@material/feature-targeting@15.0.0-canary.bc9ae6c9c.0': + dependencies: + tslib: 2.8.1 + + '@material/floating-label@15.0.0-canary.bc9ae6c9c.0': + dependencies: + '@material/animation': 15.0.0-canary.bc9ae6c9c.0 + '@material/base': 15.0.0-canary.bc9ae6c9c.0 + '@material/dom': 15.0.0-canary.bc9ae6c9c.0 + '@material/feature-targeting': 15.0.0-canary.bc9ae6c9c.0 + '@material/rtl': 15.0.0-canary.bc9ae6c9c.0 + '@material/theme': 15.0.0-canary.bc9ae6c9c.0 + '@material/typography': 15.0.0-canary.bc9ae6c9c.0 + tslib: 2.8.1 + + '@material/focus-ring@15.0.0-canary.bc9ae6c9c.0': + dependencies: + '@material/dom': 15.0.0-canary.bc9ae6c9c.0 + '@material/feature-targeting': 15.0.0-canary.bc9ae6c9c.0 + '@material/rtl': 15.0.0-canary.bc9ae6c9c.0 + + '@material/form-field@15.0.0-canary.bc9ae6c9c.0': + dependencies: + '@material/base': 15.0.0-canary.bc9ae6c9c.0 + '@material/feature-targeting': 15.0.0-canary.bc9ae6c9c.0 + '@material/ripple': 15.0.0-canary.bc9ae6c9c.0 + '@material/rtl': 15.0.0-canary.bc9ae6c9c.0 + '@material/theme': 15.0.0-canary.bc9ae6c9c.0 + '@material/typography': 15.0.0-canary.bc9ae6c9c.0 + tslib: 2.8.1 + + '@material/icon-button@15.0.0-canary.bc9ae6c9c.0': + dependencies: + '@material/base': 15.0.0-canary.bc9ae6c9c.0 + '@material/density': 15.0.0-canary.bc9ae6c9c.0 + '@material/dom': 15.0.0-canary.bc9ae6c9c.0 + '@material/elevation': 15.0.0-canary.bc9ae6c9c.0 + '@material/feature-targeting': 15.0.0-canary.bc9ae6c9c.0 + '@material/focus-ring': 15.0.0-canary.bc9ae6c9c.0 + '@material/ripple': 15.0.0-canary.bc9ae6c9c.0 + '@material/rtl': 15.0.0-canary.bc9ae6c9c.0 + '@material/theme': 15.0.0-canary.bc9ae6c9c.0 + '@material/touch-target': 15.0.0-canary.bc9ae6c9c.0 + tslib: 2.8.1 + + '@material/image-list@15.0.0-canary.bc9ae6c9c.0': + dependencies: + '@material/feature-targeting': 15.0.0-canary.bc9ae6c9c.0 + '@material/shape': 15.0.0-canary.bc9ae6c9c.0 + '@material/theme': 15.0.0-canary.bc9ae6c9c.0 + '@material/typography': 15.0.0-canary.bc9ae6c9c.0 + tslib: 2.8.1 + + '@material/layout-grid@15.0.0-canary.bc9ae6c9c.0': + dependencies: + tslib: 2.8.1 + + '@material/line-ripple@15.0.0-canary.bc9ae6c9c.0': + dependencies: + '@material/animation': 15.0.0-canary.bc9ae6c9c.0 + '@material/base': 15.0.0-canary.bc9ae6c9c.0 + '@material/feature-targeting': 15.0.0-canary.bc9ae6c9c.0 + '@material/theme': 15.0.0-canary.bc9ae6c9c.0 + tslib: 2.8.1 + + '@material/linear-progress@15.0.0-canary.bc9ae6c9c.0': + dependencies: + '@material/animation': 15.0.0-canary.bc9ae6c9c.0 + '@material/base': 15.0.0-canary.bc9ae6c9c.0 + '@material/dom': 15.0.0-canary.bc9ae6c9c.0 + '@material/feature-targeting': 15.0.0-canary.bc9ae6c9c.0 + '@material/progress-indicator': 15.0.0-canary.bc9ae6c9c.0 + '@material/rtl': 15.0.0-canary.bc9ae6c9c.0 + '@material/theme': 15.0.0-canary.bc9ae6c9c.0 + tslib: 2.8.1 + + '@material/list@15.0.0-canary.bc9ae6c9c.0': + dependencies: + '@material/base': 15.0.0-canary.bc9ae6c9c.0 + '@material/density': 15.0.0-canary.bc9ae6c9c.0 + '@material/dom': 15.0.0-canary.bc9ae6c9c.0 + '@material/feature-targeting': 15.0.0-canary.bc9ae6c9c.0 + '@material/ripple': 15.0.0-canary.bc9ae6c9c.0 + '@material/rtl': 15.0.0-canary.bc9ae6c9c.0 + '@material/shape': 15.0.0-canary.bc9ae6c9c.0 + '@material/theme': 15.0.0-canary.bc9ae6c9c.0 + '@material/tokens': 15.0.0-canary.bc9ae6c9c.0 + '@material/typography': 15.0.0-canary.bc9ae6c9c.0 + tslib: 2.8.1 + + '@material/menu-surface@15.0.0-canary.bc9ae6c9c.0': + dependencies: + '@material/animation': 15.0.0-canary.bc9ae6c9c.0 + '@material/base': 15.0.0-canary.bc9ae6c9c.0 + '@material/elevation': 15.0.0-canary.bc9ae6c9c.0 + '@material/feature-targeting': 15.0.0-canary.bc9ae6c9c.0 + '@material/rtl': 15.0.0-canary.bc9ae6c9c.0 + '@material/shape': 15.0.0-canary.bc9ae6c9c.0 + '@material/theme': 15.0.0-canary.bc9ae6c9c.0 + tslib: 2.8.1 + + '@material/menu@15.0.0-canary.bc9ae6c9c.0': + dependencies: + '@material/base': 15.0.0-canary.bc9ae6c9c.0 + '@material/dom': 15.0.0-canary.bc9ae6c9c.0 + '@material/elevation': 15.0.0-canary.bc9ae6c9c.0 + '@material/feature-targeting': 15.0.0-canary.bc9ae6c9c.0 + '@material/list': 15.0.0-canary.bc9ae6c9c.0 + '@material/menu-surface': 15.0.0-canary.bc9ae6c9c.0 + '@material/ripple': 15.0.0-canary.bc9ae6c9c.0 + '@material/rtl': 15.0.0-canary.bc9ae6c9c.0 + '@material/shape': 15.0.0-canary.bc9ae6c9c.0 + '@material/theme': 15.0.0-canary.bc9ae6c9c.0 + '@material/tokens': 15.0.0-canary.bc9ae6c9c.0 + tslib: 2.8.1 + + '@material/notched-outline@15.0.0-canary.bc9ae6c9c.0': + dependencies: + '@material/base': 15.0.0-canary.bc9ae6c9c.0 + '@material/feature-targeting': 15.0.0-canary.bc9ae6c9c.0 + '@material/floating-label': 15.0.0-canary.bc9ae6c9c.0 + '@material/rtl': 15.0.0-canary.bc9ae6c9c.0 + '@material/shape': 15.0.0-canary.bc9ae6c9c.0 + '@material/theme': 15.0.0-canary.bc9ae6c9c.0 + tslib: 2.8.1 + + '@material/progress-indicator@15.0.0-canary.bc9ae6c9c.0': + dependencies: + tslib: 2.8.1 + + '@material/radio@15.0.0-canary.bc9ae6c9c.0': + dependencies: + '@material/animation': 15.0.0-canary.bc9ae6c9c.0 + '@material/base': 15.0.0-canary.bc9ae6c9c.0 + '@material/density': 15.0.0-canary.bc9ae6c9c.0 + '@material/dom': 15.0.0-canary.bc9ae6c9c.0 + '@material/feature-targeting': 15.0.0-canary.bc9ae6c9c.0 + '@material/focus-ring': 15.0.0-canary.bc9ae6c9c.0 + '@material/ripple': 15.0.0-canary.bc9ae6c9c.0 + '@material/theme': 15.0.0-canary.bc9ae6c9c.0 + '@material/touch-target': 15.0.0-canary.bc9ae6c9c.0 + tslib: 2.8.1 + + '@material/ripple@15.0.0-canary.bc9ae6c9c.0': + dependencies: + '@material/animation': 15.0.0-canary.bc9ae6c9c.0 + '@material/base': 15.0.0-canary.bc9ae6c9c.0 + '@material/dom': 15.0.0-canary.bc9ae6c9c.0 + '@material/feature-targeting': 15.0.0-canary.bc9ae6c9c.0 + '@material/rtl': 15.0.0-canary.bc9ae6c9c.0 + '@material/theme': 15.0.0-canary.bc9ae6c9c.0 + tslib: 2.8.1 + + '@material/rtl@15.0.0-canary.bc9ae6c9c.0': + dependencies: + '@material/theme': 15.0.0-canary.bc9ae6c9c.0 + tslib: 2.8.1 + + '@material/segmented-button@15.0.0-canary.bc9ae6c9c.0': + dependencies: + '@material/base': 15.0.0-canary.bc9ae6c9c.0 + '@material/elevation': 15.0.0-canary.bc9ae6c9c.0 + '@material/feature-targeting': 15.0.0-canary.bc9ae6c9c.0 + '@material/ripple': 15.0.0-canary.bc9ae6c9c.0 + '@material/theme': 15.0.0-canary.bc9ae6c9c.0 + '@material/touch-target': 15.0.0-canary.bc9ae6c9c.0 + '@material/typography': 15.0.0-canary.bc9ae6c9c.0 + tslib: 2.8.1 + + '@material/select@15.0.0-canary.bc9ae6c9c.0': + dependencies: + '@material/animation': 15.0.0-canary.bc9ae6c9c.0 + '@material/base': 15.0.0-canary.bc9ae6c9c.0 + '@material/density': 15.0.0-canary.bc9ae6c9c.0 + '@material/dom': 15.0.0-canary.bc9ae6c9c.0 + '@material/elevation': 15.0.0-canary.bc9ae6c9c.0 + '@material/feature-targeting': 15.0.0-canary.bc9ae6c9c.0 + '@material/floating-label': 15.0.0-canary.bc9ae6c9c.0 + '@material/line-ripple': 15.0.0-canary.bc9ae6c9c.0 + '@material/list': 15.0.0-canary.bc9ae6c9c.0 + '@material/menu': 15.0.0-canary.bc9ae6c9c.0 + '@material/menu-surface': 15.0.0-canary.bc9ae6c9c.0 + '@material/notched-outline': 15.0.0-canary.bc9ae6c9c.0 + '@material/ripple': 15.0.0-canary.bc9ae6c9c.0 + '@material/rtl': 15.0.0-canary.bc9ae6c9c.0 + '@material/shape': 15.0.0-canary.bc9ae6c9c.0 + '@material/theme': 15.0.0-canary.bc9ae6c9c.0 + '@material/tokens': 15.0.0-canary.bc9ae6c9c.0 + '@material/typography': 15.0.0-canary.bc9ae6c9c.0 + tslib: 2.8.1 + + '@material/shape@15.0.0-canary.bc9ae6c9c.0': + dependencies: + '@material/feature-targeting': 15.0.0-canary.bc9ae6c9c.0 + '@material/rtl': 15.0.0-canary.bc9ae6c9c.0 + '@material/theme': 15.0.0-canary.bc9ae6c9c.0 + tslib: 2.8.1 + + '@material/slider@15.0.0-canary.bc9ae6c9c.0': + dependencies: + '@material/animation': 15.0.0-canary.bc9ae6c9c.0 + '@material/base': 15.0.0-canary.bc9ae6c9c.0 + '@material/dom': 15.0.0-canary.bc9ae6c9c.0 + '@material/elevation': 15.0.0-canary.bc9ae6c9c.0 + '@material/feature-targeting': 15.0.0-canary.bc9ae6c9c.0 + '@material/ripple': 15.0.0-canary.bc9ae6c9c.0 + '@material/rtl': 15.0.0-canary.bc9ae6c9c.0 + '@material/theme': 15.0.0-canary.bc9ae6c9c.0 + '@material/tokens': 15.0.0-canary.bc9ae6c9c.0 + '@material/typography': 15.0.0-canary.bc9ae6c9c.0 + tslib: 2.8.1 + + '@material/snackbar@15.0.0-canary.bc9ae6c9c.0': + dependencies: + '@material/animation': 15.0.0-canary.bc9ae6c9c.0 + '@material/base': 15.0.0-canary.bc9ae6c9c.0 + '@material/button': 15.0.0-canary.bc9ae6c9c.0 + '@material/dom': 15.0.0-canary.bc9ae6c9c.0 + '@material/elevation': 15.0.0-canary.bc9ae6c9c.0 + '@material/feature-targeting': 15.0.0-canary.bc9ae6c9c.0 + '@material/icon-button': 15.0.0-canary.bc9ae6c9c.0 + '@material/ripple': 15.0.0-canary.bc9ae6c9c.0 + '@material/rtl': 15.0.0-canary.bc9ae6c9c.0 + '@material/shape': 15.0.0-canary.bc9ae6c9c.0 + '@material/theme': 15.0.0-canary.bc9ae6c9c.0 + '@material/tokens': 15.0.0-canary.bc9ae6c9c.0 + '@material/typography': 15.0.0-canary.bc9ae6c9c.0 + tslib: 2.8.1 + + '@material/switch@15.0.0-canary.bc9ae6c9c.0': + dependencies: + '@material/animation': 15.0.0-canary.bc9ae6c9c.0 + '@material/base': 15.0.0-canary.bc9ae6c9c.0 + '@material/density': 15.0.0-canary.bc9ae6c9c.0 + '@material/dom': 15.0.0-canary.bc9ae6c9c.0 + '@material/elevation': 15.0.0-canary.bc9ae6c9c.0 + '@material/feature-targeting': 15.0.0-canary.bc9ae6c9c.0 + '@material/focus-ring': 15.0.0-canary.bc9ae6c9c.0 + '@material/ripple': 15.0.0-canary.bc9ae6c9c.0 + '@material/rtl': 15.0.0-canary.bc9ae6c9c.0 + '@material/shape': 15.0.0-canary.bc9ae6c9c.0 + '@material/theme': 15.0.0-canary.bc9ae6c9c.0 + '@material/tokens': 15.0.0-canary.bc9ae6c9c.0 + safevalues: 0.3.4 + tslib: 2.8.1 + + '@material/tab-bar@15.0.0-canary.bc9ae6c9c.0': + dependencies: + '@material/animation': 15.0.0-canary.bc9ae6c9c.0 + '@material/base': 15.0.0-canary.bc9ae6c9c.0 + '@material/density': 15.0.0-canary.bc9ae6c9c.0 + '@material/elevation': 15.0.0-canary.bc9ae6c9c.0 + '@material/feature-targeting': 15.0.0-canary.bc9ae6c9c.0 + '@material/tab': 15.0.0-canary.bc9ae6c9c.0 + '@material/tab-indicator': 15.0.0-canary.bc9ae6c9c.0 + '@material/tab-scroller': 15.0.0-canary.bc9ae6c9c.0 + '@material/theme': 15.0.0-canary.bc9ae6c9c.0 + '@material/tokens': 15.0.0-canary.bc9ae6c9c.0 + '@material/typography': 15.0.0-canary.bc9ae6c9c.0 + tslib: 2.8.1 + + '@material/tab-indicator@15.0.0-canary.bc9ae6c9c.0': + dependencies: + '@material/animation': 15.0.0-canary.bc9ae6c9c.0 + '@material/base': 15.0.0-canary.bc9ae6c9c.0 + '@material/feature-targeting': 15.0.0-canary.bc9ae6c9c.0 + '@material/theme': 15.0.0-canary.bc9ae6c9c.0 + tslib: 2.8.1 + + '@material/tab-scroller@15.0.0-canary.bc9ae6c9c.0': + dependencies: + '@material/animation': 15.0.0-canary.bc9ae6c9c.0 + '@material/base': 15.0.0-canary.bc9ae6c9c.0 + '@material/dom': 15.0.0-canary.bc9ae6c9c.0 + '@material/feature-targeting': 15.0.0-canary.bc9ae6c9c.0 + '@material/tab': 15.0.0-canary.bc9ae6c9c.0 + tslib: 2.8.1 + + '@material/tab@15.0.0-canary.bc9ae6c9c.0': + dependencies: + '@material/base': 15.0.0-canary.bc9ae6c9c.0 + '@material/elevation': 15.0.0-canary.bc9ae6c9c.0 + '@material/feature-targeting': 15.0.0-canary.bc9ae6c9c.0 + '@material/focus-ring': 15.0.0-canary.bc9ae6c9c.0 + '@material/ripple': 15.0.0-canary.bc9ae6c9c.0 + '@material/rtl': 15.0.0-canary.bc9ae6c9c.0 + '@material/tab-indicator': 15.0.0-canary.bc9ae6c9c.0 + '@material/theme': 15.0.0-canary.bc9ae6c9c.0 + '@material/tokens': 15.0.0-canary.bc9ae6c9c.0 + '@material/typography': 15.0.0-canary.bc9ae6c9c.0 + tslib: 2.8.1 + + '@material/textfield@15.0.0-canary.bc9ae6c9c.0': + dependencies: + '@material/animation': 15.0.0-canary.bc9ae6c9c.0 + '@material/base': 15.0.0-canary.bc9ae6c9c.0 + '@material/density': 15.0.0-canary.bc9ae6c9c.0 + '@material/dom': 15.0.0-canary.bc9ae6c9c.0 + '@material/feature-targeting': 15.0.0-canary.bc9ae6c9c.0 + '@material/floating-label': 15.0.0-canary.bc9ae6c9c.0 + '@material/line-ripple': 15.0.0-canary.bc9ae6c9c.0 + '@material/notched-outline': 15.0.0-canary.bc9ae6c9c.0 + '@material/ripple': 15.0.0-canary.bc9ae6c9c.0 + '@material/rtl': 15.0.0-canary.bc9ae6c9c.0 + '@material/shape': 15.0.0-canary.bc9ae6c9c.0 + '@material/theme': 15.0.0-canary.bc9ae6c9c.0 + '@material/tokens': 15.0.0-canary.bc9ae6c9c.0 + '@material/typography': 15.0.0-canary.bc9ae6c9c.0 + tslib: 2.8.1 + + '@material/theme@15.0.0-canary.bc9ae6c9c.0': + dependencies: + '@material/feature-targeting': 15.0.0-canary.bc9ae6c9c.0 + tslib: 2.8.1 + + '@material/tokens@15.0.0-canary.bc9ae6c9c.0': + dependencies: + '@material/elevation': 15.0.0-canary.bc9ae6c9c.0 + + '@material/tooltip@15.0.0-canary.bc9ae6c9c.0': + dependencies: + '@material/animation': 15.0.0-canary.bc9ae6c9c.0 + '@material/base': 15.0.0-canary.bc9ae6c9c.0 + '@material/button': 15.0.0-canary.bc9ae6c9c.0 + '@material/dom': 15.0.0-canary.bc9ae6c9c.0 + '@material/elevation': 15.0.0-canary.bc9ae6c9c.0 + '@material/feature-targeting': 15.0.0-canary.bc9ae6c9c.0 + '@material/rtl': 15.0.0-canary.bc9ae6c9c.0 + '@material/shape': 15.0.0-canary.bc9ae6c9c.0 + '@material/theme': 15.0.0-canary.bc9ae6c9c.0 + '@material/tokens': 15.0.0-canary.bc9ae6c9c.0 + '@material/typography': 15.0.0-canary.bc9ae6c9c.0 + safevalues: 0.3.4 + tslib: 2.8.1 + + '@material/top-app-bar@15.0.0-canary.bc9ae6c9c.0': + dependencies: + '@material/animation': 15.0.0-canary.bc9ae6c9c.0 + '@material/base': 15.0.0-canary.bc9ae6c9c.0 + '@material/elevation': 15.0.0-canary.bc9ae6c9c.0 + '@material/ripple': 15.0.0-canary.bc9ae6c9c.0 + '@material/rtl': 15.0.0-canary.bc9ae6c9c.0 + '@material/shape': 15.0.0-canary.bc9ae6c9c.0 + '@material/theme': 15.0.0-canary.bc9ae6c9c.0 + '@material/typography': 15.0.0-canary.bc9ae6c9c.0 + tslib: 2.8.1 + + '@material/touch-target@15.0.0-canary.bc9ae6c9c.0': + dependencies: + '@material/base': 15.0.0-canary.bc9ae6c9c.0 + '@material/feature-targeting': 15.0.0-canary.bc9ae6c9c.0 + '@material/rtl': 15.0.0-canary.bc9ae6c9c.0 + '@material/theme': 15.0.0-canary.bc9ae6c9c.0 + tslib: 2.8.1 + + '@material/typography@15.0.0-canary.bc9ae6c9c.0': + dependencies: + '@material/feature-targeting': 15.0.0-canary.bc9ae6c9c.0 + '@material/theme': 15.0.0-canary.bc9ae6c9c.0 + tslib: 2.8.1 + + '@mdx-js/mdx@3.1.0(acorn@8.15.0)': + dependencies: + '@types/estree': 1.0.8 + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdx': 2.0.13 + collapse-white-space: 2.1.0 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + estree-util-scope: 1.0.0 + estree-walker: 3.0.3 + hast-util-to-jsx-runtime: 2.3.6 + markdown-extensions: 2.0.0 + recma-build-jsx: 1.0.0 + recma-jsx: 1.0.0(acorn@8.15.0) + recma-stringify: 1.0.0 + rehype-recma: 1.0.0 + remark-mdx: 3.1.0 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + source-map: 0.7.4 + unified: 11.0.5 + unist-util-position-from-estree: 2.0.0 + unist-util-stringify-position: 4.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + transitivePeerDependencies: + - acorn + - supports-color + + '@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1)': + dependencies: + '@types/mdx': 2.0.13 + '@types/react': 19.1.2 + react: 18.3.1 + + '@mermaid-js/parser@0.6.2': + dependencies: + langium: 3.3.1 + + '@module-federation/error-codes@0.17.0': {} + + '@module-federation/runtime-core@0.17.0': + dependencies: + '@module-federation/error-codes': 0.17.0 + '@module-federation/sdk': 0.17.0 + + '@module-federation/runtime-tools@0.17.0': + dependencies: + '@module-federation/runtime': 0.17.0 + '@module-federation/webpack-bundler-runtime': 0.17.0 + + '@module-federation/runtime@0.17.0': + dependencies: + '@module-federation/error-codes': 0.17.0 + '@module-federation/runtime-core': 0.17.0 + '@module-federation/sdk': 0.17.0 + + '@module-federation/sdk@0.17.0': {} + + '@module-federation/webpack-bundler-runtime@0.17.0': + dependencies: + '@module-federation/runtime': 0.17.0 + '@module-federation/sdk': 0.17.0 + + '@napi-rs/wasm-runtime@0.2.12': + dependencies: + '@emnapi/core': 1.4.5 + '@emnapi/runtime': 1.4.5 + '@tybys/wasm-util': 0.10.0 + optional: true + + '@napi-rs/wasm-runtime@1.0.1': + dependencies: + '@emnapi/core': 1.4.5 + '@emnapi/runtime': 1.4.5 + '@tybys/wasm-util': 0.10.0 + optional: true + + '@netlify/framework-info@9.9.3': + dependencies: + ajv: 8.17.1 + filter-obj: 5.1.0 + find-up: 6.3.0 + is-plain-obj: 4.1.0 + locate-path: 7.2.0 + p-filter: 4.1.0 + p-locate: 6.0.0 + read-pkg-up: 9.1.0 + semver: 7.7.2 + + '@next/env@15.4.0-canary.86': {} + + '@next/eslint-plugin-next@15.4.0-canary.86': + dependencies: + fast-glob: 3.3.1 + + '@next/swc-darwin-arm64@15.4.0-canary.86': + optional: true + + '@next/swc-darwin-x64@15.4.0-canary.86': + optional: true + + '@next/swc-linux-arm64-gnu@15.4.0-canary.86': + optional: true + + '@next/swc-linux-arm64-musl@15.4.0-canary.86': + optional: true + + '@next/swc-linux-x64-gnu@15.4.0-canary.86': + optional: true + + '@next/swc-linux-x64-musl@15.4.0-canary.86': + optional: true + + '@next/swc-win32-arm64-msvc@15.4.0-canary.86': + optional: true + + '@next/swc-win32-x64-msvc@15.4.0-canary.86': + optional: true + + '@ngtools/webpack@16.2.16(@angular/compiler-cli@16.2.12(@angular/compiler@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(typescript@5.1.6))(typescript@5.1.6)(webpack@5.94.0(@swc/core@1.13.1)(esbuild@0.18.17))': + dependencies: + '@angular/compiler-cli': 16.2.12(@angular/compiler@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)))(typescript@5.1.6) + typescript: 5.1.6 + webpack: 5.94.0(@swc/core@1.13.1)(esbuild@0.18.17) + + '@ngx-translate/core@15.0.0(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2)': + dependencies: + '@angular/common': 16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2) + '@angular/core': 16.2.12(rxjs@7.8.2)(zone.js@0.13.3) + rxjs: 7.8.2 + + '@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1': + dependencies: + eslint-scope: 5.1.1 + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.19.1 + + '@nolyfill/is-core-module@1.0.39': {} + + '@npmcli/fs@2.1.2': + dependencies: + '@gar/promisify': 1.1.3 + semver: 7.7.2 + + '@npmcli/fs@3.1.1': + dependencies: + semver: 7.7.2 + + '@npmcli/git@4.1.0': + dependencies: + '@npmcli/promise-spawn': 6.0.2 + lru-cache: 7.18.3 + npm-pick-manifest: 8.0.1 + proc-log: 3.0.0 + promise-inflight: 1.0.1 + promise-retry: 2.0.1 + semver: 7.7.2 + which: 3.0.1 + transitivePeerDependencies: + - bluebird + + '@npmcli/installed-package-contents@2.1.0': + dependencies: + npm-bundled: 3.0.1 + npm-normalize-package-bin: 3.0.1 + + '@npmcli/move-file@2.0.1': + dependencies: + mkdirp: 1.0.4 + rimraf: 3.0.2 + + '@npmcli/node-gyp@3.0.0': {} + + '@npmcli/promise-spawn@6.0.2': + dependencies: + which: 3.0.1 + + '@npmcli/run-script@6.0.2': + dependencies: + '@npmcli/node-gyp': 3.0.0 + '@npmcli/promise-spawn': 6.0.2 + node-gyp: 9.4.1 + read-package-json-fast: 3.0.2 + which: 3.0.1 + transitivePeerDependencies: + - bluebird + - supports-color + + '@nrwl/devkit@16.5.1(nx@16.5.1(@swc/core@1.13.1))': + dependencies: + '@nx/devkit': 16.5.1(nx@16.5.1(@swc/core@1.13.1)) + transitivePeerDependencies: + - nx + + '@nrwl/tao@16.5.1(@swc/core@1.13.1)': + dependencies: + nx: 16.5.1(@swc/core@1.13.1) + transitivePeerDependencies: + - '@swc-node/register' + - '@swc/core' + - debug + + '@nx/devkit@16.5.1(nx@16.5.1(@swc/core@1.13.1))': + dependencies: + '@nrwl/devkit': 16.5.1(nx@16.5.1(@swc/core@1.13.1)) + ejs: 3.1.10 + ignore: 5.2.4 + nx: 16.5.1(@swc/core@1.13.1) + semver: 7.5.3 + tmp: 0.2.1 + tslib: 2.8.1 + + '@nx/nx-darwin-arm64@16.5.1': + optional: true + + '@nx/nx-darwin-x64@16.5.1': + optional: true + + '@nx/nx-freebsd-x64@16.5.1': + optional: true + + '@nx/nx-linux-arm-gnueabihf@16.5.1': + optional: true + + '@nx/nx-linux-arm64-gnu@16.5.1': + optional: true + + '@nx/nx-linux-arm64-musl@16.5.1': + optional: true + + '@nx/nx-linux-x64-gnu@16.5.1': + optional: true + + '@nx/nx-linux-x64-musl@16.5.1': + optional: true + + '@nx/nx-win32-arm64-msvc@16.5.1': + optional: true + + '@nx/nx-win32-x64-msvc@16.5.1': + optional: true + + '@otplib/core@12.0.1': {} + + '@otplib/plugin-crypto@12.0.1': + dependencies: + '@otplib/core': 12.0.1 + + '@otplib/plugin-thirty-two@12.0.1': + dependencies: + '@otplib/core': 12.0.1 + thirty-two: 1.0.2 + + '@oxc-resolver/binding-android-arm-eabi@11.6.0': + optional: true + + '@oxc-resolver/binding-android-arm64@11.6.0': + optional: true + + '@oxc-resolver/binding-darwin-arm64@11.6.0': + optional: true + + '@oxc-resolver/binding-darwin-x64@11.6.0': + optional: true + + '@oxc-resolver/binding-freebsd-x64@11.6.0': + optional: true + + '@oxc-resolver/binding-linux-arm-gnueabihf@11.6.0': + optional: true + + '@oxc-resolver/binding-linux-arm-musleabihf@11.6.0': + optional: true + + '@oxc-resolver/binding-linux-arm64-gnu@11.6.0': + optional: true + + '@oxc-resolver/binding-linux-arm64-musl@11.6.0': + optional: true + + '@oxc-resolver/binding-linux-ppc64-gnu@11.6.0': + optional: true + + '@oxc-resolver/binding-linux-riscv64-gnu@11.6.0': + optional: true + + '@oxc-resolver/binding-linux-riscv64-musl@11.6.0': + optional: true + + '@oxc-resolver/binding-linux-s390x-gnu@11.6.0': + optional: true + + '@oxc-resolver/binding-linux-x64-gnu@11.6.0': + optional: true + + '@oxc-resolver/binding-linux-x64-musl@11.6.0': + optional: true + + '@oxc-resolver/binding-wasm32-wasi@11.6.0': + dependencies: + '@napi-rs/wasm-runtime': 1.0.1 + optional: true + + '@oxc-resolver/binding-win32-arm64-msvc@11.6.0': + optional: true + + '@oxc-resolver/binding-win32-ia32-msvc@11.6.0': + optional: true + + '@oxc-resolver/binding-win32-x64-msvc@11.6.0': + optional: true + + '@parcel/watcher-android-arm64@2.5.1': + optional: true + + '@parcel/watcher-darwin-arm64@2.5.1': + optional: true + + '@parcel/watcher-darwin-x64@2.5.1': + optional: true + + '@parcel/watcher-freebsd-x64@2.5.1': + optional: true + + '@parcel/watcher-linux-arm-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-arm-musl@2.5.1': + optional: true + + '@parcel/watcher-linux-arm64-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-arm64-musl@2.5.1': + optional: true + + '@parcel/watcher-linux-x64-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-x64-musl@2.5.1': + optional: true + + '@parcel/watcher-win32-arm64@2.5.1': + optional: true + + '@parcel/watcher-win32-ia32@2.5.1': + optional: true + + '@parcel/watcher-win32-x64@2.5.1': + optional: true + + '@parcel/watcher@2.0.4': + dependencies: + node-addon-api: 3.2.1 + node-gyp-build: 4.8.4 + + '@parcel/watcher@2.5.1': + dependencies: + detect-libc: 1.0.3 + is-glob: 4.0.3 + micromatch: 4.0.8 + node-addon-api: 7.1.1 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.5.1 + '@parcel/watcher-darwin-arm64': 2.5.1 + '@parcel/watcher-darwin-x64': 2.5.1 + '@parcel/watcher-freebsd-x64': 2.5.1 + '@parcel/watcher-linux-arm-glibc': 2.5.1 + '@parcel/watcher-linux-arm-musl': 2.5.1 + '@parcel/watcher-linux-arm64-glibc': 2.5.1 + '@parcel/watcher-linux-arm64-musl': 2.5.1 + '@parcel/watcher-linux-x64-glibc': 2.5.1 + '@parcel/watcher-linux-x64-musl': 2.5.1 + '@parcel/watcher-win32-arm64': 2.5.1 + '@parcel/watcher-win32-ia32': 2.5.1 + '@parcel/watcher-win32-x64': 2.5.1 + optional: true + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@playwright/test@1.54.1': + dependencies: + playwright: 1.54.1 + + '@pnpm/config.env-replace@1.1.0': {} + + '@pnpm/network.ca-file@1.0.2': + dependencies: + graceful-fs: 4.2.10 + + '@pnpm/npm-conf@2.3.1': + dependencies: + '@pnpm/config.env-replace': 1.1.0 + '@pnpm/network.ca-file': 1.0.2 + config-chain: 1.1.13 + + '@polka/url@1.0.0-next.29': {} + + '@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': + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/inquire': 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': {} + + '@puppeteer/browsers@2.3.0': + dependencies: + debug: 4.4.1(supports-color@5.5.0) + extract-zip: 2.0.1 + progress: 2.0.3 + proxy-agent: 6.5.0 + semver: 7.7.2 + tar-fs: 3.1.0 + unbzip2-stream: 1.4.3 + yargs: 17.7.2 + transitivePeerDependencies: + - bare-buffer + - supports-color + optional: true + + '@radix-ui/number@1.1.0': {} + + '@radix-ui/number@1.1.1': {} + + '@radix-ui/primitive@1.1.1': {} + + '@radix-ui/primitive@1.1.2': {} + + '@radix-ui/react-arrow@1.1.1(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 19.1.2 + '@types/react-dom': 19.1.2(@types/react@19.1.2) + + '@radix-ui/react-arrow@1.1.2(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 19.1.2 + '@types/react-dom': 19.1.2(@types/react@19.1.2) + + '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 19.1.2 + '@types/react-dom': 19.1.2(@types/react@19.1.2) + + '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.2 + '@types/react-dom': 19.1.2(@types/react@19.1.2) + + '@radix-ui/react-avatar@1.1.2(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-context': 1.1.1(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.1.2)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 19.1.2 + '@types/react-dom': 19.1.2(@types/react@19.1.2) + + '@radix-ui/react-checkbox@1.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/react-presence': 1.1.2(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.0(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.0(@types/react@19.1.2)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 19.1.2 + '@types/react-dom': 19.1.2(@types/react@19.1.2) + + '@radix-ui/react-collection@1.1.7(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@19.1.2)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 19.1.2 + '@types/react-dom': 19.1.2(@types/react@19.1.2) + + '@radix-ui/react-compose-refs@1.1.1(@types/react@19.1.2)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 19.1.2 + + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.1.2)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 19.1.2 + + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.1.2)(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.2 + + '@radix-ui/react-context@1.1.1(@types/react@19.1.2)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 19.1.2 + + '@radix-ui/react-context@1.1.2(@types/react@19.1.2)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 19.1.2 + + '@radix-ui/react-context@1.1.2(@types/react@19.1.2)(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.2 + + '@radix-ui/react-direction@1.1.0(@types/react@19.1.2)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 19.1.2 + + '@radix-ui/react-direction@1.1.1(@types/react@19.1.2)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 19.1.2 + + '@radix-ui/react-dismissable-layer@1.1.10(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.1.2)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 19.1.2 + '@types/react-dom': 19.1.2(@types/react@19.1.2) + + '@radix-ui/react-dismissable-layer@1.1.10(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.2)(react@19.1.0) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.1.2)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.2 + '@types/react-dom': 19.1.2(@types/react@19.1.2) + + '@radix-ui/react-dismissable-layer@1.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/react-use-escape-keydown': 1.1.0(@types/react@19.1.2)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 19.1.2 + '@types/react-dom': 19.1.2(@types/react@19.1.2) + + '@radix-ui/react-dismissable-layer@1.1.5(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/react-use-escape-keydown': 1.1.0(@types/react@19.1.2)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 19.1.2 + '@types/react-dom': 19.1.2(@types/react@19.1.2) + + '@radix-ui/react-focus-guards@1.1.1(@types/react@19.1.2)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 19.1.2 + + '@radix-ui/react-focus-guards@1.1.2(@types/react@19.1.2)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 19.1.2 + + '@radix-ui/react-focus-scope@1.1.2(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.1.2)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 19.1.2 + '@types/react-dom': 19.1.2(@types/react@19.1.2) + + '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.2)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 19.1.2 + '@types/react-dom': 19.1.2(@types/react@19.1.2) + + '@radix-ui/react-hover-card@1.1.14(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-popper': 1.2.7(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.2)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 19.1.2 + '@types/react-dom': 19.1.2(@types/react@19.1.2) + + '@radix-ui/react-id@1.1.0(@types/react@19.1.2)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.1.2)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 19.1.2 + + '@radix-ui/react-id@1.1.1(@types/react@19.1.2)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.2)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 19.1.2 + + '@radix-ui/react-id@1.1.1(@types/react@19.1.2)(react@19.1.0)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.2)(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.2 + + '@radix-ui/react-popover@1.1.6(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.5(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.1(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.2(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/react-popper': 1.2.2(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.4(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.2(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.1.2(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@19.1.2)(react@18.3.1) + aria-hidden: 1.2.6 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.7.1(@types/react@19.1.2)(react@18.3.1) + optionalDependencies: + '@types/react': 19.1.2 + '@types/react-dom': 19.1.2(@types/react@19.1.2) + + '@radix-ui/react-popper@1.2.1(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/react-dom': 2.1.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-arrow': 1.1.1(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/react-use-rect': 1.1.0(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.0(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/rect': 1.1.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 19.1.2 + '@types/react-dom': 19.1.2(@types/react@19.1.2) + + '@radix-ui/react-popper@1.2.2(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/react-dom': 2.1.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-arrow': 1.1.2(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/react-use-rect': 1.1.0(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.0(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/rect': 1.1.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 19.1.2 + '@types/react-dom': 19.1.2(@types/react@19.1.2) + + '@radix-ui/react-popper@1.2.7(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/react-dom': 2.1.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/react-use-rect': 1.1.1(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/rect': 1.1.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 19.1.2 + '@types/react-dom': 19.1.2(@types/react@19.1.2) + + '@radix-ui/react-popper@1.2.7(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@floating-ui/react-dom': 2.1.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.2)(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.2)(react@19.1.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.2)(react@19.1.0) + '@radix-ui/react-use-rect': 1.1.1(@types/react@19.1.2)(react@19.1.0) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.1.2)(react@19.1.0) + '@radix-ui/rect': 1.1.1 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.2 + '@types/react-dom': 19.1.2(@types/react@19.1.2) + + '@radix-ui/react-portal@1.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.1.2)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 19.1.2 + '@types/react-dom': 19.1.2(@types/react@19.1.2) + + '@radix-ui/react-portal@1.1.4(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.1.2)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 19.1.2 + '@types/react-dom': 19.1.2(@types/react@19.1.2) + + '@radix-ui/react-portal@1.1.9(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.2)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 19.1.2 + '@types/react-dom': 19.1.2(@types/react@19.1.2) + + '@radix-ui/react-portal@1.1.9(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.2)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.2 + '@types/react-dom': 19.1.2(@types/react@19.1.2) + + '@radix-ui/react-presence@1.1.2(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.1.2)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 19.1.2 + '@types/react-dom': 19.1.2(@types/react@19.1.2) + + '@radix-ui/react-presence@1.1.4(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.2)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 19.1.2 + '@types/react-dom': 19.1.2(@types/react@19.1.2) + + '@radix-ui/react-presence@1.1.4(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.1.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.2)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.2 + '@types/react-dom': 19.1.2(@types/react@19.1.2) + + '@radix-ui/react-primitive@2.0.1(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-slot': 1.1.1(@types/react@19.1.2)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 19.1.2 + '@types/react-dom': 19.1.2(@types/react@19.1.2) + + '@radix-ui/react-primitive@2.0.2(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-slot': 1.1.2(@types/react@19.1.2)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 19.1.2 + '@types/react-dom': 19.1.2(@types/react@19.1.2) + + '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-slot': 1.2.3(@types/react@19.1.2)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 19.1.2 + '@types/react-dom': 19.1.2(@types/react@19.1.2) + + '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-slot': 1.2.3(@types/react@19.1.2)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.2 + '@types/react-dom': 19.1.2(@types/react@19.1.2) + + '@radix-ui/react-roving-focus@1.1.10(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.2)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 19.1.2 + '@types/react-dom': 19.1.2(@types/react@19.1.2) + + '@radix-ui/react-scroll-area@1.2.2(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/number': 1.1.0 + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/react-direction': 1.1.0(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/react-presence': 1.1.2(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.1.2)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 19.1.2 + '@types/react-dom': 19.1.2(@types/react@19.1.2) + + '@radix-ui/react-slot@1.1.1(@types/react@19.1.2)(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.1.2)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 19.1.2 + + '@radix-ui/react-slot@1.1.2(@types/react@19.1.2)(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.1.2)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 19.1.2 + + '@radix-ui/react-slot@1.2.3(@types/react@19.1.2)(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 19.1.2 + + '@radix-ui/react-slot@1.2.3(@types/react@19.1.2)(react@19.1.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.2 + + '@radix-ui/react-tabs@1.1.12(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-context': 1.1.2(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.10(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.2)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 19.1.2 + '@types/react-dom': 19.1.2(@types/react@19.1.2) + + '@radix-ui/react-tooltip@1.1.6(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/react-popper': 1.2.1(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.2(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.1.1(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.1.1(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 19.1.2 + '@types/react-dom': 19.1.2(@types/react@19.1.2) + + '@radix-ui/react-tooltip@1.2.7(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.2)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.2)(react@19.1.0) + '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.2)(react@19.1.0) + '@radix-ui/react-popper': 1.2.7(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-slot': 1.2.3(@types/react@19.1.2)(react@19.1.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.2)(react@19.1.0) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.2 + '@types/react-dom': 19.1.2(@types/react@19.1.2) + + '@radix-ui/react-use-callback-ref@1.1.0(@types/react@19.1.2)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 19.1.2 + + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.1.2)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 19.1.2 + + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.1.2)(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.2 + + '@radix-ui/react-use-controllable-state@1.1.0(@types/react@19.1.2)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.1.2)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 19.1.2 + + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.1.2)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.1.2)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.2)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 19.1.2 + + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.1.2)(react@19.1.0)': + dependencies: + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.1.2)(react@19.1.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.2)(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.2 + + '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.1.2)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.2)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 19.1.2 + + '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.1.2)(react@19.1.0)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.2)(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.2 + + '@radix-ui/react-use-escape-keydown@1.1.0(@types/react@19.1.2)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.1.2)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 19.1.2 + + '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.1.2)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.2)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 19.1.2 + + '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.1.2)(react@19.1.0)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.2)(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.2 + + '@radix-ui/react-use-layout-effect@1.1.0(@types/react@19.1.2)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 19.1.2 + + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.1.2)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 19.1.2 + + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.1.2)(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.2 + + '@radix-ui/react-use-previous@1.1.0(@types/react@19.1.2)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 19.1.2 + + '@radix-ui/react-use-rect@1.1.0(@types/react@19.1.2)(react@18.3.1)': + dependencies: + '@radix-ui/rect': 1.1.0 + react: 18.3.1 + optionalDependencies: + '@types/react': 19.1.2 + + '@radix-ui/react-use-rect@1.1.1(@types/react@19.1.2)(react@18.3.1)': + dependencies: + '@radix-ui/rect': 1.1.1 + react: 18.3.1 + optionalDependencies: + '@types/react': 19.1.2 + + '@radix-ui/react-use-rect@1.1.1(@types/react@19.1.2)(react@19.1.0)': + dependencies: + '@radix-ui/rect': 1.1.1 + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.2 + + '@radix-ui/react-use-size@1.1.0(@types/react@19.1.2)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.1.2)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 19.1.2 + + '@radix-ui/react-use-size@1.1.1(@types/react@19.1.2)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.2)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 19.1.2 + + '@radix-ui/react-use-size@1.1.1(@types/react@19.1.2)(react@19.1.0)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.2)(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.2 + + '@radix-ui/react-visually-hidden@1.1.1(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 19.1.2 + '@types/react-dom': 19.1.2(@types/react@19.1.2) + + '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.2 + '@types/react-dom': 19.1.2(@types/react@19.1.2) + + '@radix-ui/rect@1.1.0': {} + + '@radix-ui/rect@1.1.1': {} + + '@react-aria/focus@3.20.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@react-aria/interactions': 3.25.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@react-aria/utils': 3.29.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@react-types/shared': 3.30.0(react@19.1.0) + '@swc/helpers': 0.5.17 + clsx: 2.1.1 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + + '@react-aria/interactions@3.25.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@react-aria/ssr': 3.9.9(react@19.1.0) + '@react-aria/utils': 3.29.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@react-stately/flags': 3.1.2 + '@react-types/shared': 3.30.0(react@19.1.0) + '@swc/helpers': 0.5.17 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + + '@react-aria/ssr@3.9.9(react@19.1.0)': + dependencies: + '@swc/helpers': 0.5.17 + react: 19.1.0 + + '@react-aria/utils@3.29.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@react-aria/ssr': 3.9.9(react@19.1.0) + '@react-stately/flags': 3.1.2 + '@react-stately/utils': 3.10.7(react@19.1.0) + '@react-types/shared': 3.30.0(react@19.1.0) + '@swc/helpers': 0.5.17 + clsx: 2.1.1 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + + '@react-stately/flags@3.1.2': + dependencies: + '@swc/helpers': 0.5.17 + + '@react-stately/utils@3.10.7(react@19.1.0)': + dependencies: + '@swc/helpers': 0.5.17 + react: 19.1.0 + + '@react-types/shared@3.30.0(react@19.1.0)': + dependencies: + react: 19.1.0 + + '@redocly/ajv@8.11.2': + dependencies: + fast-deep-equal: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + uri-js-replace: 1.0.1 + + '@redocly/config@0.22.2': {} + + '@redocly/openapi-core@1.34.3': + dependencies: + '@redocly/ajv': 8.11.2 + '@redocly/config': 0.22.2 + colorette: 1.4.0 + https-proxy-agent: 7.0.6 + js-levenshtein: 1.1.6 + js-yaml: 4.1.0 + minimatch: 5.1.6 + pluralize: 8.0.0 + yaml-ast-parser: 0.0.43 + transitivePeerDependencies: + - supports-color + + '@reduxjs/toolkit@1.9.7(react-redux@7.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)': + dependencies: + immer: 9.0.21 + redux: 4.2.1 + redux-thunk: 2.4.2(redux@4.2.1) + reselect: 4.1.8 + optionalDependencies: + react: 18.3.1 + react-redux: 7.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + + '@rolldown/pluginutils@1.0.0-beta.27': {} + + '@rollup/rollup-android-arm-eabi@4.45.1': + optional: true + + '@rollup/rollup-android-arm64@4.45.1': + optional: true + + '@rollup/rollup-darwin-arm64@4.45.1': + optional: true + + '@rollup/rollup-darwin-x64@4.45.1': + optional: true + + '@rollup/rollup-freebsd-arm64@4.45.1': + optional: true + + '@rollup/rollup-freebsd-x64@4.45.1': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.45.1': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.45.1': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.45.1': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.45.1': + optional: true + + '@rollup/rollup-linux-loongarch64-gnu@4.45.1': + optional: true + + '@rollup/rollup-linux-powerpc64le-gnu@4.45.1': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.45.1': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.45.1': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.45.1': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.45.1': + optional: true + + '@rollup/rollup-linux-x64-musl@4.45.1': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.45.1': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.45.1': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.45.1': + optional: true + + '@rspack/binding-darwin-arm64@1.4.9': + optional: true + + '@rspack/binding-darwin-x64@1.4.9': + optional: true + + '@rspack/binding-linux-arm64-gnu@1.4.9': + optional: true + + '@rspack/binding-linux-arm64-musl@1.4.9': + optional: true + + '@rspack/binding-linux-x64-gnu@1.4.9': + optional: true + + '@rspack/binding-linux-x64-musl@1.4.9': + optional: true + + '@rspack/binding-wasm32-wasi@1.4.9': + dependencies: + '@napi-rs/wasm-runtime': 1.0.1 + optional: true + + '@rspack/binding-win32-arm64-msvc@1.4.9': + optional: true + + '@rspack/binding-win32-ia32-msvc@1.4.9': + optional: true + + '@rspack/binding-win32-x64-msvc@1.4.9': + optional: true + + '@rspack/binding@1.4.9': + optionalDependencies: + '@rspack/binding-darwin-arm64': 1.4.9 + '@rspack/binding-darwin-x64': 1.4.9 + '@rspack/binding-linux-arm64-gnu': 1.4.9 + '@rspack/binding-linux-arm64-musl': 1.4.9 + '@rspack/binding-linux-x64-gnu': 1.4.9 + '@rspack/binding-linux-x64-musl': 1.4.9 + '@rspack/binding-wasm32-wasi': 1.4.9 + '@rspack/binding-win32-arm64-msvc': 1.4.9 + '@rspack/binding-win32-ia32-msvc': 1.4.9 + '@rspack/binding-win32-x64-msvc': 1.4.9 + + '@rspack/core@1.4.9(@swc/helpers@0.5.17)': + dependencies: + '@module-federation/runtime-tools': 0.17.0 + '@rspack/binding': 1.4.9 + '@rspack/lite-tapable': 1.0.1 + optionalDependencies: + '@swc/helpers': 0.5.17 + + '@rspack/lite-tapable@1.0.1': {} + + '@rtsao/scc@1.1.0': {} + + '@rushstack/eslint-patch@1.12.0': {} + + '@schematics/angular@16.2.16(chokidar@3.5.3)': + dependencies: + '@angular-devkit/core': 16.2.16(chokidar@3.5.3) + '@angular-devkit/schematics': 16.2.16(chokidar@3.5.3) + jsonc-parser: 3.2.0 + transitivePeerDependencies: + - chokidar + + '@sideway/address@4.1.5': + dependencies: + '@hapi/hoek': 9.3.0 + + '@sideway/formula@3.0.1': {} + + '@sideway/pinpoint@2.0.0': {} + + '@signalwire/docusaurus-plugin-llms-txt@1.2.1(@docusaurus/core@3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3))': + dependencies: + '@docusaurus/core': 3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3) + fs-extra: 11.3.0 + hast-util-select: 6.0.4 + hast-util-to-html: 9.0.5 + hast-util-to-string: 3.0.1 + p-map: 7.0.3 + rehype-parse: 9.0.1 + rehype-remark: 10.0.1 + remark-gfm: 4.0.1 + remark-stringify: 11.0.0 + string-width: 5.1.2 + unified: 11.0.5 + unist-util-visit: 5.0.0 + transitivePeerDependencies: + - supports-color + + '@sigstore/bundle@1.1.0': + dependencies: + '@sigstore/protobuf-specs': 0.2.1 + + '@sigstore/protobuf-specs@0.2.1': {} + + '@sigstore/sign@1.0.0': + dependencies: + '@sigstore/bundle': 1.1.0 + '@sigstore/protobuf-specs': 0.2.1 + make-fetch-happen: 11.1.1 + transitivePeerDependencies: + - supports-color + + '@sigstore/tuf@1.0.3': + dependencies: + '@sigstore/protobuf-specs': 0.2.1 + tuf-js: 1.1.7 + transitivePeerDependencies: + - supports-color + + '@sinclair/typebox@0.27.8': {} + + '@sindresorhus/is@4.6.0': {} + + '@sindresorhus/is@5.6.0': {} + + '@slorber/react-helmet-async@1.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.27.6 + invariant: 2.2.4 + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-fast-compare: 3.2.2 + shallowequal: 1.1.0 + + '@slorber/remark-comment@1.0.0': + dependencies: + micromark-factory-space: 1.1.0 + micromark-util-character: 1.2.0 + micromark-util-symbol: 1.1.0 + + '@socket.io/component-emitter@3.1.2': {} + + '@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + + '@svgr/babel-plugin-remove-jsx-attribute@8.0.0(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + + '@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + + '@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + + '@svgr/babel-plugin-svg-dynamic-title@8.0.0(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + + '@svgr/babel-plugin-svg-em-dimensions@8.0.0(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + + '@svgr/babel-plugin-transform-react-native-svg@8.1.0(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + + '@svgr/babel-plugin-transform-svg-component@8.0.0(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + + '@svgr/babel-preset@8.1.0(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@svgr/babel-plugin-add-jsx-attribute': 8.0.0(@babel/core@7.28.0) + '@svgr/babel-plugin-remove-jsx-attribute': 8.0.0(@babel/core@7.28.0) + '@svgr/babel-plugin-remove-jsx-empty-expression': 8.0.0(@babel/core@7.28.0) + '@svgr/babel-plugin-replace-jsx-attribute-value': 8.0.0(@babel/core@7.28.0) + '@svgr/babel-plugin-svg-dynamic-title': 8.0.0(@babel/core@7.28.0) + '@svgr/babel-plugin-svg-em-dimensions': 8.0.0(@babel/core@7.28.0) + '@svgr/babel-plugin-transform-react-native-svg': 8.1.0(@babel/core@7.28.0) + '@svgr/babel-plugin-transform-svg-component': 8.0.0(@babel/core@7.28.0) + + '@svgr/core@8.1.0(typescript@5.8.3)': + dependencies: + '@babel/core': 7.28.0 + '@svgr/babel-preset': 8.1.0(@babel/core@7.28.0) + camelcase: 6.3.0 + cosmiconfig: 8.3.6(typescript@5.8.3) + snake-case: 3.0.4 + transitivePeerDependencies: + - supports-color + - typescript + + '@svgr/hast-util-to-babel-ast@8.0.0': + dependencies: + '@babel/types': 7.28.1 + entities: 4.5.0 + + '@svgr/plugin-jsx@8.1.0(@svgr/core@8.1.0(typescript@5.8.3))': + dependencies: + '@babel/core': 7.28.0 + '@svgr/babel-preset': 8.1.0(@babel/core@7.28.0) + '@svgr/core': 8.1.0(typescript@5.8.3) + '@svgr/hast-util-to-babel-ast': 8.0.0 + svg-parser: 2.0.4 + transitivePeerDependencies: + - supports-color + + '@svgr/plugin-svgo@8.1.0(@svgr/core@8.1.0(typescript@5.8.3))(typescript@5.8.3)': + dependencies: + '@svgr/core': 8.1.0(typescript@5.8.3) + cosmiconfig: 8.3.6(typescript@5.8.3) + deepmerge: 4.3.1 + svgo: 3.3.2 + transitivePeerDependencies: + - typescript + + '@svgr/webpack@8.1.0(typescript@5.8.3)': + dependencies: + '@babel/core': 7.28.0 + '@babel/plugin-transform-react-constant-elements': 7.27.1(@babel/core@7.28.0) + '@babel/preset-env': 7.28.0(@babel/core@7.28.0) + '@babel/preset-react': 7.27.1(@babel/core@7.28.0) + '@babel/preset-typescript': 7.27.1(@babel/core@7.28.0) + '@svgr/core': 8.1.0(typescript@5.8.3) + '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.8.3)) + '@svgr/plugin-svgo': 8.1.0(@svgr/core@8.1.0(typescript@5.8.3))(typescript@5.8.3) + transitivePeerDependencies: + - supports-color + - typescript + + '@swc/core-darwin-arm64@1.13.1': + optional: true + + '@swc/core-darwin-x64@1.13.1': + optional: true + + '@swc/core-linux-arm-gnueabihf@1.13.1': + optional: true + + '@swc/core-linux-arm64-gnu@1.13.1': + optional: true + + '@swc/core-linux-arm64-musl@1.13.1': + optional: true + + '@swc/core-linux-x64-gnu@1.13.1': + optional: true + + '@swc/core-linux-x64-musl@1.13.1': + optional: true + + '@swc/core-win32-arm64-msvc@1.13.1': + optional: true + + '@swc/core-win32-ia32-msvc@1.13.1': + optional: true + + '@swc/core-win32-x64-msvc@1.13.1': + optional: true + + '@swc/core@1.13.1(@swc/helpers@0.5.17)': + dependencies: + '@swc/counter': 0.1.3 + '@swc/types': 0.1.23 + optionalDependencies: + '@swc/core-darwin-arm64': 1.13.1 + '@swc/core-darwin-x64': 1.13.1 + '@swc/core-linux-arm-gnueabihf': 1.13.1 + '@swc/core-linux-arm64-gnu': 1.13.1 + '@swc/core-linux-arm64-musl': 1.13.1 + '@swc/core-linux-x64-gnu': 1.13.1 + '@swc/core-linux-x64-musl': 1.13.1 + '@swc/core-win32-arm64-msvc': 1.13.1 + '@swc/core-win32-ia32-msvc': 1.13.1 + '@swc/core-win32-x64-msvc': 1.13.1 + '@swc/helpers': 0.5.17 + + '@swc/counter@0.1.3': {} + + '@swc/helpers@0.5.15': + dependencies: + tslib: 2.8.1 + + '@swc/helpers@0.5.17': + dependencies: + tslib: 2.8.1 + + '@swc/html-darwin-arm64@1.13.1': + optional: true + + '@swc/html-darwin-x64@1.13.1': + optional: true + + '@swc/html-linux-arm-gnueabihf@1.13.1': + optional: true + + '@swc/html-linux-arm64-gnu@1.13.1': + optional: true + + '@swc/html-linux-arm64-musl@1.13.1': + optional: true + + '@swc/html-linux-x64-gnu@1.13.1': + optional: true + + '@swc/html-linux-x64-musl@1.13.1': + optional: true + + '@swc/html-win32-arm64-msvc@1.13.1': + optional: true + + '@swc/html-win32-ia32-msvc@1.13.1': + optional: true + + '@swc/html-win32-x64-msvc@1.13.1': + optional: true + + '@swc/html@1.13.1': + dependencies: + '@swc/counter': 0.1.3 + optionalDependencies: + '@swc/html-darwin-arm64': 1.13.1 + '@swc/html-darwin-x64': 1.13.1 + '@swc/html-linux-arm-gnueabihf': 1.13.1 + '@swc/html-linux-arm64-gnu': 1.13.1 + '@swc/html-linux-arm64-musl': 1.13.1 + '@swc/html-linux-x64-gnu': 1.13.1 + '@swc/html-linux-x64-musl': 1.13.1 + '@swc/html-win32-arm64-msvc': 1.13.1 + '@swc/html-win32-ia32-msvc': 1.13.1 + '@swc/html-win32-x64-msvc': 1.13.1 + + '@swc/types@0.1.23': + dependencies: + '@swc/counter': 0.1.3 + + '@szmarczak/http-timer@5.0.1': + dependencies: + defer-to-connect: 2.0.1 + + '@tailwindcss/forms@0.5.7(tailwindcss@3.4.14)': + dependencies: + mini-svg-data-uri: 1.4.4 + tailwindcss: 3.4.14 + + '@tanem/svg-injector@10.1.68': + dependencies: + '@babel/runtime': 7.27.6 + content-type: 1.0.5 + tslib: 2.8.1 + + '@tanstack/react-virtual@3.13.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@tanstack/virtual-core': 3.13.12 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@tanstack/react-virtual@3.13.12(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@tanstack/virtual-core': 3.13.12 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + + '@tanstack/virtual-core@3.13.12': {} + + '@testing-library/dom@10.4.0': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/runtime': 7.27.6 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + chalk: 4.1.2 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + pretty-format: 27.5.1 + + '@testing-library/jest-dom@6.6.3': + dependencies: + '@adobe/css-tools': 4.4.3 + aria-query: 5.3.2 + chalk: 3.0.0 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + lodash: 4.17.21 + redent: 3.0.0 + + '@testing-library/react@16.3.0(@testing-library/dom@10.4.0)(@types/react-dom@19.1.2(@types/react@19.1.2))(@types/react@19.1.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@babel/runtime': 7.27.6 + '@testing-library/dom': 10.4.0 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.2 + '@types/react-dom': 19.1.2(@types/react@19.1.2) + + '@tootallnate/once@1.1.2': {} + + '@tootallnate/once@2.0.0': {} + + '@tootallnate/quickjs-emscripten@0.23.0': + optional: true + + '@trysound/sax@0.2.0': {} + + '@tufjs/canonical-json@1.0.0': {} + + '@tufjs/models@1.0.4': + dependencies: + '@tufjs/canonical-json': 1.0.0 + minimatch: 9.0.5 + + '@tybys/wasm-util@0.10.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/acorn@4.0.6': + dependencies: + '@types/estree': 1.0.8 + optional: true + + '@types/aria-query@5.0.4': {} + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.28.0 + '@babel/types': 7.28.1 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.20.7 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.28.1 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.28.0 + '@babel/types': 7.28.1 + + '@types/babel__traverse@7.20.7': + dependencies: + '@babel/types': 7.28.1 + + '@types/body-parser@1.19.6': + dependencies: + '@types/connect': 3.4.38 + '@types/node': 22.16.5 + + '@types/bonjour@3.5.13': + dependencies: + '@types/node': 22.16.5 + + '@types/codemirror@5.60.16': + dependencies: + '@types/tern': 0.23.9 + + '@types/connect-history-api-fallback@1.5.4': + dependencies: + '@types/express-serve-static-core': 5.0.7 + '@types/node': 22.16.5 + + '@types/connect@3.4.38': + dependencies: + '@types/node': 22.16.5 + + '@types/cors@2.8.19': + dependencies: + '@types/node': 22.16.5 + + '@types/d3-array@3.2.1': {} + + '@types/d3-axis@3.0.6': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-brush@3.0.6': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-chord@3.0.6': {} + + '@types/d3-color@3.1.3': {} + + '@types/d3-contour@3.0.6': + dependencies: + '@types/d3-array': 3.2.1 + '@types/geojson': 7946.0.16 + + '@types/d3-delaunay@6.0.4': {} + + '@types/d3-dispatch@3.0.6': {} + + '@types/d3-drag@3.0.7': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-dsv@3.0.7': {} + + '@types/d3-ease@3.0.2': {} + + '@types/d3-fetch@3.0.7': + dependencies: + '@types/d3-dsv': 3.0.7 + + '@types/d3-force@3.0.10': {} + + '@types/d3-format@3.0.4': {} + + '@types/d3-geo@3.1.0': + dependencies: + '@types/geojson': 7946.0.16 + + '@types/d3-hierarchy@3.1.7': {} + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-path@3.1.1': {} + + '@types/d3-polygon@3.0.2': {} + + '@types/d3-quadtree@3.0.6': {} + + '@types/d3-random@3.0.3': {} + + '@types/d3-scale-chromatic@3.1.0': {} + + '@types/d3-scale@4.0.9': + dependencies: + '@types/d3-time': 3.0.4 + + '@types/d3-selection@3.0.11': {} + + '@types/d3-shape@3.1.7': + dependencies: + '@types/d3-path': 3.1.1 + + '@types/d3-time-format@4.0.3': {} + + '@types/d3-time@3.0.4': {} + + '@types/d3-timer@3.0.2': {} + + '@types/d3-transition@3.0.9': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-zoom@3.0.8': + dependencies: + '@types/d3-interpolate': 3.0.4 + '@types/d3-selection': 3.0.11 + + '@types/d3@7.4.3': + dependencies: + '@types/d3-array': 3.2.1 + '@types/d3-axis': 3.0.6 + '@types/d3-brush': 3.0.6 + '@types/d3-chord': 3.0.6 + '@types/d3-color': 3.1.3 + '@types/d3-contour': 3.0.6 + '@types/d3-delaunay': 6.0.4 + '@types/d3-dispatch': 3.0.6 + '@types/d3-drag': 3.0.7 + '@types/d3-dsv': 3.0.7 + '@types/d3-ease': 3.0.2 + '@types/d3-fetch': 3.0.7 + '@types/d3-force': 3.0.10 + '@types/d3-format': 3.0.4 + '@types/d3-geo': 3.1.0 + '@types/d3-hierarchy': 3.1.7 + '@types/d3-interpolate': 3.0.4 + '@types/d3-path': 3.1.1 + '@types/d3-polygon': 3.0.2 + '@types/d3-quadtree': 3.0.6 + '@types/d3-random': 3.0.3 + '@types/d3-scale': 4.0.9 + '@types/d3-scale-chromatic': 3.1.0 + '@types/d3-selection': 3.0.11 + '@types/d3-shape': 3.1.7 + '@types/d3-time': 3.0.4 + '@types/d3-time-format': 4.0.3 + '@types/d3-timer': 3.0.2 + '@types/d3-transition': 3.0.9 + '@types/d3-zoom': 3.0.8 + + '@types/debug@4.1.12': + dependencies: + '@types/ms': 2.1.0 + + '@types/eslint-scope@3.7.7': + dependencies: + '@types/eslint': 9.6.1 + '@types/estree': 1.0.8 + + '@types/eslint@9.6.1': + dependencies: + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 + + '@types/estree-jsx@1.0.5': + dependencies: + '@types/estree': 1.0.8 + + '@types/estree@1.0.8': {} + + '@types/express-serve-static-core@4.19.6': + dependencies: + '@types/node': 22.16.5 + '@types/qs': 6.14.0 + '@types/range-parser': 1.2.7 + '@types/send': 0.17.5 + + '@types/express-serve-static-core@5.0.7': + dependencies: + '@types/node': 22.16.5 + '@types/qs': 6.14.0 + '@types/range-parser': 1.2.7 + '@types/send': 0.17.5 + + '@types/express@4.17.23': + dependencies: + '@types/body-parser': 1.19.6 + '@types/express-serve-static-core': 4.19.6 + '@types/qs': 6.14.0 + '@types/serve-static': 1.15.8 + + '@types/file-saver@2.0.7': {} + + '@types/geojson@7946.0.16': {} + + '@types/google-protobuf@3.15.12': {} + + '@types/gtag.js@0.0.12': {} + + '@types/hast@2.3.10': + dependencies: + '@types/unist': 2.0.11 + + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/history@4.7.11': {} + + '@types/hoist-non-react-statics@3.3.7(@types/react@19.1.2)': + dependencies: + '@types/react': 19.1.2 + hoist-non-react-statics: 3.3.2 + + '@types/html-minifier-terser@6.1.0': {} + + '@types/http-cache-semantics@4.0.4': {} + + '@types/http-errors@2.0.5': {} + + '@types/http-proxy@1.17.16': + dependencies: + '@types/node': 22.16.5 + + '@types/istanbul-lib-coverage@2.0.6': {} + + '@types/istanbul-lib-report@3.0.3': + dependencies: + '@types/istanbul-lib-coverage': 2.0.6 + + '@types/istanbul-reports@3.0.4': + dependencies: + '@types/istanbul-lib-report': 3.0.3 + + '@types/jasmine@5.1.8': {} + + '@types/jasminewd2@2.0.13': + dependencies: + '@types/jasmine': 5.1.8 + + '@types/json-schema@7.0.15': {} + + '@types/json5@0.0.29': {} + + '@types/jsonwebtoken@9.0.10': + dependencies: + '@types/ms': 2.1.0 + '@types/node': 22.16.5 + + '@types/mdast@3.0.15': + dependencies: + '@types/unist': 2.0.11 + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/mdx@2.0.13': {} + + '@types/mime@1.3.5': {} + + '@types/ms@2.1.0': {} + + '@types/node-fetch@2.6.12': + dependencies: + '@types/node': 22.16.5 + form-data: 4.0.4 + + '@types/node-forge@1.3.13': + dependencies: + '@types/node': 22.16.5 + + '@types/node@12.20.55': {} + + '@types/node@17.0.45': {} + + '@types/node@18.19.120': + dependencies: + undici-types: 5.26.5 + + '@types/node@22.16.5': + dependencies: + undici-types: 6.21.0 + + '@types/node@24.1.0': + dependencies: + undici-types: 7.8.0 + + '@types/normalize-package-data@2.4.4': {} + + '@types/opentype.js@1.3.8': {} + + '@types/parse5@6.0.3': {} + + '@types/pg@8.15.4': + dependencies: + '@types/node': 22.16.5 + pg-protocol: 1.10.3 + pg-types: 2.2.0 + + '@types/prismjs@1.26.5': {} + + '@types/prop-types@15.7.15': {} + + '@types/qrcode@1.5.5': + dependencies: + '@types/node': 22.16.5 + + '@types/qs@6.14.0': {} + + '@types/range-parser@1.2.7': {} + + '@types/react-dom@19.1.2(@types/react@19.1.2)': + dependencies: + '@types/react': 19.1.2 + + '@types/react-redux@7.1.34': + dependencies: + '@types/hoist-non-react-statics': 3.3.7(@types/react@19.1.2) + '@types/react': 19.1.2 + hoist-non-react-statics: 3.3.2 + redux: 4.2.1 + + '@types/react-router-config@5.0.11': + dependencies: + '@types/history': 4.7.11 + '@types/react': 19.1.2 + '@types/react-router': 5.1.20 + + '@types/react-router-dom@5.3.3': + dependencies: + '@types/history': 4.7.11 + '@types/react': 19.1.2 + '@types/react-router': 5.1.20 + + '@types/react-router@5.1.20': + dependencies: + '@types/history': 4.7.11 + '@types/react': 19.1.2 + + '@types/react@19.1.2': + dependencies: + csstype: 3.1.3 + + '@types/retry@0.12.0': {} + + '@types/sax@1.2.7': + dependencies: + '@types/node': 22.16.5 + + '@types/semver@7.7.0': {} + + '@types/send@0.17.5': + dependencies: + '@types/mime': 1.3.5 + '@types/node': 22.16.5 + + '@types/serve-index@1.9.4': + dependencies: + '@types/express': 4.17.23 + + '@types/serve-static@1.15.8': + dependencies: + '@types/http-errors': 2.0.5 + '@types/node': 22.16.5 + '@types/send': 0.17.5 + + '@types/sinonjs__fake-timers@8.1.1': {} + + '@types/sizzle@2.3.9': {} + + '@types/sockjs@0.3.36': + dependencies: + '@types/node': 22.16.5 + + '@types/tern@0.23.9': + dependencies: + '@types/estree': 1.0.8 + + '@types/tinycolor2@1.4.3': {} + + '@types/trusted-types@2.0.7': + optional: true + + '@types/unist@2.0.11': {} + + '@types/unist@3.0.3': {} + + '@types/uuid@10.0.0': {} + + '@types/ws@8.18.1': + dependencies: + '@types/node': 22.16.5 + + '@types/yargs-parser@21.0.3': {} + + '@types/yargs@17.0.33': + dependencies: + '@types/yargs-parser': 21.0.3 + + '@types/yauzl@2.10.3': + dependencies: + '@types/node': 22.16.5 + optional: true + + '@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@8.38.0(eslint@8.57.1)(typescript@5.1.6))(eslint@8.57.1)(typescript@5.1.6)': + dependencies: + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 8.38.0(eslint@8.57.1)(typescript@5.1.6) + '@typescript-eslint/scope-manager': 5.62.0 + '@typescript-eslint/type-utils': 5.62.0(eslint@8.57.1)(typescript@5.1.6) + '@typescript-eslint/utils': 5.62.0(eslint@8.57.1)(typescript@5.1.6) + debug: 4.4.1(supports-color@5.5.0) + eslint: 8.57.1 + graphemer: 1.4.0 + ignore: 5.3.2 + natural-compare-lite: 1.4.0 + semver: 7.7.2 + tsutils: 3.21.0(typescript@5.1.6) + optionalDependencies: + typescript: 5.1.6 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@8.38.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(typescript@5.8.3)': + dependencies: + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 8.38.0(eslint@8.57.1)(typescript@5.8.3) + '@typescript-eslint/scope-manager': 7.18.0 + '@typescript-eslint/type-utils': 7.18.0(eslint@8.57.1)(typescript@5.8.3) + '@typescript-eslint/utils': 7.18.0(eslint@8.57.1)(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 7.18.0 + eslint: 8.57.1 + graphemer: 1.4.0 + ignore: 5.3.2 + natural-compare: 1.4.0 + ts-api-utils: 1.4.3(typescript@5.8.3) + optionalDependencies: + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/eslint-plugin@8.38.0(@typescript-eslint/parser@8.38.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(typescript@5.8.3)': + dependencies: + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 8.38.0(eslint@8.57.1)(typescript@5.8.3) + '@typescript-eslint/scope-manager': 8.38.0 + '@typescript-eslint/type-utils': 8.38.0(eslint@8.57.1)(typescript@5.8.3) + '@typescript-eslint/utils': 8.38.0(eslint@8.57.1)(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 8.38.0 + eslint: 8.57.1 + graphemer: 1.4.0 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.1.0(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.38.0(eslint@8.57.1)(typescript@5.1.6)': + dependencies: + '@typescript-eslint/scope-manager': 8.38.0 + '@typescript-eslint/types': 8.38.0 + '@typescript-eslint/typescript-estree': 8.38.0(typescript@5.1.6) + '@typescript-eslint/visitor-keys': 8.38.0 + debug: 4.4.1(supports-color@5.5.0) + eslint: 8.57.1 + typescript: 5.1.6 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.38.0(eslint@8.57.1)(typescript@5.8.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.38.0 + '@typescript-eslint/types': 8.38.0 + '@typescript-eslint/typescript-estree': 8.38.0(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 8.38.0 + debug: 4.4.1(supports-color@5.5.0) + eslint: 8.57.1 + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.38.0(typescript@5.1.6)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.38.0(typescript@5.1.6) + '@typescript-eslint/types': 8.38.0 + debug: 4.4.1(supports-color@5.5.0) + typescript: 5.1.6 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.38.0(typescript@5.8.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.38.0(typescript@5.8.3) + '@typescript-eslint/types': 8.38.0 + debug: 4.4.1(supports-color@5.5.0) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@5.62.0': + dependencies: + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/visitor-keys': 5.62.0 + + '@typescript-eslint/scope-manager@7.18.0': + dependencies: + '@typescript-eslint/types': 7.18.0 + '@typescript-eslint/visitor-keys': 7.18.0 + + '@typescript-eslint/scope-manager@8.0.0-alpha.20': + dependencies: + '@typescript-eslint/types': 8.0.0-alpha.20 + '@typescript-eslint/visitor-keys': 8.0.0-alpha.20 + + '@typescript-eslint/scope-manager@8.38.0': + dependencies: + '@typescript-eslint/types': 8.38.0 + '@typescript-eslint/visitor-keys': 8.38.0 + + '@typescript-eslint/tsconfig-utils@8.38.0(typescript@5.1.6)': + dependencies: + typescript: 5.1.6 + + '@typescript-eslint/tsconfig-utils@8.38.0(typescript@5.8.3)': + dependencies: + typescript: 5.8.3 + + '@typescript-eslint/type-utils@5.62.0(eslint@8.57.1)(typescript@5.1.6)': + dependencies: + '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.1.6) + '@typescript-eslint/utils': 5.62.0(eslint@8.57.1)(typescript@5.1.6) + debug: 4.4.1(supports-color@5.5.0) + eslint: 8.57.1 + tsutils: 3.21.0(typescript@5.1.6) + optionalDependencies: + typescript: 5.1.6 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/type-utils@7.18.0(eslint@8.57.1)(typescript@5.8.3)': + dependencies: + '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.8.3) + '@typescript-eslint/utils': 7.18.0(eslint@8.57.1)(typescript@5.8.3) + debug: 4.4.1(supports-color@5.5.0) + eslint: 8.57.1 + ts-api-utils: 1.4.3(typescript@5.8.3) + optionalDependencies: + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/type-utils@8.38.0(eslint@8.57.1)(typescript@5.8.3)': + dependencies: + '@typescript-eslint/types': 8.38.0 + '@typescript-eslint/typescript-estree': 8.38.0(typescript@5.8.3) + '@typescript-eslint/utils': 8.38.0(eslint@8.57.1)(typescript@5.8.3) + debug: 4.4.1(supports-color@5.5.0) + eslint: 8.57.1 + ts-api-utils: 2.1.0(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@5.62.0': {} + + '@typescript-eslint/types@7.18.0': {} + + '@typescript-eslint/types@8.0.0-alpha.20': {} + + '@typescript-eslint/types@8.38.0': {} + + '@typescript-eslint/typescript-estree@5.62.0(typescript@5.1.6)': + dependencies: + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/visitor-keys': 5.62.0 + debug: 4.4.1(supports-color@5.5.0) + globby: 11.1.0 + is-glob: 4.0.3 + semver: 7.7.2 + tsutils: 3.21.0(typescript@5.1.6) + optionalDependencies: + typescript: 5.1.6 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/typescript-estree@7.18.0(typescript@5.8.3)': + dependencies: + '@typescript-eslint/types': 7.18.0 + '@typescript-eslint/visitor-keys': 7.18.0 + debug: 4.4.1(supports-color@5.5.0) + globby: 11.1.0 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.7.2 + ts-api-utils: 1.4.3(typescript@5.8.3) + optionalDependencies: + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/typescript-estree@8.0.0-alpha.20(typescript@5.1.6)': + dependencies: + '@typescript-eslint/types': 8.0.0-alpha.20 + '@typescript-eslint/visitor-keys': 8.0.0-alpha.20 + debug: 4.4.1(supports-color@5.5.0) + globby: 11.1.0 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.7.2 + ts-api-utils: 1.4.3(typescript@5.1.6) + optionalDependencies: + typescript: 5.1.6 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/typescript-estree@8.38.0(typescript@5.1.6)': + dependencies: + '@typescript-eslint/project-service': 8.38.0(typescript@5.1.6) + '@typescript-eslint/tsconfig-utils': 8.38.0(typescript@5.1.6) + '@typescript-eslint/types': 8.38.0 + '@typescript-eslint/visitor-keys': 8.38.0 + debug: 4.4.1(supports-color@5.5.0) + fast-glob: 3.3.3 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.7.2 + ts-api-utils: 2.1.0(typescript@5.1.6) + typescript: 5.1.6 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/typescript-estree@8.38.0(typescript@5.8.3)': + dependencies: + '@typescript-eslint/project-service': 8.38.0(typescript@5.8.3) + '@typescript-eslint/tsconfig-utils': 8.38.0(typescript@5.8.3) + '@typescript-eslint/types': 8.38.0 + '@typescript-eslint/visitor-keys': 8.38.0 + debug: 4.4.1(supports-color@5.5.0) + fast-glob: 3.3.3 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.7.2 + ts-api-utils: 2.1.0(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@5.62.0(eslint@8.57.1)(typescript@5.1.6)': + dependencies: + '@eslint-community/eslint-utils': 4.7.0(eslint@8.57.1) + '@types/json-schema': 7.0.15 + '@types/semver': 7.7.0 + '@typescript-eslint/scope-manager': 5.62.0 + '@typescript-eslint/types': 5.62.0 + '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.1.6) + eslint: 8.57.1 + eslint-scope: 5.1.1 + semver: 7.7.2 + transitivePeerDependencies: + - supports-color + - typescript + + '@typescript-eslint/utils@7.18.0(eslint@8.57.1)(typescript@5.8.3)': + dependencies: + '@eslint-community/eslint-utils': 4.7.0(eslint@8.57.1) + '@typescript-eslint/scope-manager': 7.18.0 + '@typescript-eslint/types': 7.18.0 + '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.8.3) + eslint: 8.57.1 + transitivePeerDependencies: + - supports-color + - typescript + + '@typescript-eslint/utils@8.0.0-alpha.20(eslint@8.57.1)(typescript@5.1.6)': + dependencies: + '@eslint-community/eslint-utils': 4.7.0(eslint@8.57.1) + '@typescript-eslint/scope-manager': 8.0.0-alpha.20 + '@typescript-eslint/types': 8.0.0-alpha.20 + '@typescript-eslint/typescript-estree': 8.0.0-alpha.20(typescript@5.1.6) + eslint: 8.57.1 + transitivePeerDependencies: + - supports-color + - typescript + + '@typescript-eslint/utils@8.38.0(eslint@8.57.1)(typescript@5.8.3)': + dependencies: + '@eslint-community/eslint-utils': 4.7.0(eslint@8.57.1) + '@typescript-eslint/scope-manager': 8.38.0 + '@typescript-eslint/types': 8.38.0 + '@typescript-eslint/typescript-estree': 8.38.0(typescript@5.8.3) + eslint: 8.57.1 + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@5.62.0': + dependencies: + '@typescript-eslint/types': 5.62.0 + eslint-visitor-keys: 3.4.3 + + '@typescript-eslint/visitor-keys@7.18.0': + dependencies: + '@typescript-eslint/types': 7.18.0 + eslint-visitor-keys: 3.4.3 + + '@typescript-eslint/visitor-keys@8.0.0-alpha.20': + dependencies: + '@typescript-eslint/types': 8.0.0-alpha.20 + eslint-visitor-keys: 3.4.3 + + '@typescript-eslint/visitor-keys@8.38.0': + dependencies: + '@typescript-eslint/types': 8.38.0 + eslint-visitor-keys: 4.2.1 + + '@ungap/structured-clone@1.3.0': {} + + '@unrs/resolver-binding-android-arm-eabi@1.11.1': + optional: true + + '@unrs/resolver-binding-android-arm64@1.11.1': + optional: true + + '@unrs/resolver-binding-darwin-arm64@1.11.1': + optional: true + + '@unrs/resolver-binding-darwin-x64@1.11.1': + optional: true + + '@unrs/resolver-binding-freebsd-x64@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-x64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-x64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-wasm32-wasi@1.11.1': + dependencies: + '@napi-rs/wasm-runtime': 0.2.12 + optional: true + + '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': + optional: true + + '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': + optional: true + + '@unrs/resolver-binding-win32-x64-msvc@1.11.1': + optional: true + + '@vercel/analytics@1.5.0(next@15.4.0-canary.86(@babel/core@7.28.0)(@playwright/test@1.54.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.2))(react@19.1.0)': + optionalDependencies: + next: 15.4.0-canary.86(@babel/core@7.28.0)(@playwright/test@1.54.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.2) + react: 19.1.0 + + '@vercel/git-hooks@1.0.0': {} + + '@vitejs/plugin-basic-ssl@1.0.1(vite@4.5.5(@types/node@22.16.5)(less@4.1.3)(lightningcss@1.30.1)(sass@1.64.1)(terser@5.19.2))': + dependencies: + vite: 4.5.5(@types/node@22.16.5)(less@4.1.3)(lightningcss@1.30.1)(sass@1.64.1)(terser@5.19.2) + + '@vitejs/plugin-react@4.7.0(vite@5.4.19(@types/node@22.16.5)(less@4.1.3)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1))': + dependencies: + '@babel/core': 7.28.0 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.0) + '@rolldown/pluginutils': 1.0.0-beta.27 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 5.4.19(@types/node@22.16.5)(less@4.1.3)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1) + transitivePeerDependencies: + - supports-color + + '@vitest/expect@2.1.9': + dependencies: + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.2.1 + tinyrainbow: 1.2.0 + + '@vitest/mocker@2.1.9(vite@5.4.19(@types/node@22.16.5)(less@4.1.3)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1))': + dependencies: + '@vitest/spy': 2.1.9 + estree-walker: 3.0.3 + magic-string: 0.30.17 + optionalDependencies: + vite: 5.4.19(@types/node@22.16.5)(less@4.1.3)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1) + + '@vitest/mocker@2.1.9(vite@5.4.19(@types/node@24.1.0)(less@4.1.3)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1))': + dependencies: + '@vitest/spy': 2.1.9 + estree-walker: 3.0.3 + magic-string: 0.30.17 + optionalDependencies: + vite: 5.4.19(@types/node@24.1.0)(less@4.1.3)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1) + + '@vitest/pretty-format@2.1.9': + dependencies: + tinyrainbow: 1.2.0 + + '@vitest/runner@2.1.9': + dependencies: + '@vitest/utils': 2.1.9 + pathe: 1.1.2 + + '@vitest/snapshot@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + magic-string: 0.30.17 + pathe: 1.1.2 + + '@vitest/spy@2.1.9': + dependencies: + tinyspy: 3.0.2 + + '@vitest/utils@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + loupe: 3.1.4 + tinyrainbow: 1.2.0 + + '@webassemblyjs/ast@1.14.1': + dependencies: + '@webassemblyjs/helper-numbers': 1.13.2 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + + '@webassemblyjs/floating-point-hex-parser@1.13.2': {} + + '@webassemblyjs/helper-api-error@1.13.2': {} + + '@webassemblyjs/helper-buffer@1.14.1': {} + + '@webassemblyjs/helper-numbers@1.13.2': + dependencies: + '@webassemblyjs/floating-point-hex-parser': 1.13.2 + '@webassemblyjs/helper-api-error': 1.13.2 + '@xtuc/long': 4.2.2 + + '@webassemblyjs/helper-wasm-bytecode@1.13.2': {} + + '@webassemblyjs/helper-wasm-section@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/wasm-gen': 1.14.1 + + '@webassemblyjs/ieee754@1.13.2': + dependencies: + '@xtuc/ieee754': 1.2.0 + + '@webassemblyjs/leb128@1.13.2': + dependencies: + '@xtuc/long': 4.2.2 + + '@webassemblyjs/utf8@1.13.2': {} + + '@webassemblyjs/wasm-edit@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/helper-wasm-section': 1.14.1 + '@webassemblyjs/wasm-gen': 1.14.1 + '@webassemblyjs/wasm-opt': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + '@webassemblyjs/wast-printer': 1.14.1 + + '@webassemblyjs/wasm-gen@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/ieee754': 1.13.2 + '@webassemblyjs/leb128': 1.13.2 + '@webassemblyjs/utf8': 1.13.2 + + '@webassemblyjs/wasm-opt@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/wasm-gen': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + + '@webassemblyjs/wasm-parser@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-api-error': 1.13.2 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/ieee754': 1.13.2 + '@webassemblyjs/leb128': 1.13.2 + '@webassemblyjs/utf8': 1.13.2 + + '@webassemblyjs/wast-printer@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@xtuc/long': 4.2.2 + + '@wessberg/ts-evaluator@0.0.27(typescript@5.1.6)': + dependencies: + chalk: 4.1.2 + jsdom: 16.7.0 + object-path: 0.11.8 + tslib: 2.8.1 + typescript: 5.1.6 + transitivePeerDependencies: + - bufferutil + - canvas + - supports-color + - utf-8-validate + + '@xtuc/ieee754@1.2.0': {} + + '@xtuc/long@4.2.2': {} + + '@yarnpkg/lockfile@1.1.0': {} + + '@yarnpkg/parsers@3.0.0-rc.46': + dependencies: + js-yaml: 3.14.1 + tslib: 2.8.1 + + '@zag-js/core@1.19.0': + dependencies: + '@zag-js/dom-query': 1.19.0 + '@zag-js/utils': 1.19.0 + + '@zag-js/dom-query@1.19.0': + dependencies: + '@zag-js/types': 1.19.0 + + '@zag-js/focus-trap@1.19.0': + dependencies: + '@zag-js/dom-query': 1.19.0 + + '@zag-js/presence@1.19.0': + dependencies: + '@zag-js/core': 1.19.0 + '@zag-js/dom-query': 1.19.0 + '@zag-js/types': 1.19.0 + + '@zag-js/react@1.19.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@zag-js/core': 1.19.0 + '@zag-js/store': 1.19.0 + '@zag-js/types': 1.19.0 + '@zag-js/utils': 1.19.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@zag-js/store@1.19.0': + dependencies: + proxy-compare: 3.0.1 + + '@zag-js/types@1.19.0': + dependencies: + csstype: 3.1.3 + + '@zag-js/utils@1.19.0': {} + + '@zkochan/js-yaml@0.0.6': + dependencies: + argparse: 2.0.1 + + abab@2.0.6: {} + + abbrev@1.1.1: {} + + abort-controller-x@0.4.3: {} + + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + + accepts@1.3.8: + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + + acorn-globals@6.0.0: + dependencies: + acorn: 7.4.1 + acorn-walk: 7.2.0 + + acorn-import-attributes@1.9.5(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + + acorn-import-phases@1.0.4(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + + acorn-jsx@5.3.2(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + + acorn-walk@7.2.0: {} + + acorn-walk@8.3.4: + dependencies: + acorn: 8.15.0 + + acorn@7.4.1: {} + + acorn@8.15.0: {} + + address@1.2.2: {} + + adjust-sourcemap-loader@4.0.0: + dependencies: + loader-utils: 2.0.4 + regex-parser: 2.3.1 + + agent-base@6.0.2: + dependencies: + debug: 4.4.1(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + + agent-base@7.1.4: {} + + agentkeepalive@4.6.0: + dependencies: + humanize-ms: 1.2.1 + + aggregate-error@3.1.0: + dependencies: + clean-stack: 2.2.0 + indent-string: 4.0.0 + + ajv-draft-04@1.0.0(ajv@8.11.0): + optionalDependencies: + ajv: 8.11.0 + + ajv-formats@2.1.1(ajv@8.11.0): + optionalDependencies: + ajv: 8.11.0 + + ajv-formats@2.1.1(ajv@8.12.0): + optionalDependencies: + ajv: 8.12.0 + + ajv-formats@2.1.1(ajv@8.17.1): + optionalDependencies: + ajv: 8.17.1 + + ajv-keywords@3.5.2(ajv@6.12.6): + dependencies: + ajv: 6.12.6 + + ajv-keywords@5.1.0(ajv@8.17.1): + dependencies: + ajv: 8.17.1 + fast-deep-equal: 3.1.3 + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ajv@8.11.0: + dependencies: + fast-deep-equal: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + uri-js: 4.4.1 + + ajv@8.12.0: + dependencies: + fast-deep-equal: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + uri-js: 4.4.1 + + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.0.6 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + algoliasearch-helper@3.26.0(algoliasearch@5.34.0): + dependencies: + '@algolia/events': 4.0.1 + algoliasearch: 5.34.0 + + algoliasearch@5.34.0: + dependencies: + '@algolia/client-abtesting': 5.34.0 + '@algolia/client-analytics': 5.34.0 + '@algolia/client-common': 5.34.0 + '@algolia/client-insights': 5.34.0 + '@algolia/client-personalization': 5.34.0 + '@algolia/client-query-suggestions': 5.34.0 + '@algolia/client-search': 5.34.0 + '@algolia/ingestion': 1.34.0 + '@algolia/monitoring': 1.34.0 + '@algolia/recommend': 5.34.0 + '@algolia/requester-browser-xhr': 5.34.0 + '@algolia/requester-fetch': 5.34.0 + '@algolia/requester-node-http': 5.34.0 + + allof-merge@0.6.6: + dependencies: + json-crawl: 0.5.3 + + altcha-lib@1.3.0: {} + + angular-oauth2-oidc@15.0.1(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)): + dependencies: + '@angular/common': 16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2) + '@angular/core': 16.2.12(rxjs@7.8.2)(zone.js@0.13.3) + tslib: 2.8.1 + + angularx-qrcode@16.0.2(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)): + dependencies: + '@angular/core': 16.2.12(rxjs@7.8.2)(zone.js@0.13.3) + qrcode: 1.5.3 + tslib: 2.8.1 + + ansi-align@3.0.1: + dependencies: + string-width: 4.2.3 + + ansi-colors@4.1.3: {} + + ansi-escapes@4.3.2: + dependencies: + type-fest: 0.21.3 + + ansi-escapes@7.0.0: + dependencies: + environment: 1.1.0 + + ansi-html-community@0.0.8: {} + + ansi-regex@5.0.1: {} + + ansi-regex@6.1.0: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@5.2.0: {} + + ansi-styles@6.2.1: {} + + any-promise@1.3.0: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + aproba@2.1.0: {} + + arch@2.2.0: {} + + are-we-there-yet@2.0.0: + dependencies: + delegates: 1.0.0 + readable-stream: 3.6.2 + + are-we-there-yet@3.0.1: + dependencies: + delegates: 1.0.0 + readable-stream: 3.6.2 + + arg@5.0.2: {} + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + argparse@2.0.1: {} + + aria-hidden@1.2.6: + dependencies: + tslib: 2.8.1 + + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + + aria-query@5.3.2: {} + + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + + array-flatten@1.1.1: {} + + array-includes@3.1.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + is-string: 1.1.1 + math-intrinsics: 1.1.0 + + array-union@2.1.0: {} + + array.prototype.findlast@1.2.5: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-shim-unscopables: 1.1.0 + + array.prototype.findlastindex@1.2.6: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-shim-unscopables: 1.1.0 + + array.prototype.flat@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-shim-unscopables: 1.1.0 + + array.prototype.flatmap@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-shim-unscopables: 1.1.0 + + array.prototype.tosorted@1.1.4: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + es-shim-unscopables: 1.1.0 + + arraybuffer.prototype.slice@1.0.4: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + is-array-buffer: 3.0.5 + + asn1@0.2.6: + dependencies: + safer-buffer: 2.1.2 + + assert-plus@1.0.0: {} + + assertion-error@2.0.1: {} + + ast-types-flow@0.0.8: {} + + ast-types@0.13.4: + dependencies: + tslib: 2.8.1 + optional: true + + astral-regex@2.0.0: {} + + astring@1.9.0: {} + + async-function@1.0.0: {} + + async@3.2.2: {} + + async@3.2.4: {} + + async@3.2.6: {} + + asynckit@0.4.0: {} + + at-least-node@1.0.0: {} + + autoprefixer@10.4.14(postcss@8.4.31): + dependencies: + browserslist: 4.25.1 + caniuse-lite: 1.0.30001727 + fraction.js: 4.3.7 + normalize-range: 0.1.2 + picocolors: 1.1.1 + postcss: 8.4.31 + postcss-value-parser: 4.2.0 + + autoprefixer@10.4.21(postcss@8.5.3): + dependencies: + browserslist: 4.25.1 + caniuse-lite: 1.0.30001727 + fraction.js: 4.3.7 + normalize-range: 0.1.2 + picocolors: 1.1.1 + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + + autoprefixer@10.4.21(postcss@8.5.6): + dependencies: + browserslist: 4.25.1 + caniuse-lite: 1.0.30001727 + fraction.js: 4.3.7 + normalize-range: 0.1.2 + picocolors: 1.1.1 + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + + aws-sign2@0.7.0: {} + + aws4@1.13.2: {} + + axe-core@4.10.3: {} + + axios@1.10.0(debug@4.4.1): + dependencies: + follow-redirects: 1.15.9(debug@4.4.1) + form-data: 4.0.4 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + + axobject-query@3.2.1: + dependencies: + dequal: 2.0.3 + + axobject-query@4.0.0: + dependencies: + dequal: 2.0.3 + + axobject-query@4.1.0: {} + + b4a@1.6.7: + optional: true + + babel-loader@9.1.3(@babel/core@7.22.9)(webpack@5.94.0(@swc/core@1.13.1)(esbuild@0.18.17)): + dependencies: + '@babel/core': 7.22.9 + find-cache-dir: 4.0.0 + schema-utils: 4.3.2 + webpack: 5.94.0(@swc/core@1.13.1)(esbuild@0.18.17) + + babel-loader@9.2.1(@babel/core@7.28.0)(webpack@5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17))): + dependencies: + '@babel/core': 7.28.0 + find-cache-dir: 4.0.0 + schema-utils: 4.3.2 + webpack: 5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17)) + + babel-plugin-dynamic-import-node@2.3.3: + dependencies: + object.assign: 4.1.7 + + babel-plugin-istanbul@6.1.1: + dependencies: + '@babel/helper-plugin-utils': 7.27.1 + '@istanbuljs/load-nyc-config': 1.1.0 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-instrument: 5.2.1 + test-exclude: 6.0.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-polyfill-corejs2@0.4.14(@babel/core@7.22.9): + dependencies: + '@babel/compat-data': 7.28.0 + '@babel/core': 7.22.9 + '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.22.9) + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + babel-plugin-polyfill-corejs2@0.4.14(@babel/core@7.28.0): + dependencies: + '@babel/compat-data': 7.28.0 + '@babel/core': 7.28.0 + '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.28.0) + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + babel-plugin-polyfill-corejs3@0.13.0(@babel/core@7.28.0): + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.28.0) + core-js-compat: 3.44.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-polyfill-corejs3@0.8.7(@babel/core@7.22.9): + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-define-polyfill-provider': 0.4.4(@babel/core@7.22.9) + core-js-compat: 3.44.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-polyfill-regenerator@0.5.5(@babel/core@7.22.9): + dependencies: + '@babel/core': 7.22.9 + '@babel/helper-define-polyfill-provider': 0.5.0(@babel/core@7.22.9) + transitivePeerDependencies: + - supports-color + + babel-plugin-polyfill-regenerator@0.6.5(@babel/core@7.28.0): + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.28.0) + transitivePeerDependencies: + - supports-color + + bail@2.0.2: {} + + balanced-match@1.0.2: {} + + bare-events@2.6.0: + optional: true + + bare-fs@4.1.6: + dependencies: + bare-events: 2.6.0 + bare-path: 3.0.0 + bare-stream: 2.6.5(bare-events@2.6.0) + optional: true + + bare-os@3.6.1: + optional: true + + bare-path@3.0.0: + dependencies: + bare-os: 3.6.1 + optional: true + + bare-stream@2.6.5(bare-events@2.6.0): + dependencies: + streamx: 2.22.1 + optionalDependencies: + bare-events: 2.6.0 + optional: true + + base64-js@1.5.1: {} + + base64id@2.0.0: {} + + basic-ftp@5.0.5: + optional: true + + batch@0.6.1: {} + + bcp-47-match@2.0.3: {} + + bcrypt-pbkdf@1.0.2: + dependencies: + tweetnacl: 0.14.5 + + better-path-resolve@1.0.0: + dependencies: + is-windows: 1.0.2 + + big.js@5.2.2: {} + + binary-extensions@2.3.0: {} + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + + blob-util@2.0.2: {} + + bluebird@3.7.2: {} + + body-parser@1.20.3: + 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 + transitivePeerDependencies: + - supports-color + + bonjour-service@1.3.0: + dependencies: + fast-deep-equal: 3.1.3 + multicast-dns: 7.2.5 + + boolbase@1.0.0: {} + + boxen@6.2.1: + dependencies: + ansi-align: 3.0.1 + camelcase: 6.3.0 + chalk: 4.1.2 + cli-boxes: 3.0.0 + string-width: 5.1.2 + type-fest: 2.19.0 + widest-line: 4.0.1 + wrap-ansi: 8.1.0 + + boxen@7.1.1: + dependencies: + ansi-align: 3.0.1 + camelcase: 7.0.1 + chalk: 5.4.1 + cli-boxes: 3.0.0 + string-width: 5.1.2 + type-fest: 2.19.0 + widest-line: 4.0.1 + wrap-ansi: 8.1.0 + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browser-process-hrtime@1.0.0: {} + + browser-stdout@1.3.1: {} + + browserslist@4.25.1: + dependencies: + caniuse-lite: 1.0.30001727 + electron-to-chromium: 1.5.189 + node-releases: 2.0.19 + update-browserslist-db: 1.1.3(browserslist@4.25.1) + + buffer-crc32@0.2.13: {} + + buffer-equal-constant-time@1.0.1: {} + + buffer-from@1.1.2: {} + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + bundle-require@5.1.0(esbuild@0.25.8): + dependencies: + esbuild: 0.25.8 + load-tsconfig: 0.2.5 + + bytes@3.0.0: {} + + bytes@3.1.2: {} + + cac@6.7.14: {} + + cacache@16.1.3: + dependencies: + '@npmcli/fs': 2.1.2 + '@npmcli/move-file': 2.0.1 + chownr: 2.0.0 + fs-minipass: 2.1.0 + glob: 8.1.0 + infer-owner: 1.0.4 + lru-cache: 7.18.3 + minipass: 3.3.6 + minipass-collect: 1.0.2 + minipass-flush: 1.0.5 + minipass-pipeline: 1.2.4 + mkdirp: 1.0.4 + p-map: 4.0.0 + promise-inflight: 1.0.1 + rimraf: 3.0.2 + ssri: 9.0.1 + tar: 6.2.1 + unique-filename: 2.0.1 + transitivePeerDependencies: + - bluebird + + cacache@17.1.4: + dependencies: + '@npmcli/fs': 3.1.1 + fs-minipass: 3.0.3 + glob: 10.4.5 + lru-cache: 7.18.3 + minipass: 7.1.2 + minipass-collect: 1.0.2 + minipass-flush: 1.0.5 + minipass-pipeline: 1.2.4 + p-map: 4.0.0 + ssri: 10.0.6 + tar: 6.2.1 + unique-filename: 3.0.0 + + cacheable-lookup@7.0.0: {} + + cacheable-request@10.2.14: + dependencies: + '@types/http-cache-semantics': 4.0.4 + get-stream: 6.0.1 + http-cache-semantics: 4.2.0 + keyv: 4.5.4 + mimic-response: 4.0.0 + normalize-url: 8.0.2 + responselike: 3.0.0 + + cachedir@2.4.0: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.8: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + call-me-maybe@1.0.2: {} + + callsites@3.1.0: {} + + camel-case@4.1.2: + dependencies: + pascal-case: 3.1.2 + tslib: 2.8.1 + + camelcase-css@2.0.1: {} + + camelcase@5.3.1: {} + + camelcase@6.3.0: {} + + camelcase@7.0.1: {} + + caniuse-api@3.0.0: + dependencies: + browserslist: 4.25.1 + caniuse-lite: 1.0.30001727 + lodash.memoize: 4.1.2 + lodash.uniq: 4.5.0 + + caniuse-lite@1.0.30001727: {} + + case-anything@2.1.13: {} + + caseless@0.12.0: {} + + ccount@2.0.1: {} + + chai@5.2.1: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.1 + deep-eql: 5.0.2 + loupe: 3.1.4 + pathval: 2.0.1 + + chalk@3.0.0: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@5.4.1: {} + + char-regex@1.0.2: {} + + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + character-entities@2.0.2: {} + + character-reference-invalid@2.0.1: {} + + chardet@0.7.0: {} + + charset@1.0.1: {} + + check-error@2.1.1: {} + + check-more-types@2.24.0: {} + + cheerio-select@2.1.0: + dependencies: + boolbase: 1.0.0 + css-select: 5.2.2 + css-what: 6.2.2 + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + + cheerio@1.0.0-rc.12: + dependencies: + cheerio-select: 2.1.0 + dom-serializer: 2.0.0 + domhandler: 5.0.3 + domutils: 3.2.2 + htmlparser2: 8.0.2 + parse5: 7.3.0 + parse5-htmlparser2-tree-adapter: 7.1.0 + + chevrotain-allstar@0.3.1(chevrotain@11.0.3): + dependencies: + chevrotain: 11.0.3 + lodash-es: 4.17.21 + + chevrotain@11.0.3: + dependencies: + '@chevrotain/cst-dts-gen': 11.0.3 + '@chevrotain/gast': 11.0.3 + '@chevrotain/regexp-to-ast': 11.0.3 + '@chevrotain/types': 11.0.3 + '@chevrotain/utils': 11.0.3 + lodash-es: 4.17.21 + + chokidar@3.5.3: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + chownr@2.0.0: {} + + chrome-trace-event@1.0.4: {} + + chromium-bidi@0.6.3(devtools-protocol@0.0.1312386): + dependencies: + devtools-protocol: 0.0.1312386 + mitt: 3.0.1 + urlpattern-polyfill: 10.0.0 + zod: 3.23.8 + optional: true + + ci-info@3.9.0: {} + + ci-info@4.3.0: {} + + class-variance-authority@0.7.1: + dependencies: + clsx: 2.1.1 + + clean-css@5.3.3: + dependencies: + source-map: 0.6.1 + + clean-stack@2.2.0: {} + + cli-boxes@3.0.0: {} + + cli-cursor@3.1.0: + dependencies: + restore-cursor: 3.1.0 + + cli-cursor@5.0.0: + dependencies: + restore-cursor: 5.1.0 + + cli-spinners@2.6.1: {} + + cli-spinners@2.9.2: {} + + cli-table3@0.6.1: + dependencies: + string-width: 4.2.3 + optionalDependencies: + colors: 1.4.0 + + cli-table3@0.6.5: + dependencies: + string-width: 4.2.3 + optionalDependencies: + '@colors/colors': 1.5.0 + + cli-truncate@2.1.0: + dependencies: + slice-ansi: 3.0.0 + string-width: 4.2.3 + + cli-truncate@4.0.0: + dependencies: + slice-ansi: 5.0.0 + string-width: 7.2.0 + + cli-width@3.0.0: {} + + client-only@0.0.1: {} + + cliui@6.0.0: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + + cliui@7.0.4: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + clone-deep@4.0.1: + dependencies: + is-plain-object: 2.0.4 + kind-of: 6.0.3 + shallow-clone: 3.0.1 + + clone@1.0.4: {} + + clsx@1.2.1: {} + + clsx@2.1.1: {} + + codemirror@5.65.19: {} + + collapse-white-space@2.1.0: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + color-string@1.9.1: + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.2 + optional: true + + color-support@1.1.3: {} + + color@4.2.3: + dependencies: + color-convert: 2.0.1 + color-string: 1.9.1 + optional: true + + colord@2.9.3: {} + + colorette@1.4.0: {} + + colorette@2.0.20: {} + + colorjs.io@0.5.2: {} + + colors@1.4.0: {} + + combine-promises@1.2.0: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + comma-separated-tokens@2.0.3: {} + + commander@10.0.1: {} + + commander@13.1.0: {} + + commander@2.20.3: {} + + commander@4.1.1: {} + + commander@5.1.0: {} + + commander@6.2.1: {} + + commander@7.2.0: {} + + commander@8.3.0: {} + + common-path-prefix@3.0.0: {} + + common-tags@1.8.2: {} + + compressible@2.0.18: + dependencies: + mime-db: 1.54.0 + + compression@1.8.1: + dependencies: + bytes: 3.1.2 + compressible: 2.0.18 + debug: 2.6.9 + negotiator: 0.6.4 + on-headers: 1.1.0 + safe-buffer: 5.2.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + compute-gcd@1.2.1: + dependencies: + validate.io-array: 1.0.6 + validate.io-function: 1.0.2 + validate.io-integer-array: 1.0.0 + + compute-lcm@1.1.2: + dependencies: + compute-gcd: 1.2.1 + validate.io-array: 1.0.6 + validate.io-function: 1.0.2 + validate.io-integer-array: 1.0.0 + + concat-map@0.0.1: {} + + concurrently@9.2.0: + dependencies: + chalk: 4.1.2 + lodash: 4.17.21 + rxjs: 7.8.2 + shell-quote: 1.8.3 + supports-color: 8.1.1 + tree-kill: 1.2.2 + yargs: 17.7.2 + + confbox@0.1.8: {} + + confbox@0.2.2: {} + + config-chain@1.1.13: + dependencies: + ini: 1.3.8 + proto-list: 1.2.4 + + configstore@6.0.0: + dependencies: + dot-prop: 6.0.1 + graceful-fs: 4.2.11 + unique-string: 3.0.0 + write-file-atomic: 3.0.3 + xdg-basedir: 5.1.0 + + connect-history-api-fallback@2.0.0: {} + + connect@3.7.0: + dependencies: + debug: 2.6.9 + finalhandler: 1.1.2 + parseurl: 1.3.3 + utils-merge: 1.0.1 + transitivePeerDependencies: + - supports-color + + consola@3.4.2: {} + + console-control-strings@1.1.0: {} + + content-disposition@0.5.2: {} + + content-disposition@0.5.4: + dependencies: + safe-buffer: 5.2.1 + + content-type@1.0.5: {} + + convert-source-map@1.9.0: {} + + convert-source-map@2.0.0: {} + + cookie-signature@1.0.6: {} + + cookie@0.7.1: {} + + cookie@0.7.2: {} + + copy-anything@2.0.6: + dependencies: + is-what: 3.14.1 + + copy-text-to-clipboard@3.2.0: {} + + copy-to-clipboard@3.3.3: + dependencies: + toggle-selection: 1.0.6 + + copy-webpack-plugin@11.0.0(webpack@5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17))): + dependencies: + fast-glob: 3.3.1 + glob-parent: 6.0.2 + globby: 13.2.2 + normalize-path: 3.0.0 + schema-utils: 4.3.2 + serialize-javascript: 6.0.2 + webpack: 5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17)) + + copy-webpack-plugin@11.0.0(webpack@5.94.0(@swc/core@1.13.1)(esbuild@0.18.17)): + dependencies: + fast-glob: 3.3.1 + glob-parent: 6.0.2 + globby: 13.2.2 + normalize-path: 3.0.0 + schema-utils: 4.3.2 + serialize-javascript: 6.0.2 + webpack: 5.94.0(@swc/core@1.13.1)(esbuild@0.18.17) + + core-js-compat@3.44.0: + dependencies: + browserslist: 4.25.1 + + core-js-pure@3.44.0: {} + + core-js@3.44.0: {} + + core-util-is@1.0.2: {} + + core-util-is@1.0.3: {} + + cors@2.8.5: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + cose-base@1.0.3: + dependencies: + layout-base: 1.0.2 + + cose-base@2.2.0: + dependencies: + layout-base: 2.0.1 + + cosmiconfig@8.3.6(typescript@5.1.6): + dependencies: + import-fresh: 3.3.1 + js-yaml: 4.1.0 + parse-json: 5.2.0 + path-type: 4.0.0 + optionalDependencies: + typescript: 5.1.6 + + cosmiconfig@8.3.6(typescript@5.8.3): + dependencies: + import-fresh: 3.3.1 + js-yaml: 4.1.0 + parse-json: 5.2.0 + path-type: 4.0.0 + optionalDependencies: + typescript: 5.8.3 + + cosmiconfig@9.0.0(typescript@5.8.3): + dependencies: + env-paths: 2.2.1 + import-fresh: 3.3.1 + js-yaml: 4.1.0 + parse-json: 5.2.0 + optionalDependencies: + typescript: 5.8.3 + optional: true + + critters@0.0.20: + dependencies: + chalk: 4.1.2 + css-select: 5.2.2 + dom-serializer: 2.0.0 + domhandler: 5.0.3 + htmlparser2: 8.0.2 + postcss: 8.5.3 + pretty-bytes: 5.6.0 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + crypto-js@4.2.0: {} + + crypto-random-string@4.0.0: + dependencies: + type-fest: 1.4.0 + + css-blank-pseudo@7.0.1(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 7.1.0 + + css-declaration-sorter@7.2.0(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + + css-declaration-sorter@7.2.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + css-has-pseudo@7.0.2(postcss@8.5.6): + dependencies: + '@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.1.0) + postcss: 8.5.6 + postcss-selector-parser: 7.1.0 + postcss-value-parser: 4.2.0 + + css-loader@6.11.0(@rspack/core@1.4.9(@swc/helpers@0.5.17))(webpack@5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17))): + dependencies: + icss-utils: 5.1.0(postcss@8.5.3) + postcss: 8.5.3 + postcss-modules-extract-imports: 3.1.0(postcss@8.5.3) + postcss-modules-local-by-default: 4.2.0(postcss@8.5.3) + postcss-modules-scope: 3.2.1(postcss@8.5.3) + postcss-modules-values: 4.0.0(postcss@8.5.3) + postcss-value-parser: 4.2.0 + semver: 7.7.2 + optionalDependencies: + '@rspack/core': 1.4.9(@swc/helpers@0.5.17) + webpack: 5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17)) + + css-loader@6.8.1(webpack@5.94.0(@swc/core@1.13.1)(esbuild@0.18.17)): + dependencies: + icss-utils: 5.1.0(postcss@8.5.3) + postcss: 8.5.3 + postcss-modules-extract-imports: 3.1.0(postcss@8.5.3) + postcss-modules-local-by-default: 4.2.0(postcss@8.5.3) + postcss-modules-scope: 3.2.1(postcss@8.5.3) + postcss-modules-values: 4.0.0(postcss@8.5.3) + postcss-value-parser: 4.2.0 + semver: 7.7.2 + webpack: 5.94.0(@swc/core@1.13.1)(esbuild@0.18.17) + + css-minimizer-webpack-plugin@5.0.1(clean-css@5.3.3)(webpack@5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17))): + dependencies: + '@jridgewell/trace-mapping': 0.3.29 + cssnano: 6.1.2(postcss@8.5.3) + jest-worker: 29.7.0 + postcss: 8.5.3 + schema-utils: 4.3.2 + serialize-javascript: 6.0.2 + webpack: 5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17)) + optionalDependencies: + clean-css: 5.3.3 + + css-prefers-color-scheme@10.0.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + css-select@4.3.0: + dependencies: + boolbase: 1.0.0 + css-what: 6.2.2 + domhandler: 4.3.1 + domutils: 2.8.0 + nth-check: 2.1.1 + + css-select@5.2.2: + dependencies: + boolbase: 1.0.0 + css-what: 6.2.2 + domhandler: 5.0.3 + domutils: 3.2.2 + nth-check: 2.1.1 + + css-selector-parser@3.1.3: {} + + css-tree@2.2.1: + dependencies: + mdn-data: 2.0.28 + source-map-js: 1.2.1 + + css-tree@2.3.1: + dependencies: + mdn-data: 2.0.30 + source-map-js: 1.2.1 + + css-what@6.2.2: {} + + css.escape@1.5.1: {} + + cssdb@8.3.1: {} + + cssesc@3.0.0: {} + + cssnano-preset-advanced@6.1.2(postcss@8.5.6): + dependencies: + autoprefixer: 10.4.21(postcss@8.5.6) + browserslist: 4.25.1 + cssnano-preset-default: 6.1.2(postcss@8.5.6) + postcss: 8.5.6 + postcss-discard-unused: 6.0.5(postcss@8.5.6) + postcss-merge-idents: 6.0.3(postcss@8.5.6) + postcss-reduce-idents: 6.0.3(postcss@8.5.6) + postcss-zindex: 6.0.2(postcss@8.5.6) + + cssnano-preset-default@6.1.2(postcss@8.5.3): + dependencies: + browserslist: 4.25.1 + css-declaration-sorter: 7.2.0(postcss@8.5.3) + cssnano-utils: 4.0.2(postcss@8.5.3) + postcss: 8.5.3 + postcss-calc: 9.0.1(postcss@8.5.3) + postcss-colormin: 6.1.0(postcss@8.5.3) + postcss-convert-values: 6.1.0(postcss@8.5.3) + postcss-discard-comments: 6.0.2(postcss@8.5.3) + postcss-discard-duplicates: 6.0.3(postcss@8.5.3) + postcss-discard-empty: 6.0.3(postcss@8.5.3) + postcss-discard-overridden: 6.0.2(postcss@8.5.3) + postcss-merge-longhand: 6.0.5(postcss@8.5.3) + postcss-merge-rules: 6.1.1(postcss@8.5.3) + postcss-minify-font-values: 6.1.0(postcss@8.5.3) + postcss-minify-gradients: 6.0.3(postcss@8.5.3) + postcss-minify-params: 6.1.0(postcss@8.5.3) + postcss-minify-selectors: 6.0.4(postcss@8.5.3) + postcss-normalize-charset: 6.0.2(postcss@8.5.3) + postcss-normalize-display-values: 6.0.2(postcss@8.5.3) + postcss-normalize-positions: 6.0.2(postcss@8.5.3) + postcss-normalize-repeat-style: 6.0.2(postcss@8.5.3) + postcss-normalize-string: 6.0.2(postcss@8.5.3) + postcss-normalize-timing-functions: 6.0.2(postcss@8.5.3) + postcss-normalize-unicode: 6.1.0(postcss@8.5.3) + postcss-normalize-url: 6.0.2(postcss@8.5.3) + postcss-normalize-whitespace: 6.0.2(postcss@8.5.3) + postcss-ordered-values: 6.0.2(postcss@8.5.3) + postcss-reduce-initial: 6.1.0(postcss@8.5.3) + postcss-reduce-transforms: 6.0.2(postcss@8.5.3) + postcss-svgo: 6.0.3(postcss@8.5.3) + postcss-unique-selectors: 6.0.4(postcss@8.5.3) + + cssnano-preset-default@6.1.2(postcss@8.5.6): + dependencies: + browserslist: 4.25.1 + css-declaration-sorter: 7.2.0(postcss@8.5.6) + cssnano-utils: 4.0.2(postcss@8.5.6) + postcss: 8.5.6 + postcss-calc: 9.0.1(postcss@8.5.6) + postcss-colormin: 6.1.0(postcss@8.5.6) + postcss-convert-values: 6.1.0(postcss@8.5.6) + postcss-discard-comments: 6.0.2(postcss@8.5.6) + postcss-discard-duplicates: 6.0.3(postcss@8.5.6) + postcss-discard-empty: 6.0.3(postcss@8.5.6) + postcss-discard-overridden: 6.0.2(postcss@8.5.6) + postcss-merge-longhand: 6.0.5(postcss@8.5.6) + postcss-merge-rules: 6.1.1(postcss@8.5.6) + postcss-minify-font-values: 6.1.0(postcss@8.5.6) + postcss-minify-gradients: 6.0.3(postcss@8.5.6) + postcss-minify-params: 6.1.0(postcss@8.5.6) + postcss-minify-selectors: 6.0.4(postcss@8.5.6) + postcss-normalize-charset: 6.0.2(postcss@8.5.6) + postcss-normalize-display-values: 6.0.2(postcss@8.5.6) + postcss-normalize-positions: 6.0.2(postcss@8.5.6) + postcss-normalize-repeat-style: 6.0.2(postcss@8.5.6) + postcss-normalize-string: 6.0.2(postcss@8.5.6) + postcss-normalize-timing-functions: 6.0.2(postcss@8.5.6) + postcss-normalize-unicode: 6.1.0(postcss@8.5.6) + postcss-normalize-url: 6.0.2(postcss@8.5.6) + postcss-normalize-whitespace: 6.0.2(postcss@8.5.6) + postcss-ordered-values: 6.0.2(postcss@8.5.6) + postcss-reduce-initial: 6.1.0(postcss@8.5.6) + postcss-reduce-transforms: 6.0.2(postcss@8.5.6) + postcss-svgo: 6.0.3(postcss@8.5.6) + postcss-unique-selectors: 6.0.4(postcss@8.5.6) + + cssnano-utils@4.0.2(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + + cssnano-utils@4.0.2(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + cssnano@6.1.2(postcss@8.5.3): + dependencies: + cssnano-preset-default: 6.1.2(postcss@8.5.3) + lilconfig: 3.1.3 + postcss: 8.5.3 + + cssnano@6.1.2(postcss@8.5.6): + dependencies: + cssnano-preset-default: 6.1.2(postcss@8.5.6) + lilconfig: 3.1.3 + postcss: 8.5.6 + + csso@5.0.5: + dependencies: + css-tree: 2.2.1 + + cssom@0.3.8: {} + + cssom@0.4.4: {} + + cssstyle@2.3.0: + dependencies: + cssom: 0.3.8 + + cssstyle@4.6.0: + dependencies: + '@asamuzakjp/css-color': 3.2.0 + rrweb-cssom: 0.8.0 + + csstype@3.1.3: {} + + custom-event@1.0.1: {} + + cypress-wait-until@3.0.2: {} + + cypress@14.5.2: + dependencies: + '@cypress/request': 3.0.8 + '@cypress/xvfb': 1.2.4(supports-color@8.1.1) + '@types/sinonjs__fake-timers': 8.1.1 + '@types/sizzle': 2.3.9 + arch: 2.2.0 + blob-util: 2.0.2 + bluebird: 3.7.2 + buffer: 5.7.1 + cachedir: 2.4.0 + chalk: 4.1.2 + check-more-types: 2.24.0 + ci-info: 4.3.0 + cli-cursor: 3.1.0 + cli-table3: 0.6.1 + commander: 6.2.1 + common-tags: 1.8.2 + dayjs: 1.11.13 + debug: 4.4.1(supports-color@8.1.1) + enquirer: 2.4.1 + eventemitter2: 6.4.7 + execa: 4.1.0 + executable: 4.1.1 + extract-zip: 2.0.1(supports-color@8.1.1) + figures: 3.2.0 + fs-extra: 9.1.0 + getos: 3.2.1 + hasha: 5.2.2 + is-installed-globally: 0.4.0 + lazy-ass: 1.6.0 + listr2: 3.14.0(enquirer@2.4.1) + lodash: 4.17.21 + log-symbols: 4.1.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.7.2 + supports-color: 8.1.1 + tmp: 0.2.3 + tree-kill: 1.2.2 + untildify: 4.0.0 + yauzl: 2.10.0 + + cypress@14.5.3: + dependencies: + '@cypress/request': 3.0.9 + '@cypress/xvfb': 1.2.4(supports-color@8.1.1) + '@types/sinonjs__fake-timers': 8.1.1 + '@types/sizzle': 2.3.9 + arch: 2.2.0 + blob-util: 2.0.2 + bluebird: 3.7.2 + buffer: 5.7.1 + cachedir: 2.4.0 + chalk: 4.1.2 + check-more-types: 2.24.0 + ci-info: 4.3.0 + cli-cursor: 3.1.0 + cli-table3: 0.6.1 + commander: 6.2.1 + common-tags: 1.8.2 + dayjs: 1.11.13 + debug: 4.4.1(supports-color@8.1.1) + enquirer: 2.4.1 + eventemitter2: 6.4.7 + execa: 4.1.0 + executable: 4.1.1 + extract-zip: 2.0.1(supports-color@8.1.1) + figures: 3.2.0 + fs-extra: 9.1.0 + getos: 3.2.1 + hasha: 5.2.2 + is-installed-globally: 0.4.0 + lazy-ass: 1.6.0 + listr2: 3.14.0(enquirer@2.4.1) + lodash: 4.17.21 + log-symbols: 4.1.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.7.2 + supports-color: 8.1.1 + tmp: 0.2.3 + tree-kill: 1.2.2 + untildify: 4.0.0 + yauzl: 2.10.0 + + cytoscape-cose-bilkent@4.1.0(cytoscape@3.32.1): + dependencies: + cose-base: 1.0.3 + cytoscape: 3.32.1 + + cytoscape-fcose@2.2.0(cytoscape@3.32.1): + dependencies: + cose-base: 2.2.0 + cytoscape: 3.32.1 + + cytoscape@3.32.1: {} + + d3-array@2.12.1: + dependencies: + internmap: 1.0.1 + + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-axis@3.0.0: {} + + d3-brush@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3-chord@3.0.1: + dependencies: + d3-path: 3.1.0 + + d3-color@3.1.0: {} + + d3-contour@4.0.2: + dependencies: + d3-array: 3.2.4 + + d3-delaunay@6.0.4: + dependencies: + delaunator: 5.0.1 + + d3-dispatch@3.0.1: {} + + d3-drag@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-selection: 3.0.0 + + d3-dsv@3.0.1: + dependencies: + commander: 7.2.0 + iconv-lite: 0.6.3 + rw: 1.3.3 + + d3-ease@3.0.1: {} + + d3-fetch@3.0.1: + dependencies: + d3-dsv: 3.0.1 + + d3-force@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-quadtree: 3.0.1 + d3-timer: 3.0.1 + + d3-format@3.1.0: {} + + d3-geo@3.1.1: + dependencies: + d3-array: 3.2.4 + + d3-hierarchy@3.1.2: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@1.0.9: {} + + d3-path@3.1.0: {} + + d3-polygon@3.0.1: {} + + d3-quadtree@3.0.1: {} + + d3-random@3.0.1: {} + + d3-sankey@0.12.3: + dependencies: + d3-array: 2.12.1 + d3-shape: 1.3.7 + + d3-scale-chromatic@3.1.0: + dependencies: + d3-color: 3.1.0 + d3-interpolate: 3.0.1 + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.0 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-selection@3.0.0: {} + + d3-shape@1.3.7: + dependencies: + d3-path: 1.0.9 + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@3.0.1: {} + + d3-transition@3.0.1(d3-selection@3.0.0): + dependencies: + d3-color: 3.1.0 + d3-dispatch: 3.0.1 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-timer: 3.0.1 + + d3-zoom@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3@7.9.0: + dependencies: + d3-array: 3.2.4 + d3-axis: 3.0.0 + d3-brush: 3.0.0 + d3-chord: 3.0.1 + d3-color: 3.1.0 + d3-contour: 4.0.2 + d3-delaunay: 6.0.4 + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-dsv: 3.0.1 + d3-ease: 3.0.1 + d3-fetch: 3.0.1 + d3-force: 3.0.0 + d3-format: 3.1.0 + d3-geo: 3.1.1 + d3-hierarchy: 3.1.2 + d3-interpolate: 3.0.1 + d3-path: 3.1.0 + d3-polygon: 3.0.1 + d3-quadtree: 3.0.1 + d3-random: 3.0.1 + d3-scale: 4.0.2 + d3-scale-chromatic: 3.1.0 + d3-selection: 3.0.0 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + d3-timer: 3.0.1 + d3-transition: 3.0.1(d3-selection@3.0.0) + d3-zoom: 3.0.0 + + dagre-d3-es@7.0.11: + dependencies: + d3: 7.9.0 + lodash-es: 4.17.21 + + damerau-levenshtein@1.0.8: {} + + dashdash@1.14.1: + dependencies: + assert-plus: 1.0.0 + + data-uri-to-buffer@4.0.1: {} + + data-uri-to-buffer@6.0.2: + optional: true + + data-urls@2.0.0: + dependencies: + abab: 2.0.6 + whatwg-mimetype: 2.3.0 + whatwg-url: 8.7.0 + + data-urls@5.0.0: + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + + data-view-buffer@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-offset@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + date-format@4.0.14: {} + + dateformat@4.6.3: {} + + dayjs@1.11.13: {} + + debounce@1.2.1: {} + + debug@2.6.9: + dependencies: + ms: 2.0.0 + + debug@3.2.7(supports-color@8.1.1): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 8.1.1 + + debug@4.3.7: + dependencies: + ms: 2.1.3 + + debug@4.4.1(supports-color@5.5.0): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 5.5.0 + + debug@4.4.1(supports-color@8.1.1): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 8.1.1 + + decamelize@1.2.0: {} + + decamelize@4.0.0: {} + + decimal.js@10.6.0: {} + + decode-named-character-reference@1.2.0: + dependencies: + character-entities: 2.0.2 + + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + + deep-eql@5.0.2: {} + + deep-extend@0.6.0: {} + + deep-is@0.1.4: {} + + deepmerge@4.3.1: {} + + default-gateway@6.0.3: + dependencies: + execa: 5.1.1 + + defaults@1.0.4: + dependencies: + clone: 1.0.4 + + defer-to-connect@2.0.1: {} + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-lazy-prop@2.0.0: {} + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + degenerator@5.0.1: + dependencies: + ast-types: 0.13.4 + escodegen: 2.1.0 + esprima: 4.0.1 + optional: true + + delaunator@5.0.1: + dependencies: + robust-predicates: 3.0.2 + + delayed-stream@1.0.0: {} + + delegates@1.0.0: {} + + depd@1.1.2: {} + + depd@2.0.0: {} + + dequal@2.0.3: {} + + destroy@1.2.0: {} + + detect-indent@6.1.0: {} + + detect-libc@1.0.3: {} + + detect-libc@2.0.4: {} + + detect-node-es@1.1.0: {} + + detect-node@2.1.0: {} + + detect-package-manager@3.0.2: + dependencies: + execa: 5.1.1 + + detect-port@1.6.1: + dependencies: + address: 1.2.2 + debug: 4.4.1(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + + devtools-protocol@0.0.1312386: + optional: true + + di@0.0.1: {} + + diacritics@1.3.0: {} + + didyoumean@1.2.2: {} + + diff@5.2.0: {} + + diff@7.0.0: {} + + dijkstrajs@1.0.3: {} + + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + direction@2.0.1: {} + + dlv@1.1.3: {} + + dns-packet@5.6.1: + dependencies: + '@leichtgewicht/ip-codec': 2.0.5 + + doctrine@2.1.0: + dependencies: + esutils: 2.0.3 + + doctrine@3.0.0: + dependencies: + esutils: 2.0.3 + + docusaurus-plugin-image-zoom@3.0.1(@docusaurus/theme-classic@3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(@types/react@19.1.2)(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3)): + dependencies: + '@docusaurus/theme-classic': 3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(@types/react@19.1.2)(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3) + medium-zoom: 1.1.0 + validate-peer-dependencies: 2.2.0 + + docusaurus-plugin-openapi-docs@4.4.0(@docusaurus/plugin-content-docs@3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3))(@docusaurus/utils-validation@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@docusaurus/utils@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(encoding@0.1.13)(react@18.3.1): + dependencies: + '@apidevtools/json-schema-ref-parser': 11.9.3 + '@docusaurus/plugin-content-docs': 3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3) + '@docusaurus/utils': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-validation': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@redocly/openapi-core': 1.34.3 + allof-merge: 0.6.6 + chalk: 4.1.2 + clsx: 1.2.1 + fs-extra: 9.1.0 + json-pointer: 0.6.2 + json5: 2.2.3 + lodash: 4.17.21 + mustache: 4.2.0 + openapi-to-postmanv2: 4.25.0(encoding@0.1.13) + postman-collection: 4.5.0 + react: 18.3.1 + slugify: 1.6.6 + swagger2openapi: 7.0.8(encoding@0.1.13) + xml-formatter: 2.6.1 + transitivePeerDependencies: + - encoding + - supports-color + + docusaurus-plugin-sass@0.2.6(@docusaurus/core@3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(sass@1.89.2)(webpack@5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17))): + dependencies: + '@docusaurus/core': 3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3) + sass: 1.89.2 + sass-loader: 16.0.5(@rspack/core@1.4.9(@swc/helpers@0.5.17))(sass@1.89.2)(webpack@5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17))) + transitivePeerDependencies: + - '@rspack/core' + - node-sass + - sass-embedded + - webpack + + docusaurus-theme-github-codeblock@2.0.2(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@docusaurus/types': 3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + transitivePeerDependencies: + - '@swc/core' + - acorn + - esbuild + - react + - react-dom + - supports-color + - uglify-js + - webpack-cli + + ? docusaurus-theme-openapi-docs@4.4.0(@docusaurus/theme-common@3.8.1(@docusaurus/plugin-content-docs@3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@types/react@19.1.2)(docusaurus-plugin-openapi-docs@4.4.0(@docusaurus/plugin-content-docs@3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3))(@docusaurus/utils-validation@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@docusaurus/utils@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(encoding@0.1.13)(react@18.3.1))(docusaurus-plugin-sass@0.2.6(@docusaurus/core@3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(sass@1.89.2)(webpack@5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17))))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(webpack@5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17))) + : dependencies: + '@docusaurus/theme-common': 3.8.1(@docusaurus/plugin-content-docs@3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@hookform/error-message': 2.0.1(react-dom@18.3.1(react@18.3.1))(react-hook-form@7.60.0(react@18.3.1))(react@18.3.1) + '@reduxjs/toolkit': 1.9.7(react-redux@7.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + allof-merge: 0.6.6 + buffer: 6.0.3 + clsx: 1.2.1 + copy-text-to-clipboard: 3.2.0 + crypto-js: 4.2.0 + docusaurus-plugin-openapi-docs: 4.4.0(@docusaurus/plugin-content-docs@3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3))(@docusaurus/utils-validation@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@docusaurus/utils@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(encoding@0.1.13)(react@18.3.1) + docusaurus-plugin-sass: 0.2.6(@docusaurus/core@3.8.1(@docusaurus/faster@3.8.1(@docusaurus/types@3.8.1(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@swc/helpers@0.5.17))(@mdx-js/react@3.1.0(@types/react@19.1.2)(react@18.3.1))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(@swc/core@1.13.1(@swc/helpers@0.5.17))(acorn@8.15.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3))(@rspack/core@1.4.9(@swc/helpers@0.5.17))(sass@1.89.2)(webpack@5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17))) + file-saver: 2.0.5 + lodash: 4.17.21 + pako: 2.1.0 + postman-code-generators: 1.14.2 + postman-collection: 4.5.0 + prism-react-renderer: 2.4.1(react@18.3.1) + process: 0.11.10 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-hook-form: 7.60.0(react@18.3.1) + react-live: 4.1.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-magic-dropzone: 1.0.1 + react-markdown: 8.0.7(@types/react@19.1.2)(react@18.3.1) + react-modal: 3.16.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-redux: 7.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rehype-raw: 6.1.1 + remark-gfm: 3.0.1 + sass: 1.89.2 + sass-loader: 16.0.5(@rspack/core@1.4.9(@swc/helpers@0.5.17))(sass@1.89.2)(webpack@5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17))) + unist-util-visit: 5.0.0 + url: 0.11.4 + xml-formatter: 2.6.1 + transitivePeerDependencies: + - '@rspack/core' + - '@types/react' + - node-sass + - react-native + - sass-embedded + - supports-color + - webpack + + dom-accessibility-api@0.5.16: {} + + dom-accessibility-api@0.6.3: {} + + dom-converter@0.2.0: + dependencies: + utila: 0.4.0 + + dom-serialize@2.2.1: + dependencies: + custom-event: 1.0.1 + ent: 2.2.2 + extend: 3.0.2 + void-elements: 2.0.1 + + dom-serializer@1.4.1: + dependencies: + domelementtype: 2.3.0 + domhandler: 4.3.1 + entities: 2.2.0 + + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + domelementtype@2.3.0: {} + + domexception@2.0.1: + dependencies: + webidl-conversions: 5.0.0 + + domhandler@4.3.1: + dependencies: + domelementtype: 2.3.0 + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + dompurify@3.2.6: + optionalDependencies: + '@types/trusted-types': 2.0.7 + + domutils@2.8.0: + dependencies: + dom-serializer: 1.4.1 + domelementtype: 2.3.0 + domhandler: 4.3.1 + + domutils@3.2.2: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + + dot-case@3.0.4: + dependencies: + no-case: 3.0.4 + tslib: 2.8.1 + + dot-prop@6.0.1: + dependencies: + is-obj: 2.0.0 + + dotenv-cli@8.0.0: + dependencies: + cross-spawn: 7.0.6 + dotenv: 16.6.1 + dotenv-expand: 10.0.0 + minimist: 1.2.8 + + dotenv-expand@10.0.0: {} + + dotenv@10.0.0: {} + + dotenv@16.6.1: {} + + dprint-node@1.0.8: + dependencies: + detect-libc: 1.0.3 + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + duplexer@0.1.2: {} + + eastasianwidth@0.2.0: {} + + ecc-jsbn@0.1.2: + dependencies: + jsbn: 0.1.1 + safer-buffer: 2.1.2 + + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + + ee-first@1.1.1: {} + + ejs@3.1.10: + dependencies: + jake: 10.9.2 + + electron-to-chromium@1.5.189: {} + + emoji-regex@10.4.0: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + emojilib@2.4.0: {} + + emojis-list@3.0.0: {} + + emoticon@4.1.0: {} + + encode-utf8@1.0.3: {} + + encodeurl@1.0.2: {} + + encodeurl@2.0.0: {} + + encoding@0.1.13: + dependencies: + iconv-lite: 0.6.3 + optional: true + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + + engine.io-parser@5.2.3: {} + + engine.io@6.6.4: + dependencies: + '@types/cors': 2.8.19 + '@types/node': 22.16.5 + accepts: 1.3.8 + base64id: 2.0.0 + cookie: 0.7.2 + cors: 2.8.5 + debug: 4.3.7 + engine.io-parser: 5.2.3 + ws: 8.17.1 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + enhanced-resolve@5.18.2: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.2.2 + + enquirer@2.3.6: + dependencies: + ansi-colors: 4.1.3 + + enquirer@2.4.1: + dependencies: + ansi-colors: 4.1.3 + strip-ansi: 6.0.1 + + ent@2.2.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + punycode: 1.4.1 + safe-regex-test: 1.1.0 + + entities@2.2.0: {} + + entities@4.5.0: {} + + entities@6.0.1: {} + + env-cmd@10.1.0: + dependencies: + commander: 4.1.1 + cross-spawn: 7.0.6 + + env-paths@2.2.1: {} + + environment@1.1.0: {} + + err-code@2.0.3: {} + + errno@0.1.8: + dependencies: + prr: 1.0.1 + optional: true + + error-ex@1.3.2: + dependencies: + is-arrayish: 0.2.1 + + es-abstract@1.24.0: + dependencies: + array-buffer-byte-length: 1.0.2 + arraybuffer.prototype.slice: 1.0.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + data-view-buffer: 1.0.2 + data-view-byte-length: 1.0.2 + data-view-byte-offset: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-set-tostringtag: 2.1.0 + es-to-primitive: 1.3.0 + function.prototype.name: 1.1.8 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + get-symbol-description: 1.1.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + internal-slot: 1.1.0 + is-array-buffer: 3.0.5 + is-callable: 1.2.7 + is-data-view: 1.0.2 + is-negative-zero: 2.0.3 + is-regex: 1.2.1 + is-set: 2.0.3 + is-shared-array-buffer: 1.0.4 + is-string: 1.1.1 + is-typed-array: 1.1.15 + is-weakref: 1.1.1 + math-intrinsics: 1.1.0 + object-inspect: 1.13.4 + object-keys: 1.1.1 + object.assign: 4.1.7 + own-keys: 1.0.1 + regexp.prototype.flags: 1.5.4 + safe-array-concat: 1.1.3 + safe-push-apply: 1.0.0 + safe-regex-test: 1.1.0 + set-proto: 1.0.0 + stop-iteration-iterator: 1.1.0 + string.prototype.trim: 1.2.10 + string.prototype.trimend: 1.0.9 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.3 + typed-array-byte-length: 1.0.3 + typed-array-byte-offset: 1.0.4 + typed-array-length: 1.0.7 + unbox-primitive: 1.1.0 + which-typed-array: 1.1.19 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-iterator-helpers@1.2.1: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + es-set-tostringtag: 2.1.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + iterator.prototype: 1.1.5 + safe-array-concat: 1.1.3 + + es-module-lexer@1.7.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + es-shim-unscopables@1.1.0: + dependencies: + hasown: 2.0.2 + + es-to-primitive@1.3.0: + dependencies: + is-callable: 1.2.7 + is-date-object: 1.1.0 + is-symbol: 1.1.1 + + es6-promise@3.3.1: {} + + esast-util-from-estree@2.0.0: + dependencies: + '@types/estree-jsx': 1.0.5 + devlop: 1.1.0 + estree-util-visit: 2.0.0 + unist-util-position-from-estree: 2.0.0 + + esast-util-from-js@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + acorn: 8.15.0 + esast-util-from-estree: 2.0.0 + vfile-message: 4.0.2 + + esbuild-wasm@0.18.17: {} + + esbuild@0.18.17: + optionalDependencies: + '@esbuild/android-arm': 0.18.17 + '@esbuild/android-arm64': 0.18.17 + '@esbuild/android-x64': 0.18.17 + '@esbuild/darwin-arm64': 0.18.17 + '@esbuild/darwin-x64': 0.18.17 + '@esbuild/freebsd-arm64': 0.18.17 + '@esbuild/freebsd-x64': 0.18.17 + '@esbuild/linux-arm': 0.18.17 + '@esbuild/linux-arm64': 0.18.17 + '@esbuild/linux-ia32': 0.18.17 + '@esbuild/linux-loong64': 0.18.17 + '@esbuild/linux-mips64el': 0.18.17 + '@esbuild/linux-ppc64': 0.18.17 + '@esbuild/linux-riscv64': 0.18.17 + '@esbuild/linux-s390x': 0.18.17 + '@esbuild/linux-x64': 0.18.17 + '@esbuild/netbsd-x64': 0.18.17 + '@esbuild/openbsd-x64': 0.18.17 + '@esbuild/sunos-x64': 0.18.17 + '@esbuild/win32-arm64': 0.18.17 + '@esbuild/win32-ia32': 0.18.17 + '@esbuild/win32-x64': 0.18.17 + + esbuild@0.21.5: + optionalDependencies: + '@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 + + esbuild@0.25.8: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.8 + '@esbuild/android-arm': 0.25.8 + '@esbuild/android-arm64': 0.25.8 + '@esbuild/android-x64': 0.25.8 + '@esbuild/darwin-arm64': 0.25.8 + '@esbuild/darwin-x64': 0.25.8 + '@esbuild/freebsd-arm64': 0.25.8 + '@esbuild/freebsd-x64': 0.25.8 + '@esbuild/linux-arm': 0.25.8 + '@esbuild/linux-arm64': 0.25.8 + '@esbuild/linux-ia32': 0.25.8 + '@esbuild/linux-loong64': 0.25.8 + '@esbuild/linux-mips64el': 0.25.8 + '@esbuild/linux-ppc64': 0.25.8 + '@esbuild/linux-riscv64': 0.25.8 + '@esbuild/linux-s390x': 0.25.8 + '@esbuild/linux-x64': 0.25.8 + '@esbuild/netbsd-arm64': 0.25.8 + '@esbuild/netbsd-x64': 0.25.8 + '@esbuild/openbsd-arm64': 0.25.8 + '@esbuild/openbsd-x64': 0.25.8 + '@esbuild/openharmony-arm64': 0.25.8 + '@esbuild/sunos-x64': 0.25.8 + '@esbuild/win32-arm64': 0.25.8 + '@esbuild/win32-ia32': 0.25.8 + '@esbuild/win32-x64': 0.25.8 + + escalade@3.2.0: {} + + escape-goat@4.0.0: {} + + escape-html@1.0.3: {} + + escape-string-regexp@1.0.5: {} + + escape-string-regexp@4.0.0: {} + + escape-string-regexp@5.0.0: {} + + escodegen@2.1.0: + dependencies: + esprima: 4.0.1 + estraverse: 5.3.0 + esutils: 2.0.3 + optionalDependencies: + source-map: 0.6.1 + + eslint-config-next@15.4.0-canary.86(eslint@8.57.1)(typescript@5.8.3): + dependencies: + '@next/eslint-plugin-next': 15.4.0-canary.86 + '@rushstack/eslint-patch': 1.12.0 + '@typescript-eslint/eslint-plugin': 8.38.0(@typescript-eslint/parser@8.38.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(typescript@5.8.3) + '@typescript-eslint/parser': 8.38.0(eslint@8.57.1)(typescript@5.8.3) + eslint: 8.57.1 + eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.38.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) + eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1) + eslint-plugin-react: 7.37.5(eslint@8.57.1) + eslint-plugin-react-hooks: 5.2.0(eslint@8.57.1) + optionalDependencies: + typescript: 5.8.3 + transitivePeerDependencies: + - eslint-import-resolver-webpack + - eslint-plugin-import-x + - supports-color + + eslint-config-prettier@9.1.2(eslint@8.57.1): + dependencies: + eslint: 8.57.1 + + eslint-import-resolver-node@0.3.9: + dependencies: + debug: 3.2.7(supports-color@8.1.1) + is-core-module: 2.16.1 + resolve: 1.22.10 + transitivePeerDependencies: + - supports-color + + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1): + dependencies: + '@nolyfill/is-core-module': 1.0.39 + debug: 4.4.1(supports-color@5.5.0) + eslint: 8.57.1 + get-tsconfig: 4.10.1 + is-bun-module: 2.0.0 + stable-hash: 0.0.5 + tinyglobby: 0.2.14 + unrs-resolver: 1.11.1 + optionalDependencies: + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.38.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) + transitivePeerDependencies: + - supports-color + + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.38.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1))(eslint@8.57.1): + dependencies: + debug: 3.2.7(supports-color@8.1.1) + optionalDependencies: + '@typescript-eslint/parser': 8.38.0(eslint@8.57.1)(typescript@5.8.3) + eslint: 8.57.1 + eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1) + transitivePeerDependencies: + - supports-color + + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.38.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1): + dependencies: + '@rtsao/scc': 1.1.0 + array-includes: 3.1.9 + array.prototype.findlastindex: 1.2.6 + array.prototype.flat: 1.3.3 + array.prototype.flatmap: 1.3.3 + debug: 3.2.7(supports-color@8.1.1) + doctrine: 2.1.0 + eslint: 8.57.1 + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.38.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1))(eslint@8.57.1) + hasown: 2.0.2 + is-core-module: 2.16.1 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.1 + semver: 6.3.1 + string.prototype.trimend: 1.0.9 + tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 8.38.0(eslint@8.57.1)(typescript@5.8.3) + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + + eslint-plugin-jsx-a11y@6.10.2(eslint@8.57.1): + dependencies: + aria-query: 5.3.2 + array-includes: 3.1.9 + array.prototype.flatmap: 1.3.3 + ast-types-flow: 0.0.8 + axe-core: 4.10.3 + axobject-query: 4.1.0 + damerau-levenshtein: 1.0.8 + emoji-regex: 9.2.2 + eslint: 8.57.1 + hasown: 2.0.2 + jsx-ast-utils: 3.3.5 + language-tags: 1.0.9 + minimatch: 3.1.2 + object.fromentries: 2.0.8 + safe-regex-test: 1.1.0 + string.prototype.includes: 2.0.1 + + eslint-plugin-react-hooks@5.2.0(eslint@8.57.1): + dependencies: + eslint: 8.57.1 + + eslint-plugin-react@7.37.5(eslint@8.57.1): + dependencies: + array-includes: 3.1.9 + array.prototype.findlast: 1.2.5 + array.prototype.flatmap: 1.3.3 + array.prototype.tosorted: 1.1.4 + doctrine: 2.1.0 + es-iterator-helpers: 1.2.1 + eslint: 8.57.1 + estraverse: 5.3.0 + hasown: 2.0.2 + jsx-ast-utils: 3.3.5 + minimatch: 3.1.2 + object.entries: 1.1.9 + object.fromentries: 2.0.8 + object.values: 1.2.1 + prop-types: 15.8.1 + resolve: 2.0.0-next.5 + semver: 6.3.1 + string.prototype.matchall: 4.0.12 + string.prototype.repeat: 1.0.0 + + eslint-scope@5.1.1: + dependencies: + esrecurse: 4.3.0 + estraverse: 4.3.0 + + eslint-scope@7.2.2: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@2.1.0: {} + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint@8.57.1: + dependencies: + '@eslint-community/eslint-utils': 4.7.0(eslint@8.57.1) + '@eslint-community/regexpp': 4.12.1 + '@eslint/eslintrc': 2.1.4 + '@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.3.0 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.1(supports-color@5.5.0) + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.24.0 + graphemer: 1.4.0 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-yaml: 4.1.0 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + + espree@9.6.1: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 3.4.3 + + esprima@4.0.1: {} + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@4.3.0: {} + + estraverse@5.3.0: {} + + estree-util-attach-comments@2.1.1: + dependencies: + '@types/estree': 1.0.8 + optional: true + + estree-util-attach-comments@3.0.0: + dependencies: + '@types/estree': 1.0.8 + + estree-util-build-jsx@3.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + estree-walker: 3.0.3 + + estree-util-is-identifier-name@2.1.0: + optional: true + + estree-util-is-identifier-name@3.0.0: {} + + estree-util-scope@1.0.0: + dependencies: + '@types/estree': 1.0.8 + devlop: 1.1.0 + + estree-util-to-js@1.2.0: + dependencies: + '@types/estree-jsx': 1.0.5 + astring: 1.9.0 + source-map: 0.7.4 + optional: true + + estree-util-to-js@2.0.0: + dependencies: + '@types/estree-jsx': 1.0.5 + astring: 1.9.0 + source-map: 0.7.4 + + estree-util-value-to-estree@3.4.0: + dependencies: + '@types/estree': 1.0.8 + + estree-util-visit@1.2.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/unist': 2.0.11 + optional: true + + estree-util-visit@2.0.0: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/unist': 3.0.3 + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + esutils@2.0.3: {} + + eta@2.2.0: {} + + etag@1.8.1: {} + + eval@0.1.8: + dependencies: + '@types/node': 22.16.5 + require-like: 0.1.2 + + event-stream@3.3.4: + dependencies: + duplexer: 0.1.2 + from: 0.1.7 + map-stream: 0.1.0 + pause-stream: 0.0.11 + split: 0.3.3 + stream-combiner: 0.0.4 + through: 2.3.8 + + event-target-shim@5.0.1: {} + + eventemitter-asyncresource@1.0.0: {} + + eventemitter2@6.4.7: {} + + eventemitter3@4.0.7: {} + + eventemitter3@5.0.1: {} + + events@3.3.0: {} + + execa@4.1.0: + dependencies: + cross-spawn: 7.0.6 + get-stream: 5.2.0 + human-signals: 1.1.1 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + + execa@5.1.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + + execa@8.0.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 8.0.1 + human-signals: 5.0.0 + is-stream: 3.0.0 + merge-stream: 2.0.0 + npm-run-path: 5.3.0 + onetime: 6.0.0 + signal-exit: 4.1.0 + strip-final-newline: 3.0.0 + + executable@4.1.1: + dependencies: + pify: 2.3.0 + + exenv@1.2.2: {} + + expect-type@1.2.2: {} + + exponential-backoff@3.1.2: {} + + express@4.21.2: + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.3 + content-disposition: 0.5.4 + content-type: 1.0.5 + 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.12 + 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 + transitivePeerDependencies: + - supports-color + + exsolve@1.0.7: {} + + extend-shallow@2.0.1: + dependencies: + is-extendable: 0.1.1 + + extend@3.0.2: {} + + extendable-error@0.1.7: {} + + external-editor@3.1.0: + dependencies: + chardet: 0.7.0 + iconv-lite: 0.4.24 + tmp: 0.0.33 + + extract-zip@2.0.1: + dependencies: + debug: 4.4.1(supports-color@5.5.0) + get-stream: 5.2.0 + yauzl: 2.10.0 + optionalDependencies: + '@types/yauzl': 2.10.3 + transitivePeerDependencies: + - supports-color + optional: true + + extract-zip@2.0.1(supports-color@8.1.1): + dependencies: + debug: 4.4.1(supports-color@8.1.1) + get-stream: 5.2.0 + yauzl: 2.10.0 + optionalDependencies: + '@types/yauzl': 2.10.3 + transitivePeerDependencies: + - supports-color + + extsprintf@1.3.0: {} + + fast-deep-equal@3.1.3: {} + + fast-fifo@1.3.2: + optional: true + + fast-glob@3.2.7: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-glob@3.3.1: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fast-safe-stringify@2.1.1: {} + + fast-uri@3.0.6: {} + + fastq@1.19.1: + dependencies: + reusify: 1.1.0 + + fault@2.0.1: + dependencies: + format: 0.2.2 + + faye-websocket@0.11.4: + dependencies: + websocket-driver: 0.7.4 + + fd-package-json@2.0.0: + dependencies: + walk-up-path: 4.0.0 + + fd-slicer@1.1.0: + dependencies: + pend: 1.2.0 + + fdir@6.4.6(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + feed@4.2.2: + dependencies: + xml-js: 1.6.11 + + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + + fflate@0.4.8: {} + + fflate@0.8.2: {} + + figures@3.2.0: + dependencies: + escape-string-regexp: 1.0.5 + + file-entry-cache@6.0.1: + dependencies: + flat-cache: 3.2.0 + + file-loader@6.2.0(webpack@5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17))): + dependencies: + loader-utils: 2.0.4 + schema-utils: 3.3.0 + webpack: 5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17)) + + file-saver@2.0.5: {} + + file-type@3.9.0: {} + + filelist@1.0.4: + dependencies: + minimatch: 5.1.6 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + filter-obj@5.1.0: {} + + finalhandler@1.1.2: + dependencies: + debug: 2.6.9 + encodeurl: 1.0.2 + escape-html: 1.0.3 + on-finished: 2.3.0 + parseurl: 1.3.3 + statuses: 1.5.0 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + finalhandler@1.3.1: + 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 + transitivePeerDependencies: + - supports-color + + find-cache-dir@4.0.0: + dependencies: + common-path-prefix: 3.0.0 + pkg-dir: 7.0.0 + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + find-up@6.3.0: + dependencies: + locate-path: 7.2.0 + path-exists: 5.0.0 + + fix-dts-default-cjs-exports@1.0.1: + dependencies: + magic-string: 0.30.17 + mlly: 1.7.4 + rollup: 4.45.1 + + flag-icons@7.5.0: {} + + flat-cache@3.2.0: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + rimraf: 3.0.2 + + flat@5.0.2: {} + + flatted@3.3.3: {} + + follow-redirects@1.15.9(debug@4.4.1): + optionalDependencies: + debug: 4.4.1(supports-color@5.5.0) + + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + + foreach@2.0.6: {} + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + forever-agent@0.6.1: {} + + form-data-encoder@1.7.2: {} + + form-data-encoder@2.1.4: {} + + form-data@3.0.4: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + + form-data@4.0.4: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + + format@0.2.2: {} + + formatly@0.2.4: + dependencies: + fd-package-json: 2.0.0 + + formdata-node@4.4.1: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 4.0.0-beta.3 + + formdata-polyfill@4.0.10: + dependencies: + fetch-blob: 3.2.0 + + forwarded@0.2.0: {} + + fraction.js@4.3.7: {} + + fresh@0.5.2: {} + + from@0.1.7: {} + + fs-constants@1.0.0: {} + + fs-extra@10.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + + fs-extra@11.3.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + + fs-extra@7.0.1: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + fs-extra@8.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + fs-extra@9.1.0: + dependencies: + at-least-node: 1.0.0 + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + + fs-minipass@2.1.0: + dependencies: + minipass: 3.3.6 + + fs-minipass@3.0.3: + dependencies: + minipass: 7.1.2 + + fs-monkey@1.1.0: {} + + fs.realpath@1.0.0: {} + + fsevents@2.3.2: + optional: true + + fsevents@2.3.3: + optional: true + + fsu@1.1.1: {} + + function-bind@1.1.2: {} + + function.prototype.name@1.1.8: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + functions-have-names: 1.2.3 + hasown: 2.0.2 + is-callable: 1.2.7 + + functions-have-names@1.2.3: {} + + gauge@3.0.2: + dependencies: + aproba: 2.1.0 + color-support: 1.1.3 + console-control-strings: 1.1.0 + has-unicode: 2.0.1 + object-assign: 4.1.1 + signal-exit: 3.0.7 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wide-align: 1.1.5 + + gauge@4.0.4: + dependencies: + aproba: 2.1.0 + color-support: 1.1.3 + console-control-strings: 1.1.0 + has-unicode: 2.0.1 + signal-exit: 3.0.7 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wide-align: 1.1.5 + + gaxios@7.1.1: + dependencies: + extend: 3.0.2 + https-proxy-agent: 7.0.6 + node-fetch: 3.3.2 + transitivePeerDependencies: + - supports-color + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-east-asian-width@1.3.0: {} + + get-intrinsic@1.3.0: + 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 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-nonce@1.0.1: {} + + get-own-enumerable-property-symbols@3.0.2: {} + + get-package-type@0.1.0: {} + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-stream@5.2.0: + dependencies: + pump: 3.0.3 + + get-stream@6.0.1: {} + + get-stream@8.0.1: {} + + get-symbol-description@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + + get-tsconfig@4.10.1: + dependencies: + resolve-pkg-maps: 1.0.0 + + get-uri@6.0.5: + dependencies: + basic-ftp: 5.0.5 + data-uri-to-buffer: 6.0.2 + debug: 4.4.1(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + optional: true + + getos@3.2.1: + dependencies: + async: 3.2.6 + + getpass@0.1.7: + dependencies: + assert-plus: 1.0.0 + + github-slugger@1.5.0: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob-to-regexp@0.4.1: {} + + glob@10.4.5: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + glob@11.0.3: + dependencies: + foreground-child: 3.3.1 + jackspeak: 4.1.1 + minimatch: 10.0.3 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 2.0.0 + + glob@7.1.4: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + glob@8.1.0: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 5.1.6 + once: 1.4.0 + + global-dirs@3.0.1: + dependencies: + ini: 2.0.0 + + globals@13.24.0: + dependencies: + type-fest: 0.20.2 + + globals@15.15.0: {} + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + + globby@13.2.2: + dependencies: + dir-glob: 3.0.1 + fast-glob: 3.3.1 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 4.0.0 + + globrex@0.1.2: {} + + google-protobuf@3.21.4: {} + + gopd@1.2.0: {} + + got@12.6.1: + dependencies: + '@sindresorhus/is': 5.6.0 + '@szmarczak/http-timer': 5.0.1 + cacheable-lookup: 7.0.0 + cacheable-request: 10.2.14 + decompress-response: 6.0.0 + form-data-encoder: 2.1.4 + get-stream: 6.0.1 + http2-wrapper: 2.2.1 + lowercase-keys: 3.0.0 + p-cancelable: 3.0.0 + responselike: 3.0.0 + + graceful-fs@4.2.10: {} + + graceful-fs@4.2.11: {} + + graphemer@1.4.0: {} + + graphlib@2.1.8: + dependencies: + lodash: 4.17.21 + + gray-matter@4.0.3: + dependencies: + js-yaml: 3.14.1 + kind-of: 6.0.3 + section-matter: 1.0.0 + strip-bom-string: 1.0.0 + + grpc-tools@1.13.0(encoding@0.1.13): + dependencies: + '@mapbox/node-pre-gyp': 1.0.11(encoding@0.1.13) + transitivePeerDependencies: + - encoding + - supports-color + + grpc-web@1.5.0: {} + + guess-parser@0.4.22(typescript@5.1.6): + dependencies: + '@wessberg/ts-evaluator': 0.0.27(typescript@5.1.6) + typescript: 5.1.6 + transitivePeerDependencies: + - bufferutil + - canvas + - supports-color + - utf-8-validate + + gzip-size@6.0.0: + dependencies: + duplexer: 0.1.2 + + hachure-fill@0.5.2: {} + + handle-thing@2.0.1: {} + + has-bigints@1.1.0: {} + + has-flag@3.0.0: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-proto@1.2.0: + dependencies: + dunder-proto: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + has-unicode@2.0.1: {} + + has-yarn@3.0.0: {} + + hasha@5.2.2: + dependencies: + is-stream: 2.0.1 + type-fest: 0.8.1 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + hast-util-embedded@3.0.0: + dependencies: + '@types/hast': 3.0.4 + hast-util-is-element: 3.0.0 + + hast-util-from-html@1.0.2: + dependencies: + '@types/hast': 2.3.10 + hast-util-from-parse5: 7.1.2 + parse5: 7.3.0 + vfile: 5.3.7 + vfile-message: 3.1.4 + optional: true + + hast-util-from-html@2.0.3: + dependencies: + '@types/hast': 3.0.4 + devlop: 1.1.0 + hast-util-from-parse5: 8.0.3 + parse5: 7.3.0 + vfile: 6.0.3 + vfile-message: 4.0.2 + + hast-util-from-parse5@7.1.2: + dependencies: + '@types/hast': 2.3.10 + '@types/unist': 2.0.11 + hastscript: 7.2.0 + property-information: 6.5.0 + vfile: 5.3.7 + vfile-location: 4.1.0 + web-namespaces: 2.0.1 + + hast-util-from-parse5@8.0.3: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + devlop: 1.1.0 + hastscript: 9.0.1 + property-information: 7.1.0 + vfile: 6.0.3 + vfile-location: 5.0.3 + web-namespaces: 2.0.1 + + hast-util-has-property@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + hast-util-is-body-ok-link@3.0.1: + dependencies: + '@types/hast': 3.0.4 + + hast-util-is-element@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + hast-util-minify-whitespace@1.0.1: + dependencies: + '@types/hast': 3.0.4 + hast-util-embedded: 3.0.0 + hast-util-is-element: 3.0.0 + hast-util-whitespace: 3.0.0 + unist-util-is: 6.0.0 + + hast-util-parse-selector@3.1.1: + dependencies: + '@types/hast': 2.3.10 + + hast-util-parse-selector@4.0.0: + dependencies: + '@types/hast': 3.0.4 + + hast-util-phrasing@3.0.1: + dependencies: + '@types/hast': 3.0.4 + hast-util-embedded: 3.0.0 + hast-util-has-property: 3.0.0 + hast-util-is-body-ok-link: 3.0.1 + hast-util-is-element: 3.0.0 + + hast-util-raw@7.2.3: + dependencies: + '@types/hast': 2.3.10 + '@types/parse5': 6.0.3 + hast-util-from-parse5: 7.1.2 + hast-util-to-parse5: 7.1.0 + html-void-elements: 2.0.1 + parse5: 6.0.1 + unist-util-position: 4.0.4 + unist-util-visit: 4.1.2 + vfile: 5.3.7 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + + hast-util-raw@9.1.0: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + '@ungap/structured-clone': 1.3.0 + hast-util-from-parse5: 8.0.3 + hast-util-to-parse5: 8.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.0 + parse5: 7.3.0 + unist-util-position: 5.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + + hast-util-select@6.0.4: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + bcp-47-match: 2.0.3 + comma-separated-tokens: 2.0.3 + css-selector-parser: 3.1.3 + devlop: 1.1.0 + direction: 2.0.1 + hast-util-has-property: 3.0.0 + hast-util-to-string: 3.0.1 + hast-util-whitespace: 3.0.0 + nth-check: 2.1.1 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + unist-util-visit: 5.0.0 + zwitch: 2.0.4 + + hast-util-to-estree@2.3.3: + dependencies: + '@types/estree': 1.0.8 + '@types/estree-jsx': 1.0.5 + '@types/hast': 2.3.10 + '@types/unist': 2.0.11 + comma-separated-tokens: 2.0.3 + estree-util-attach-comments: 2.1.1 + estree-util-is-identifier-name: 2.1.0 + hast-util-whitespace: 2.0.1 + mdast-util-mdx-expression: 1.3.2 + mdast-util-mdxjs-esm: 1.3.1 + property-information: 6.5.0 + space-separated-tokens: 2.0.2 + style-to-object: 0.4.4 + unist-util-position: 4.0.4 + zwitch: 2.0.4 + transitivePeerDependencies: + - supports-color + optional: true + + hast-util-to-estree@3.1.3: + dependencies: + '@types/estree': 1.0.8 + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-attach-comments: 3.0.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + style-to-js: 1.1.17 + unist-util-position: 5.0.0 + zwitch: 2.0.4 + transitivePeerDependencies: + - supports-color + + hast-util-to-html@9.0.5: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.0 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + + hast-util-to-jsx-runtime@2.3.6: + dependencies: + '@types/estree': 1.0.8 + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + style-to-js: 1.1.17 + unist-util-position: 5.0.0 + vfile-message: 4.0.2 + transitivePeerDependencies: + - supports-color + + hast-util-to-mdast@10.1.2: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.0 + hast-util-phrasing: 3.0.1 + hast-util-to-html: 9.0.5 + hast-util-to-text: 4.0.2 + hast-util-whitespace: 3.0.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-hast: 13.2.0 + mdast-util-to-string: 4.0.0 + rehype-minify-whitespace: 6.0.2 + trim-trailing-lines: 2.1.0 + unist-util-position: 5.0.0 + unist-util-visit: 5.0.0 + + hast-util-to-parse5@7.1.0: + dependencies: + '@types/hast': 2.3.10 + comma-separated-tokens: 2.0.3 + property-information: 6.5.0 + space-separated-tokens: 2.0.2 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + + hast-util-to-parse5@8.0.0: + dependencies: + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + property-information: 6.5.0 + space-separated-tokens: 2.0.2 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + + hast-util-to-string@3.0.1: + dependencies: + '@types/hast': 3.0.4 + + hast-util-to-text@4.0.2: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + hast-util-is-element: 3.0.0 + unist-util-find-after: 5.0.0 + + hast-util-whitespace@2.0.1: {} + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + hastscript@7.2.0: + dependencies: + '@types/hast': 2.3.10 + comma-separated-tokens: 2.0.3 + hast-util-parse-selector: 3.1.1 + property-information: 6.5.0 + space-separated-tokens: 2.0.2 + + hastscript@9.0.1: + dependencies: + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + hast-util-parse-selector: 4.0.0 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + + hdr-histogram-js@2.0.3: + dependencies: + '@assemblyscript/loader': 0.10.1 + base64-js: 1.5.1 + pako: 1.0.11 + + hdr-histogram-percentiles-obj@3.0.0: {} + + he@1.2.0: {} + + history@4.10.1: + dependencies: + '@babel/runtime': 7.27.6 + loose-envify: 1.4.0 + resolve-pathname: 3.0.0 + tiny-invariant: 1.3.3 + tiny-warning: 1.0.3 + value-equal: 1.0.1 + + hoist-non-react-statics@3.3.2: + dependencies: + react-is: 16.13.1 + + hosted-git-info@4.1.0: + dependencies: + lru-cache: 6.0.0 + + hosted-git-info@6.1.3: + dependencies: + lru-cache: 7.18.3 + + hpack.js@2.1.6: + dependencies: + inherits: 2.0.4 + obuf: 1.1.2 + readable-stream: 2.3.8 + wbuf: 1.7.3 + + html-encoding-sniffer@2.0.1: + dependencies: + whatwg-encoding: 1.0.5 + + html-encoding-sniffer@4.0.0: + dependencies: + whatwg-encoding: 3.1.1 + + html-entities@2.6.0: {} + + html-escaper@2.0.2: {} + + html-minifier-terser@6.1.0: + dependencies: + camel-case: 4.1.2 + clean-css: 5.3.3 + commander: 8.3.0 + he: 1.2.0 + param-case: 3.0.4 + relateurl: 0.2.7 + terser: 5.43.1 + + html-minifier-terser@7.2.0: + dependencies: + camel-case: 4.1.2 + clean-css: 5.3.3 + commander: 10.0.1 + entities: 4.5.0 + param-case: 3.0.4 + relateurl: 0.2.7 + terser: 5.43.1 + + html-tags@3.3.1: {} + + html-url-attributes@3.0.1: {} + + html-void-elements@2.0.1: {} + + html-void-elements@3.0.0: {} + + html-webpack-plugin@5.6.3(@rspack/core@1.4.9(@swc/helpers@0.5.17))(webpack@5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17))): + dependencies: + '@types/html-minifier-terser': 6.1.0 + html-minifier-terser: 6.1.0 + lodash: 4.17.21 + pretty-error: 4.0.0 + tapable: 2.2.2 + optionalDependencies: + '@rspack/core': 1.4.9(@swc/helpers@0.5.17) + webpack: 5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17)) + + html-webpack-plugin@5.6.3(webpack@5.94.0(@swc/core@1.13.1)(esbuild@0.18.17)): + dependencies: + '@types/html-minifier-terser': 6.1.0 + html-minifier-terser: 6.1.0 + lodash: 4.17.21 + pretty-error: 4.0.0 + tapable: 2.2.2 + optionalDependencies: + webpack: 5.94.0(@swc/core@1.13.1)(esbuild@0.18.17) + optional: true + + htmlparser2@6.1.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 4.3.1 + domutils: 2.8.0 + entities: 2.2.0 + + htmlparser2@8.0.2: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + entities: 4.5.0 + + http-cache-semantics@4.2.0: {} + + http-deceiver@1.2.7: {} + + http-errors@1.6.3: + dependencies: + depd: 1.1.2 + inherits: 2.0.3 + setprototypeof: 1.1.0 + statuses: 1.5.0 + + http-errors@2.0.0: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + + http-parser-js@0.5.10: {} + + http-proxy-agent@4.0.1: + dependencies: + '@tootallnate/once': 1.1.2 + agent-base: 6.0.2 + debug: 4.4.1(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + + http-proxy-agent@5.0.0: + dependencies: + '@tootallnate/once': 2.0.0 + agent-base: 6.0.2 + debug: 4.4.1(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.1(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + + http-proxy-middleware@2.0.9(@types/express@4.17.23): + dependencies: + '@types/http-proxy': 1.17.16 + http-proxy: 1.18.1 + is-glob: 4.0.3 + is-plain-obj: 3.0.0 + micromatch: 4.0.8 + optionalDependencies: + '@types/express': 4.17.23 + transitivePeerDependencies: + - debug + + http-proxy@1.18.1: + dependencies: + eventemitter3: 4.0.7 + follow-redirects: 1.15.9(debug@4.4.1) + requires-port: 1.0.0 + transitivePeerDependencies: + - debug + + http-reasons@0.1.0: {} + + http-signature@1.4.0: + dependencies: + assert-plus: 1.0.0 + jsprim: 2.0.2 + sshpk: 1.18.0 + + http2-client@1.3.5: {} + + http2-wrapper@2.2.1: + dependencies: + quick-lru: 5.1.1 + resolve-alpn: 1.2.1 + + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.1(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.1(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + + human-id@4.1.1: {} + + human-signals@1.1.1: {} + + human-signals@2.1.0: {} + + human-signals@5.0.0: {} + + humanize-ms@1.2.1: + dependencies: + ms: 2.1.3 + + humps@2.0.1: {} + + i18n-iso-countries@7.14.0: + dependencies: + diacritics: 1.3.0 + + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + icss-utils@5.1.0(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + + ieee754@1.2.1: {} + + ignore-by-default@1.0.1: {} + + ignore-walk@6.0.5: + dependencies: + minimatch: 9.0.5 + + ignore@5.2.4: {} + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + image-size@0.5.5: + optional: true + + image-size@2.0.2: {} + + immer@9.0.21: {} + + immutable@4.3.7: {} + + immutable@5.1.3: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + import-lazy@4.0.0: {} + + imurmurhash@0.1.4: {} + + indent-string@4.0.0: {} + + infer-owner@1.0.4: {} + + infima@0.2.0-alpha.45: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.3: {} + + inherits@2.0.4: {} + + ini@1.3.8: {} + + ini@2.0.0: {} + + ini@4.1.1: {} + + inline-style-parser@0.1.1: {} + + inline-style-parser@0.2.4: {} + + inquirer@8.2.4: + dependencies: + ansi-escapes: 4.3.2 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-width: 3.0.0 + external-editor: 3.1.0 + figures: 3.2.0 + lodash: 4.17.21 + mute-stream: 0.0.8 + ora: 5.4.1 + run-async: 2.4.1 + rxjs: 7.8.2 + string-width: 4.2.3 + strip-ansi: 6.0.1 + through: 2.3.8 + wrap-ansi: 7.0.0 + + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.2 + side-channel: 1.1.0 + + internmap@1.0.1: {} + + internmap@2.0.3: {} + + interpret@1.4.0: {} + + intl-messageformat@10.7.16: + dependencies: + '@formatjs/ecma402-abstract': 2.3.4 + '@formatjs/fast-memoize': 2.2.7 + '@formatjs/icu-messageformat-parser': 2.11.2 + tslib: 2.8.1 + + invariant@2.2.4: + dependencies: + loose-envify: 1.4.0 + + ip-address@9.0.5: + dependencies: + jsbn: 1.1.0 + sprintf-js: 1.1.3 + + ipaddr.js@1.9.1: {} + + ipaddr.js@2.2.0: {} + + is-alphabetical@2.0.1: {} + + is-alphanumerical@2.0.1: + dependencies: + is-alphabetical: 2.0.1 + is-decimal: 2.0.1 + + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-arrayish@0.2.1: {} + + is-arrayish@0.3.2: + optional: true + + is-async-function@2.1.1: + dependencies: + async-function: 1.0.0 + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-buffer@2.0.5: {} + + is-bun-module@2.0.0: + dependencies: + semver: 7.7.2 + + is-callable@1.2.7: {} + + is-ci@3.0.1: + dependencies: + ci-info: 3.9.0 + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-data-view@1.0.2: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-typed-array: 1.1.15 + + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-decimal@2.0.1: {} + + is-docker@2.2.1: {} + + is-extendable@0.1.1: {} + + is-extglob@2.1.1: {} + + is-finalizationregistry@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-fullwidth-code-point@3.0.0: {} + + is-fullwidth-code-point@4.0.0: {} + + is-fullwidth-code-point@5.0.0: + dependencies: + get-east-asian-width: 1.3.0 + + is-generator-function@1.1.0: + dependencies: + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-hexadecimal@2.0.1: {} + + is-installed-globally@0.4.0: + dependencies: + global-dirs: 3.0.1 + is-path-inside: 3.0.3 + + is-interactive@1.0.0: {} + + is-lambda@1.0.1: {} + + is-map@2.0.3: {} + + is-negative-zero@2.0.3: {} + + is-npm@6.0.0: {} + + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-number@7.0.0: {} + + is-obj@1.0.1: {} + + is-obj@2.0.0: {} + + is-path-inside@3.0.3: {} + + is-plain-obj@2.1.0: {} + + is-plain-obj@3.0.0: {} + + is-plain-obj@4.1.0: {} + + is-plain-object@2.0.4: + dependencies: + isobject: 3.0.1 + + is-potential-custom-element-name@1.0.1: {} + + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + is-regexp@1.0.0: {} + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.4 + + is-stream@2.0.1: {} + + is-stream@3.0.0: {} + + is-string@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-subdir@1.2.0: + dependencies: + better-path-resolve: 1.0.0 + + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.19 + + is-typedarray@1.0.0: {} + + is-unicode-supported@0.1.0: {} + + is-weakmap@2.0.2: {} + + is-weakref@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-what@3.14.1: {} + + is-what@4.1.16: {} + + is-windows@1.0.2: {} + + is-wsl@2.2.0: + dependencies: + is-docker: 2.2.1 + + is-yarn-global@0.4.1: {} + + isarray@0.0.1: {} + + isarray@1.0.0: {} + + isarray@2.0.5: {} + + isbinaryfile@4.0.10: {} + + isexe@2.0.0: {} + + isobject@3.0.1: {} + + isstream@0.1.2: {} + + istanbul-lib-coverage@2.0.5: {} + + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-instrument@5.2.1: + dependencies: + '@babel/core': 7.28.0 + '@babel/parser': 7.28.0 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.2 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-lib-source-maps@3.0.6: + dependencies: + debug: 4.4.1(supports-color@5.5.0) + istanbul-lib-coverage: 2.0.5 + make-dir: 2.1.0 + rimraf: 2.7.1 + source-map: 0.6.1 + transitivePeerDependencies: + - supports-color + + istanbul-lib-source-maps@4.0.1: + dependencies: + debug: 4.4.1(supports-color@5.5.0) + istanbul-lib-coverage: 3.2.2 + source-map: 0.6.1 + transitivePeerDependencies: + - supports-color + + istanbul-reports@3.1.7: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + + iterator.prototype@1.1.5: + dependencies: + define-data-property: 1.1.4 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + has-symbols: 1.1.0 + set-function-name: 2.0.2 + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + jackspeak@4.1.1: + dependencies: + '@isaacs/cliui': 8.0.2 + + jake@10.9.2: + dependencies: + async: 3.2.6 + chalk: 4.1.2 + filelist: 1.0.4 + minimatch: 3.1.2 + + jasmine-core@4.6.1: {} + + jasmine-core@5.6.0: {} + + jasmine-spec-reporter@7.0.0: + dependencies: + colors: 1.4.0 + + jest-util@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/node': 22.16.5 + chalk: 4.1.2 + ci-info: 3.9.0 + graceful-fs: 4.2.11 + picomatch: 2.3.1 + + jest-worker@27.5.1: + dependencies: + '@types/node': 22.16.5 + merge-stream: 2.0.0 + supports-color: 8.1.1 + + jest-worker@29.7.0: + dependencies: + '@types/node': 22.16.5 + jest-util: 29.7.0 + merge-stream: 2.0.0 + supports-color: 8.1.1 + + jiti@1.21.7: {} + + jiti@2.4.2: {} + + joi@17.13.3: + dependencies: + '@hapi/hoek': 9.3.0 + '@hapi/topo': 5.1.0 + '@sideway/address': 4.1.5 + '@sideway/formula': 3.0.1 + '@sideway/pinpoint': 2.0.0 + + jose@5.10.0: {} + + joycon@3.1.1: {} + + js-levenshtein@1.1.6: {} + + js-tokens@4.0.0: {} + + js-yaml@3.14.1: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + jsbn@0.1.1: {} + + jsbn@1.1.0: {} + + jsdom@16.7.0: + dependencies: + abab: 2.0.6 + acorn: 8.15.0 + acorn-globals: 6.0.0 + cssom: 0.4.4 + cssstyle: 2.3.0 + data-urls: 2.0.0 + decimal.js: 10.6.0 + domexception: 2.0.1 + escodegen: 2.1.0 + form-data: 3.0.4 + html-encoding-sniffer: 2.0.1 + http-proxy-agent: 4.0.1 + https-proxy-agent: 5.0.1 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.20 + parse5: 6.0.1 + saxes: 5.0.1 + symbol-tree: 3.2.4 + tough-cookie: 4.1.4 + w3c-hr-time: 1.0.2 + w3c-xmlserializer: 2.0.0 + webidl-conversions: 6.1.0 + whatwg-encoding: 1.0.5 + whatwg-mimetype: 2.3.0 + whatwg-url: 8.7.0 + ws: 7.5.10 + xml-name-validator: 3.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + jsdom@26.1.0: + dependencies: + cssstyle: 4.6.0 + data-urls: 5.0.0 + decimal.js: 10.6.0 + 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.20 + parse5: 7.3.0 + rrweb-cssom: 0.8.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 5.1.2 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + ws: 8.18.3 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + jsesc@2.5.2: {} + + jsesc@3.0.2: {} + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-crawl@0.5.3: {} + + json-parse-even-better-errors@2.3.1: {} + + json-parse-even-better-errors@3.0.2: {} + + json-pointer@0.6.2: + dependencies: + foreach: 2.0.6 + + json-schema-compare@0.2.2: + dependencies: + lodash: 4.17.21 + + json-schema-merge-allof@0.8.1: + dependencies: + compute-lcm: 1.1.2 + json-schema-compare: 0.2.2 + lodash: 4.17.21 + + json-schema-traverse@0.4.1: {} + + json-schema-traverse@1.0.0: {} + + json-schema@0.4.0: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json-stringify-safe@5.0.1: {} + + json5@1.0.2: + dependencies: + minimist: 1.2.8 + + json5@2.2.3: {} + + jsonc-parser@3.2.0: {} + + jsonfile@4.0.0: + optionalDependencies: + graceful-fs: 4.2.11 + + jsonfile@6.1.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + jsonparse@1.3.1: {} + + jsonwebtoken@9.0.2: + dependencies: + jws: 3.2.2 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.7.2 + + jsprim@2.0.2: + dependencies: + assert-plus: 1.0.0 + extsprintf: 1.3.0 + json-schema: 0.4.0 + verror: 1.10.0 + + jsx-ast-utils@3.3.5: + dependencies: + array-includes: 3.1.9 + array.prototype.flat: 1.3.3 + object.assign: 4.1.7 + object.values: 1.2.1 + + jwa@1.4.2: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@3.2.2: + dependencies: + jwa: 1.4.2 + safe-buffer: 5.2.1 + + karma-chrome-launcher@3.2.0: + dependencies: + which: 1.3.1 + + karma-coverage-istanbul-reporter@3.0.3: + dependencies: + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 3.0.6 + istanbul-reports: 3.1.7 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + karma-coverage@2.2.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + istanbul-lib-instrument: 5.2.1 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 4.0.1 + istanbul-reports: 3.1.7 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + karma-jasmine-html-reporter@2.1.0(jasmine-core@5.6.0)(karma-jasmine@5.1.0(karma@6.4.4))(karma@6.4.4): + dependencies: + jasmine-core: 5.6.0 + karma: 6.4.4 + karma-jasmine: 5.1.0(karma@6.4.4) + + karma-jasmine@5.1.0(karma@6.4.4): + dependencies: + jasmine-core: 4.6.1 + karma: 6.4.4 + + karma-source-map-support@1.4.0: + dependencies: + source-map-support: 0.5.21 + + karma@6.4.4: + dependencies: + '@colors/colors': 1.5.0 + body-parser: 1.20.3 + braces: 3.0.3 + chokidar: 3.6.0 + connect: 3.7.0 + di: 0.0.1 + dom-serialize: 2.2.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + http-proxy: 1.18.1 + isbinaryfile: 4.0.10 + lodash: 4.17.21 + log4js: 6.9.1 + mime: 2.6.0 + minimatch: 3.1.2 + mkdirp: 0.5.6 + qjobs: 1.2.0 + range-parser: 1.2.1 + rimraf: 3.0.2 + socket.io: 4.8.1 + source-map: 0.6.1 + tmp: 0.2.3 + ua-parser-js: 0.7.40 + yargs: 16.2.0 + transitivePeerDependencies: + - bufferutil + - debug + - supports-color + - utf-8-validate + + katex@0.16.22: + dependencies: + commander: 8.3.0 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + khroma@2.1.0: {} + + kind-of@6.0.3: {} + + kleur@3.0.3: {} + + kleur@4.1.5: {} + + klona@2.0.6: {} + + knip@5.62.0(@types/node@24.1.0)(typescript@5.8.3): + dependencies: + '@nodelib/fs.walk': 1.2.8 + '@types/node': 24.1.0 + fast-glob: 3.3.3 + formatly: 0.2.4 + jiti: 2.4.2 + js-yaml: 4.1.0 + minimist: 1.2.8 + oxc-resolver: 11.6.0 + picocolors: 1.1.1 + picomatch: 4.0.3 + smol-toml: 1.4.1 + strip-json-comments: 5.0.2 + typescript: 5.8.3 + zod: 3.25.76 + zod-validation-error: 3.5.3(zod@3.25.76) + + kolorist@1.8.0: {} + + langium@3.3.1: + dependencies: + chevrotain: 11.0.3 + chevrotain-allstar: 0.3.1(chevrotain@11.0.3) + vscode-languageserver: 9.0.1 + vscode-languageserver-textdocument: 1.0.12 + vscode-uri: 3.0.8 + + language-subtag-registry@0.3.23: {} + + language-tags@1.0.9: + dependencies: + language-subtag-registry: 0.3.23 + + latest-version@7.0.0: + dependencies: + package-json: 8.1.1 + + launch-editor@2.10.0: + dependencies: + picocolors: 1.1.1 + shell-quote: 1.8.3 + + layout-base@1.0.2: {} + + layout-base@2.0.1: {} + + lazy-ass@1.6.0: {} + + less-loader@11.1.0(less@4.1.3)(webpack@5.94.0(@swc/core@1.13.1)(esbuild@0.18.17)): + dependencies: + klona: 2.0.6 + less: 4.1.3 + webpack: 5.94.0(@swc/core@1.13.1)(esbuild@0.18.17) + + less@4.1.3: + dependencies: + copy-anything: 2.0.6 + parse-node-version: 1.0.1 + tslib: 2.8.1 + optionalDependencies: + errno: 0.1.8 + graceful-fs: 4.2.11 + image-size: 0.5.5 + make-dir: 2.1.0 + mime: 1.6.0 + needle: 3.3.1 + source-map: 0.6.1 + + leven@3.1.0: {} + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + libphonenumber-js@1.12.10: {} + + license-webpack-plugin@4.0.2(webpack@5.94.0(@swc/core@1.13.1)(esbuild@0.18.17)): + dependencies: + webpack-sources: 3.3.3 + optionalDependencies: + webpack: 5.94.0(@swc/core@1.13.1)(esbuild@0.18.17) + + lightningcss-darwin-arm64@1.30.1: + optional: true + + lightningcss-darwin-x64@1.30.1: + optional: true + + lightningcss-freebsd-x64@1.30.1: + optional: true + + lightningcss-linux-arm-gnueabihf@1.30.1: + optional: true + + lightningcss-linux-arm64-gnu@1.30.1: + optional: true + + lightningcss-linux-arm64-musl@1.30.1: + optional: true + + lightningcss-linux-x64-gnu@1.30.1: + optional: true + + lightningcss-linux-x64-musl@1.30.1: + optional: true + + lightningcss-win32-arm64-msvc@1.30.1: + optional: true + + lightningcss-win32-x64-msvc@1.30.1: + optional: true + + lightningcss@1.30.1: + dependencies: + detect-libc: 2.0.4 + optionalDependencies: + lightningcss-darwin-arm64: 1.30.1 + lightningcss-darwin-x64: 1.30.1 + lightningcss-freebsd-x64: 1.30.1 + lightningcss-linux-arm-gnueabihf: 1.30.1 + lightningcss-linux-arm64-gnu: 1.30.1 + lightningcss-linux-arm64-musl: 1.30.1 + lightningcss-linux-x64-gnu: 1.30.1 + lightningcss-linux-x64-musl: 1.30.1 + lightningcss-win32-arm64-msvc: 1.30.1 + lightningcss-win32-x64-msvc: 1.30.1 + + lilconfig@2.1.0: {} + + lilconfig@3.1.3: {} + + lines-and-columns@1.2.4: {} + + lines-and-columns@2.0.4: {} + + lint-staged@15.5.1: + dependencies: + chalk: 5.4.1 + commander: 13.1.0 + debug: 4.4.1(supports-color@5.5.0) + execa: 8.0.1 + lilconfig: 3.1.3 + listr2: 8.3.3 + micromatch: 4.0.8 + pidtree: 0.6.0 + string-argv: 0.3.2 + yaml: 2.8.0 + transitivePeerDependencies: + - supports-color + + liquid-json@0.3.1: {} + + listr2@3.14.0(enquirer@2.4.1): + dependencies: + cli-truncate: 2.1.0 + colorette: 2.0.20 + log-update: 4.0.0 + p-map: 4.0.0 + rfdc: 1.4.1 + rxjs: 7.8.2 + through: 2.3.8 + wrap-ansi: 7.0.0 + optionalDependencies: + enquirer: 2.4.1 + + listr2@8.3.3: + dependencies: + cli-truncate: 4.0.0 + colorette: 2.0.20 + eventemitter3: 5.0.1 + log-update: 6.1.0 + rfdc: 1.4.1 + wrap-ansi: 9.0.0 + + load-script@1.0.0: {} + + load-tsconfig@0.2.5: {} + + loader-runner@4.3.0: {} + + loader-utils@2.0.4: + dependencies: + big.js: 5.2.2 + emojis-list: 3.0.0 + json5: 2.2.3 + + loader-utils@3.2.1: {} + + local-pkg@1.1.1: + dependencies: + mlly: 1.7.4 + pkg-types: 2.2.0 + quansync: 0.2.10 + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + locate-path@7.2.0: + dependencies: + p-locate: 6.0.0 + + lodash-es@4.17.21: {} + + lodash.camelcase@4.3.0: {} + + lodash.debounce@4.0.8: {} + + lodash.includes@4.3.0: {} + + lodash.isboolean@3.0.3: {} + + lodash.isempty@4.4.0: {} + + lodash.isfunction@3.0.9: {} + + lodash.isinteger@4.0.4: {} + + lodash.isnumber@3.0.3: {} + + lodash.isobject@3.0.2: {} + + lodash.isplainobject@4.0.6: {} + + lodash.isstring@4.0.1: {} + + lodash.memoize@4.1.2: {} + + lodash.merge@4.6.2: {} + + lodash.once@4.1.1: {} + + lodash.sortby@4.7.0: {} + + lodash.startcase@4.4.0: {} + + lodash.uniq@4.5.0: {} + + lodash@4.17.21: {} + + log-symbols@4.1.0: + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + + log-update@4.0.0: + dependencies: + ansi-escapes: 4.3.2 + cli-cursor: 3.1.0 + slice-ansi: 4.0.0 + wrap-ansi: 6.2.0 + + log-update@6.1.0: + dependencies: + ansi-escapes: 7.0.0 + cli-cursor: 5.0.0 + slice-ansi: 7.1.0 + strip-ansi: 7.1.0 + wrap-ansi: 9.0.0 + + log4js@6.9.1: + dependencies: + date-format: 4.0.14 + debug: 4.4.1(supports-color@5.5.0) + flatted: 3.3.3 + rfdc: 1.4.1 + streamroller: 3.1.5 + transitivePeerDependencies: + - supports-color + + long@5.3.2: {} + + longest-streak@3.1.0: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + loupe@3.1.4: {} + + lower-case@2.0.2: + dependencies: + tslib: 2.8.1 + + lowercase-keys@3.0.0: {} + + lru-cache@10.4.3: {} + + lru-cache@11.1.0: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lru-cache@6.0.0: + dependencies: + yallist: 4.0.0 + + lru-cache@7.18.3: {} + + lucide-react@0.469.0(react@19.1.0): + dependencies: + react: 19.1.0 + + lucide-react@0.503.0(react@18.3.1): + dependencies: + react: 18.3.1 + + lz-string@1.5.0: {} + + magic-string@0.30.1: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.4 + + magic-string@0.30.17: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.4 + + make-dir-cli@4.0.0: + dependencies: + make-dir: 5.0.0 + meow: 13.2.0 + + make-dir@2.1.0: + dependencies: + pify: 4.0.1 + semver: 5.7.2 + + make-dir@3.1.0: + dependencies: + semver: 6.3.1 + + make-dir@4.0.0: + dependencies: + semver: 7.7.2 + + make-dir@5.0.0: {} + + make-fetch-happen@10.2.1: + dependencies: + agentkeepalive: 4.6.0 + cacache: 16.1.3 + http-cache-semantics: 4.2.0 + http-proxy-agent: 5.0.0 + https-proxy-agent: 5.0.1 + is-lambda: 1.0.1 + lru-cache: 7.18.3 + minipass: 3.3.6 + minipass-collect: 1.0.2 + minipass-fetch: 2.1.2 + minipass-flush: 1.0.5 + minipass-pipeline: 1.2.4 + negotiator: 0.6.4 + promise-retry: 2.0.1 + socks-proxy-agent: 7.0.0 + ssri: 9.0.1 + transitivePeerDependencies: + - bluebird + - supports-color + + make-fetch-happen@11.1.1: + dependencies: + agentkeepalive: 4.6.0 + cacache: 17.1.4 + http-cache-semantics: 4.2.0 + http-proxy-agent: 5.0.0 + https-proxy-agent: 5.0.1 + is-lambda: 1.0.1 + lru-cache: 7.18.3 + minipass: 5.0.0 + minipass-fetch: 3.0.5 + minipass-flush: 1.0.5 + minipass-pipeline: 1.2.4 + negotiator: 0.6.4 + promise-retry: 2.0.1 + socks-proxy-agent: 7.0.0 + ssri: 10.0.6 + transitivePeerDependencies: + - supports-color + + map-stream@0.1.0: {} + + markdown-extensions@2.0.0: {} + + markdown-table@2.0.0: + dependencies: + repeat-string: 1.6.1 + + markdown-table@3.0.4: {} + + marked@15.0.12: {} + + marked@16.1.1: {} + + material-colors@1.2.6: {} + + material-design-icons-iconfont@6.7.0: {} + + math-intrinsics@1.1.0: {} + + mdast-util-definitions@5.1.2: + dependencies: + '@types/mdast': 3.0.15 + '@types/unist': 2.0.11 + unist-util-visit: 4.1.2 + + mdast-util-directive@3.1.0: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + parse-entities: 4.0.2 + stringify-entities: 4.0.4 + unist-util-visit-parents: 6.0.1 + transitivePeerDependencies: + - supports-color + + mdast-util-find-and-replace@2.2.2: + dependencies: + '@types/mdast': 3.0.15 + escape-string-regexp: 5.0.0 + unist-util-is: 5.2.1 + unist-util-visit-parents: 5.1.3 + + mdast-util-find-and-replace@3.0.2: + dependencies: + '@types/mdast': 4.0.4 + escape-string-regexp: 5.0.0 + unist-util-is: 6.0.0 + unist-util-visit-parents: 6.0.1 + + mdast-util-from-markdown@1.3.1: + dependencies: + '@types/mdast': 3.0.15 + '@types/unist': 2.0.11 + decode-named-character-reference: 1.2.0 + mdast-util-to-string: 3.2.0 + micromark: 3.2.0 + micromark-util-decode-numeric-character-reference: 1.1.0 + micromark-util-decode-string: 1.1.0 + micromark-util-normalize-identifier: 1.1.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + unist-util-stringify-position: 3.0.3 + uvu: 0.5.6 + transitivePeerDependencies: + - supports-color + + mdast-util-from-markdown@2.0.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.2.0 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.2 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-frontmatter@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + escape-string-regexp: 5.0.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + micromark-extension-frontmatter: 2.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-autolink-literal@1.0.3: + dependencies: + '@types/mdast': 3.0.15 + ccount: 2.0.1 + mdast-util-find-and-replace: 2.2.2 + micromark-util-character: 1.2.0 + + mdast-util-gfm-autolink-literal@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-find-and-replace: 3.0.2 + micromark-util-character: 2.1.1 + + mdast-util-gfm-footnote@1.0.2: + dependencies: + '@types/mdast': 3.0.15 + mdast-util-to-markdown: 1.5.0 + micromark-util-normalize-identifier: 1.1.0 + + mdast-util-gfm-footnote@2.1.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + micromark-util-normalize-identifier: 2.0.1 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-strikethrough@1.0.3: + dependencies: + '@types/mdast': 3.0.15 + mdast-util-to-markdown: 1.5.0 + + mdast-util-gfm-strikethrough@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-table@1.0.7: + dependencies: + '@types/mdast': 3.0.15 + markdown-table: 3.0.4 + mdast-util-from-markdown: 1.3.1 + mdast-util-to-markdown: 1.5.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-table@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + markdown-table: 3.0.4 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-task-list-item@1.0.2: + dependencies: + '@types/mdast': 3.0.15 + mdast-util-to-markdown: 1.5.0 + + mdast-util-gfm-task-list-item@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm@2.0.2: + dependencies: + mdast-util-from-markdown: 1.3.1 + mdast-util-gfm-autolink-literal: 1.0.3 + mdast-util-gfm-footnote: 1.0.2 + mdast-util-gfm-strikethrough: 1.0.3 + mdast-util-gfm-table: 1.0.7 + mdast-util-gfm-task-list-item: 1.0.2 + mdast-util-to-markdown: 1.5.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm@3.1.0: + dependencies: + mdast-util-from-markdown: 2.0.2 + mdast-util-gfm-autolink-literal: 2.0.1 + mdast-util-gfm-footnote: 2.1.0 + mdast-util-gfm-strikethrough: 2.0.0 + mdast-util-gfm-table: 2.0.0 + mdast-util-gfm-task-list-item: 2.0.0 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-expression@1.3.2: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 2.3.10 + '@types/mdast': 3.0.15 + mdast-util-from-markdown: 1.3.1 + mdast-util-to-markdown: 1.5.0 + transitivePeerDependencies: + - supports-color + optional: true + + mdast-util-mdx-expression@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-jsx@2.1.4: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 2.3.10 + '@types/mdast': 3.0.15 + '@types/unist': 2.0.11 + ccount: 2.0.1 + mdast-util-from-markdown: 1.3.1 + mdast-util-to-markdown: 1.5.0 + parse-entities: 4.0.2 + stringify-entities: 4.0.4 + unist-util-remove-position: 4.0.2 + unist-util-stringify-position: 3.0.3 + vfile-message: 3.1.4 + transitivePeerDependencies: + - supports-color + optional: true + + mdast-util-mdx-jsx@3.2.0: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + parse-entities: 4.0.2 + stringify-entities: 4.0.4 + unist-util-stringify-position: 4.0.0 + vfile-message: 4.0.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx@2.0.1: + dependencies: + mdast-util-from-markdown: 1.3.1 + mdast-util-mdx-expression: 1.3.2 + mdast-util-mdx-jsx: 2.1.4 + mdast-util-mdxjs-esm: 1.3.1 + mdast-util-to-markdown: 1.5.0 + transitivePeerDependencies: + - supports-color + optional: true + + mdast-util-mdx@3.0.0: + dependencies: + mdast-util-from-markdown: 2.0.2 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdxjs-esm@1.3.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 2.3.10 + '@types/mdast': 3.0.15 + mdast-util-from-markdown: 1.3.1 + mdast-util-to-markdown: 1.5.0 + transitivePeerDependencies: + - supports-color + optional: true + + mdast-util-mdxjs-esm@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@3.0.1: + dependencies: + '@types/mdast': 3.0.15 + unist-util-is: 5.2.1 + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.0 + + mdast-util-to-hast@12.3.0: + dependencies: + '@types/hast': 2.3.10 + '@types/mdast': 3.0.15 + mdast-util-definitions: 5.1.2 + micromark-util-sanitize-uri: 1.2.0 + trim-lines: 3.0.1 + unist-util-generated: 2.0.1 + unist-util-position: 4.0.4 + unist-util-visit: 4.1.2 + + mdast-util-to-hast@13.2.0: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.0 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + + mdast-util-to-markdown@1.5.0: + dependencies: + '@types/mdast': 3.0.15 + '@types/unist': 2.0.11 + longest-streak: 3.1.0 + mdast-util-phrasing: 3.0.1 + mdast-util-to-string: 3.2.0 + micromark-util-decode-string: 1.1.0 + unist-util-visit: 4.1.2 + zwitch: 2.0.4 + + mdast-util-to-markdown@2.1.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + unist-util-visit: 5.0.0 + zwitch: 2.0.4 + + mdast-util-to-string@3.2.0: + dependencies: + '@types/mdast': 3.0.15 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + + mdn-data@2.0.28: {} + + mdn-data@2.0.30: {} + + mdx-mermaid@2.0.3(mermaid@11.9.0)(react@18.3.1)(typescript@5.8.3)(unist-util-visit@5.0.0): + dependencies: + mermaid: 11.9.0 + react: 18.3.1 + unist-util-visit: 5.0.0 + optionalDependencies: + estree-util-to-js: 1.2.0 + estree-util-visit: 1.2.1 + hast-util-from-html: 1.0.2 + hast-util-to-estree: 2.3.3 + mdast-util-from-markdown: 1.3.1 + mdast-util-mdx: 2.0.1 + micromark-extension-mdxjs: 1.0.1 + puppeteer: 22.15.0(typescript@5.8.3) + transitivePeerDependencies: + - bare-buffer + - bufferutil + - supports-color + - typescript + - utf-8-validate + + media-typer@0.3.0: {} + + medium-zoom@1.1.0: {} + + memfs@3.5.3: + dependencies: + fs-monkey: 1.1.0 + + memoize-one@5.2.1: {} + + meow@13.2.0: {} + + merge-anything@5.1.7: + dependencies: + is-what: 4.1.16 + + merge-descriptors@1.0.3: {} + + merge-stream@2.0.0: {} + + merge2@1.4.1: {} + + mermaid@11.9.0: + dependencies: + '@braintree/sanitize-url': 7.1.1 + '@iconify/utils': 2.3.0 + '@mermaid-js/parser': 0.6.2 + '@types/d3': 7.4.3 + cytoscape: 3.32.1 + cytoscape-cose-bilkent: 4.1.0(cytoscape@3.32.1) + cytoscape-fcose: 2.2.0(cytoscape@3.32.1) + d3: 7.9.0 + d3-sankey: 0.12.3 + dagre-d3-es: 7.0.11 + dayjs: 1.11.13 + dompurify: 3.2.6 + katex: 0.16.22 + khroma: 2.1.0 + lodash-es: 4.17.21 + marked: 16.1.1 + roughjs: 4.6.6 + stylis: 4.3.6 + ts-dedent: 2.2.0 + uuid: 11.1.0 + transitivePeerDependencies: + - supports-color + + methods@1.1.2: {} + + micromark-core-commonmark@1.1.0: + dependencies: + decode-named-character-reference: 1.2.0 + micromark-factory-destination: 1.1.0 + micromark-factory-label: 1.1.0 + micromark-factory-space: 1.1.0 + micromark-factory-title: 1.1.0 + micromark-factory-whitespace: 1.1.0 + micromark-util-character: 1.2.0 + micromark-util-chunked: 1.1.0 + micromark-util-classify-character: 1.1.0 + micromark-util-html-tag-name: 1.2.0 + micromark-util-normalize-identifier: 1.1.0 + micromark-util-resolve-all: 1.1.0 + micromark-util-subtokenize: 1.1.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + uvu: 0.5.6 + + micromark-core-commonmark@2.0.3: + dependencies: + decode-named-character-reference: 1.2.0 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-directive@3.0.2: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + parse-entities: 4.0.2 + + micromark-extension-frontmatter@2.0.0: + dependencies: + fault: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-autolink-literal@1.0.5: + dependencies: + micromark-util-character: 1.2.0 + micromark-util-sanitize-uri: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + + micromark-extension-gfm-autolink-literal@2.1.0: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-footnote@1.1.2: + dependencies: + micromark-core-commonmark: 1.1.0 + micromark-factory-space: 1.1.0 + micromark-util-character: 1.2.0 + micromark-util-normalize-identifier: 1.1.0 + micromark-util-sanitize-uri: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + uvu: 0.5.6 + + micromark-extension-gfm-footnote@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-strikethrough@1.0.7: + dependencies: + micromark-util-chunked: 1.1.0 + micromark-util-classify-character: 1.1.0 + micromark-util-resolve-all: 1.1.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + uvu: 0.5.6 + + micromark-extension-gfm-strikethrough@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-table@1.0.7: + dependencies: + micromark-factory-space: 1.1.0 + micromark-util-character: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + uvu: 0.5.6 + + micromark-extension-gfm-table@2.1.1: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-tagfilter@1.0.2: + dependencies: + micromark-util-types: 1.1.0 + + micromark-extension-gfm-tagfilter@2.0.0: + dependencies: + micromark-util-types: 2.0.2 + + micromark-extension-gfm-task-list-item@1.0.5: + dependencies: + micromark-factory-space: 1.1.0 + micromark-util-character: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + uvu: 0.5.6 + + micromark-extension-gfm-task-list-item@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm@2.0.3: + dependencies: + micromark-extension-gfm-autolink-literal: 1.0.5 + micromark-extension-gfm-footnote: 1.1.2 + micromark-extension-gfm-strikethrough: 1.0.7 + micromark-extension-gfm-table: 1.0.7 + micromark-extension-gfm-tagfilter: 1.0.2 + micromark-extension-gfm-task-list-item: 1.0.5 + micromark-util-combine-extensions: 1.1.0 + micromark-util-types: 1.1.0 + + micromark-extension-gfm@3.0.0: + dependencies: + micromark-extension-gfm-autolink-literal: 2.1.0 + micromark-extension-gfm-footnote: 2.1.0 + micromark-extension-gfm-strikethrough: 2.1.0 + micromark-extension-gfm-table: 2.1.1 + micromark-extension-gfm-tagfilter: 2.0.0 + micromark-extension-gfm-task-list-item: 2.1.0 + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-mdx-expression@1.0.8: + dependencies: + '@types/estree': 1.0.8 + micromark-factory-mdx-expression: 1.0.9 + micromark-factory-space: 1.1.0 + micromark-util-character: 1.2.0 + micromark-util-events-to-acorn: 1.2.3 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + uvu: 0.5.6 + optional: true + + micromark-extension-mdx-expression@3.0.1: + dependencies: + '@types/estree': 1.0.8 + devlop: 1.1.0 + micromark-factory-mdx-expression: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-events-to-acorn: 2.0.3 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-mdx-jsx@1.0.5: + dependencies: + '@types/acorn': 4.0.6 + '@types/estree': 1.0.8 + estree-util-is-identifier-name: 2.1.0 + micromark-factory-mdx-expression: 1.0.9 + micromark-factory-space: 1.1.0 + micromark-util-character: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + uvu: 0.5.6 + vfile-message: 3.1.4 + optional: true + + micromark-extension-mdx-jsx@3.0.2: + dependencies: + '@types/estree': 1.0.8 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + micromark-factory-mdx-expression: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-events-to-acorn: 2.0.3 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + vfile-message: 4.0.2 + + micromark-extension-mdx-md@1.0.1: + dependencies: + micromark-util-types: 1.1.0 + optional: true + + micromark-extension-mdx-md@2.0.0: + dependencies: + micromark-util-types: 2.0.2 + + micromark-extension-mdxjs-esm@1.0.5: + dependencies: + '@types/estree': 1.0.8 + micromark-core-commonmark: 1.1.0 + micromark-util-character: 1.2.0 + micromark-util-events-to-acorn: 1.2.3 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + unist-util-position-from-estree: 1.1.2 + uvu: 0.5.6 + vfile-message: 3.1.4 + optional: true + + micromark-extension-mdxjs-esm@3.0.0: + dependencies: + '@types/estree': 1.0.8 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-util-character: 2.1.1 + micromark-util-events-to-acorn: 2.0.3 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-position-from-estree: 2.0.0 + vfile-message: 4.0.2 + + micromark-extension-mdxjs@1.0.1: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + micromark-extension-mdx-expression: 1.0.8 + micromark-extension-mdx-jsx: 1.0.5 + micromark-extension-mdx-md: 1.0.1 + micromark-extension-mdxjs-esm: 1.0.5 + micromark-util-combine-extensions: 1.1.0 + micromark-util-types: 1.1.0 + optional: true + + micromark-extension-mdxjs@3.0.0: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + micromark-extension-mdx-expression: 3.0.1 + micromark-extension-mdx-jsx: 3.0.2 + micromark-extension-mdx-md: 2.0.0 + micromark-extension-mdxjs-esm: 3.0.0 + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-destination@1.1.0: + dependencies: + micromark-util-character: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-label@1.1.0: + dependencies: + micromark-util-character: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + uvu: 0.5.6 + + micromark-factory-label@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-mdx-expression@1.0.9: + dependencies: + '@types/estree': 1.0.8 + micromark-util-character: 1.2.0 + micromark-util-events-to-acorn: 1.2.3 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + unist-util-position-from-estree: 1.1.2 + uvu: 0.5.6 + vfile-message: 3.1.4 + optional: true + + micromark-factory-mdx-expression@2.0.3: + dependencies: + '@types/estree': 1.0.8 + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-events-to-acorn: 2.0.3 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-position-from-estree: 2.0.0 + vfile-message: 4.0.2 + + micromark-factory-space@1.1.0: + dependencies: + micromark-util-character: 1.2.0 + micromark-util-types: 1.1.0 + + micromark-factory-space@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.2 + + micromark-factory-title@1.1.0: + dependencies: + micromark-factory-space: 1.1.0 + micromark-util-character: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-whitespace@1.1.0: + dependencies: + micromark-factory-space: 1.1.0 + micromark-util-character: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-character@1.2.0: + dependencies: + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-chunked@1.1.0: + dependencies: + micromark-util-symbol: 1.1.0 + + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-classify-character@1.1.0: + dependencies: + micromark-util-character: 1.2.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-combine-extensions@1.1.0: + dependencies: + micromark-util-chunked: 1.1.0 + micromark-util-types: 1.1.0 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-decode-numeric-character-reference@1.1.0: + dependencies: + micromark-util-symbol: 1.1.0 + + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-decode-string@1.1.0: + dependencies: + decode-named-character-reference: 1.2.0 + micromark-util-character: 1.2.0 + micromark-util-decode-numeric-character-reference: 1.1.0 + micromark-util-symbol: 1.1.0 + + micromark-util-decode-string@2.0.1: + dependencies: + decode-named-character-reference: 1.2.0 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 + + micromark-util-encode@1.1.0: {} + + micromark-util-encode@2.0.1: {} + + micromark-util-events-to-acorn@1.2.3: + dependencies: + '@types/acorn': 4.0.6 + '@types/estree': 1.0.8 + '@types/unist': 2.0.11 + estree-util-visit: 1.2.1 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + uvu: 0.5.6 + vfile-message: 3.1.4 + optional: true + + micromark-util-events-to-acorn@2.0.3: + dependencies: + '@types/estree': 1.0.8 + '@types/unist': 3.0.3 + devlop: 1.1.0 + estree-util-visit: 2.0.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + vfile-message: 4.0.2 + + micromark-util-html-tag-name@1.2.0: {} + + micromark-util-html-tag-name@2.0.1: {} + + micromark-util-normalize-identifier@1.1.0: + dependencies: + micromark-util-symbol: 1.1.0 + + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-resolve-all@1.1.0: + dependencies: + micromark-util-types: 1.1.0 + + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.2 + + micromark-util-sanitize-uri@1.2.0: + dependencies: + micromark-util-character: 1.2.0 + micromark-util-encode: 1.1.0 + micromark-util-symbol: 1.1.0 + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-subtokenize@1.1.0: + dependencies: + micromark-util-chunked: 1.1.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + uvu: 0.5.6 + + micromark-util-subtokenize@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-symbol@1.1.0: {} + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@1.1.0: {} + + micromark-util-types@2.0.2: {} + + micromark@3.2.0: + dependencies: + '@types/debug': 4.1.12 + debug: 4.4.1(supports-color@5.5.0) + decode-named-character-reference: 1.2.0 + micromark-core-commonmark: 1.1.0 + micromark-factory-space: 1.1.0 + micromark-util-character: 1.2.0 + micromark-util-chunked: 1.1.0 + micromark-util-combine-extensions: 1.1.0 + micromark-util-decode-numeric-character-reference: 1.1.0 + micromark-util-encode: 1.1.0 + micromark-util-normalize-identifier: 1.1.0 + micromark-util-resolve-all: 1.1.0 + micromark-util-sanitize-uri: 1.2.0 + micromark-util-subtokenize: 1.1.0 + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + uvu: 0.5.6 + transitivePeerDependencies: + - supports-color + + micromark@4.0.2: + dependencies: + '@types/debug': 4.1.12 + debug: 4.4.1(supports-color@5.5.0) + decode-named-character-reference: 1.2.0 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + transitivePeerDependencies: + - supports-color + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mime-db@1.33.0: {} + + mime-db@1.52.0: {} + + mime-db@1.54.0: {} + + mime-format@2.0.1: + dependencies: + charset: 1.0.1 + + mime-types@2.1.18: + dependencies: + mime-db: 1.33.0 + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime@1.6.0: {} + + mime@2.6.0: {} + + mimic-fn@2.1.0: {} + + mimic-fn@4.0.0: {} + + mimic-function@5.0.1: {} + + mimic-response@3.1.0: {} + + mimic-response@4.0.0: {} + + min-indent@1.0.1: {} + + mini-css-extract-plugin@2.7.6(webpack@5.94.0(@swc/core@1.13.1)(esbuild@0.18.17)): + dependencies: + schema-utils: 4.3.2 + webpack: 5.94.0(@swc/core@1.13.1)(esbuild@0.18.17) + + mini-css-extract-plugin@2.9.2(webpack@5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17))): + dependencies: + schema-utils: 4.3.2 + tapable: 2.2.2 + webpack: 5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17)) + + mini-svg-data-uri@1.4.4: {} + + minimalistic-assert@1.0.1: {} + + minimatch@10.0.3: + dependencies: + '@isaacs/brace-expansion': 5.0.0 + + minimatch@3.0.5: + dependencies: + brace-expansion: 1.1.12 + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + + minimatch@5.1.6: + dependencies: + brace-expansion: 2.0.2 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.2 + + minimist@1.2.8: {} + + minipass-collect@1.0.2: + dependencies: + minipass: 3.3.6 + + minipass-fetch@2.1.2: + dependencies: + minipass: 3.3.6 + minipass-sized: 1.0.3 + minizlib: 2.1.2 + optionalDependencies: + encoding: 0.1.13 + + minipass-fetch@3.0.5: + dependencies: + minipass: 7.1.2 + minipass-sized: 1.0.3 + minizlib: 2.1.2 + optionalDependencies: + encoding: 0.1.13 + + minipass-flush@1.0.5: + dependencies: + minipass: 3.3.6 + + minipass-json-stream@1.0.2: + dependencies: + jsonparse: 1.3.1 + minipass: 3.3.6 + + minipass-pipeline@1.2.4: + dependencies: + minipass: 3.3.6 + + minipass-sized@1.0.3: + dependencies: + minipass: 3.3.6 + + minipass@3.3.6: + dependencies: + yallist: 4.0.0 + + minipass@5.0.0: {} + + minipass@7.1.2: {} + + minizlib@2.1.2: + dependencies: + minipass: 3.3.6 + yallist: 4.0.0 + + mitt@3.0.1: + optional: true + + mkdirp@0.5.6: + dependencies: + minimist: 1.2.8 + + mkdirp@1.0.4: {} + + mlly@1.7.4: + dependencies: + acorn: 8.15.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.1 + + mocha@11.7.1: + dependencies: + browser-stdout: 1.3.1 + chokidar: 4.0.3 + debug: 4.4.1(supports-color@8.1.1) + diff: 7.0.0 + escape-string-regexp: 4.0.0 + find-up: 5.0.0 + glob: 10.4.5 + he: 1.2.0 + js-yaml: 4.1.0 + log-symbols: 4.1.0 + minimatch: 9.0.5 + ms: 2.1.3 + picocolors: 1.1.1 + serialize-javascript: 6.0.2 + strip-json-comments: 3.1.1 + supports-color: 8.1.1 + workerpool: 9.3.3 + yargs: 17.7.2 + yargs-parser: 21.1.1 + yargs-unparser: 2.0.0 + + mochawesome-report-generator@6.2.0: + dependencies: + chalk: 4.1.2 + dateformat: 4.6.3 + escape-html: 1.0.3 + fs-extra: 10.1.0 + fsu: 1.1.1 + lodash.isfunction: 3.0.9 + opener: 1.5.2 + prop-types: 15.8.1 + tcomb: 3.2.29 + tcomb-validation: 3.4.1 + validator: 13.15.15 + yargs: 17.7.2 + + mochawesome@7.1.3(mocha@11.7.1): + dependencies: + chalk: 4.1.2 + diff: 5.2.0 + json-stringify-safe: 5.0.1 + lodash.isempty: 4.4.0 + lodash.isfunction: 3.0.9 + lodash.isobject: 3.0.2 + lodash.isstring: 4.0.1 + mocha: 11.7.1 + mochawesome-report-generator: 6.2.0 + strip-ansi: 6.0.1 + uuid: 8.3.2 + + moment@2.30.1: {} + + mri@1.2.0: {} + + mrmime@1.0.1: {} + + mrmime@2.0.1: {} + + ms@2.0.0: {} + + ms@2.1.3: {} + + multicast-dns@7.2.5: + dependencies: + dns-packet: 5.6.1 + thunky: 1.1.0 + + mustache@4.2.0: {} + + mute-stream@0.0.8: {} + + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + + nanoid@3.3.11: {} + + napi-postinstall@0.3.2: {} + + natural-compare-lite@1.4.0: {} + + natural-compare@1.4.0: {} + + needle@3.3.1: + dependencies: + iconv-lite: 0.6.3 + sax: 1.4.1 + optional: true + + negotiator@0.6.3: {} + + negotiator@0.6.4: {} + + negotiator@1.0.0: {} + + neo-async@2.6.2: {} + + neotraverse@0.6.15: {} + + netmask@2.0.2: + optional: true + + next-intl@3.26.5(next@15.4.0-canary.86(@babel/core@7.28.0)(@playwright/test@1.54.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.2))(react@19.1.0): + dependencies: + '@formatjs/intl-localematcher': 0.5.10 + negotiator: 1.0.0 + next: 15.4.0-canary.86(@babel/core@7.28.0)(@playwright/test@1.54.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.2) + react: 19.1.0 + use-intl: 3.26.5(react@19.1.0) + + next-themes@0.2.1(next@15.4.0-canary.86(@babel/core@7.28.0)(@playwright/test@1.54.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.2))(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + next: 15.4.0-canary.86(@babel/core@7.28.0)(@playwright/test@1.54.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.2) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + + next@15.4.0-canary.86(@babel/core@7.28.0)(@playwright/test@1.54.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.89.2): + dependencies: + '@next/env': 15.4.0-canary.86 + '@swc/helpers': 0.5.15 + caniuse-lite: 1.0.30001727 + postcss: 8.4.31 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + styled-jsx: 5.1.6(@babel/core@7.28.0)(react@19.1.0) + optionalDependencies: + '@next/swc-darwin-arm64': 15.4.0-canary.86 + '@next/swc-darwin-x64': 15.4.0-canary.86 + '@next/swc-linux-arm64-gnu': 15.4.0-canary.86 + '@next/swc-linux-arm64-musl': 15.4.0-canary.86 + '@next/swc-linux-x64-gnu': 15.4.0-canary.86 + '@next/swc-linux-x64-musl': 15.4.0-canary.86 + '@next/swc-win32-arm64-msvc': 15.4.0-canary.86 + '@next/swc-win32-x64-msvc': 15.4.0-canary.86 + '@playwright/test': 1.54.1 + sass: 1.89.2 + sharp: 0.34.3 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + + ngx-color@9.0.0(@angular/common@16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2))(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3)): + dependencies: + '@angular/common': 16.2.12(@angular/core@16.2.12(rxjs@7.8.2)(zone.js@0.13.3))(rxjs@7.8.2) + '@angular/core': 16.2.12(rxjs@7.8.2)(zone.js@0.13.3) + '@ctrl/tinycolor': 3.6.1 + material-colors: 1.2.6 + tslib: 2.8.1 + + nice-grpc-common@2.0.2: + dependencies: + ts-error: 1.0.6 + + nice-grpc@2.0.1: + dependencies: + '@grpc/grpc-js': 1.13.4 + abort-controller-x: 0.4.3 + nice-grpc-common: 2.0.2 + + nice-napi@1.0.2: + dependencies: + node-addon-api: 3.2.1 + node-gyp-build: 4.8.4 + optional: true + + no-case@3.0.4: + dependencies: + lower-case: 2.0.2 + tslib: 2.8.1 + + node-addon-api@3.2.1: {} + + node-addon-api@7.1.1: + optional: true + + node-domexception@1.0.0: {} + + node-emoji@2.2.0: + dependencies: + '@sindresorhus/is': 4.6.0 + char-regex: 1.0.2 + emojilib: 2.4.0 + skin-tone: 2.0.0 + + node-fetch-h2@2.3.0: + dependencies: + http2-client: 1.3.5 + + node-fetch@2.7.0(encoding@0.1.13): + dependencies: + whatwg-url: 5.0.0 + optionalDependencies: + encoding: 0.1.13 + + node-fetch@3.3.2: + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + + node-forge@1.3.1: {} + + node-gyp-build@4.8.4: {} + + node-gyp@9.4.1: + dependencies: + env-paths: 2.2.1 + exponential-backoff: 3.1.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + make-fetch-happen: 10.2.1 + nopt: 6.0.0 + npmlog: 6.0.2 + rimraf: 3.0.2 + semver: 7.7.2 + tar: 6.2.1 + which: 2.0.2 + transitivePeerDependencies: + - bluebird + - supports-color + + node-readfiles@0.2.0: + dependencies: + es6-promise: 3.3.1 + + node-releases@2.0.19: {} + + nodemon@3.1.10: + dependencies: + chokidar: 3.6.0 + debug: 4.4.1(supports-color@5.5.0) + ignore-by-default: 1.0.1 + minimatch: 3.1.2 + pstree.remy: 1.1.8 + semver: 7.7.2 + simple-update-notifier: 2.0.0 + supports-color: 5.5.0 + touch: 3.1.1 + undefsafe: 2.0.5 + + nopt@5.0.0: + dependencies: + abbrev: 1.1.1 + + nopt@6.0.0: + dependencies: + abbrev: 1.1.1 + + normalize-package-data@3.0.3: + dependencies: + hosted-git-info: 4.1.0 + is-core-module: 2.16.1 + semver: 7.7.2 + validate-npm-package-license: 3.0.4 + + normalize-package-data@5.0.0: + dependencies: + hosted-git-info: 6.1.3 + is-core-module: 2.16.1 + semver: 7.7.2 + validate-npm-package-license: 3.0.4 + + normalize-path@3.0.0: {} + + normalize-range@0.1.2: {} + + normalize-url@8.0.2: {} + + npm-bundled@3.0.1: + dependencies: + npm-normalize-package-bin: 3.0.1 + + npm-install-checks@6.3.0: + dependencies: + semver: 7.7.2 + + npm-normalize-package-bin@3.0.1: {} + + npm-package-arg@10.1.0: + dependencies: + hosted-git-info: 6.1.3 + proc-log: 3.0.0 + semver: 7.7.2 + validate-npm-package-name: 5.0.1 + + npm-packlist@7.0.4: + dependencies: + ignore-walk: 6.0.5 + + npm-pick-manifest@8.0.1: + dependencies: + npm-install-checks: 6.3.0 + npm-normalize-package-bin: 3.0.1 + npm-package-arg: 10.1.0 + semver: 7.7.2 + + npm-registry-fetch@14.0.5: + dependencies: + make-fetch-happen: 11.1.1 + minipass: 5.0.0 + minipass-fetch: 3.0.5 + minipass-json-stream: 1.0.2 + minizlib: 2.1.2 + npm-package-arg: 10.1.0 + proc-log: 3.0.0 + transitivePeerDependencies: + - supports-color + + npm-run-path@4.0.1: + dependencies: + path-key: 3.1.1 + + npm-run-path@5.3.0: + dependencies: + path-key: 4.0.0 + + npmlog@5.0.1: + dependencies: + are-we-there-yet: 2.0.0 + console-control-strings: 1.1.0 + gauge: 3.0.2 + set-blocking: 2.0.0 + + npmlog@6.0.2: + dependencies: + are-we-there-yet: 3.0.1 + console-control-strings: 1.1.0 + gauge: 4.0.4 + set-blocking: 2.0.0 + + nprogress@0.2.0: {} + + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + + null-loader@4.0.1(webpack@5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17))): + dependencies: + loader-utils: 2.0.4 + schema-utils: 3.3.0 + webpack: 5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17)) + + nwsapi@2.2.20: {} + + nx@16.5.1(@swc/core@1.13.1): + dependencies: + '@nrwl/tao': 16.5.1(@swc/core@1.13.1) + '@parcel/watcher': 2.0.4 + '@yarnpkg/lockfile': 1.1.0 + '@yarnpkg/parsers': 3.0.0-rc.46 + '@zkochan/js-yaml': 0.0.6 + axios: 1.10.0(debug@4.4.1) + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-spinners: 2.6.1 + cliui: 7.0.4 + dotenv: 10.0.0 + enquirer: 2.3.6 + fast-glob: 3.2.7 + figures: 3.2.0 + flat: 5.0.2 + fs-extra: 11.3.0 + glob: 7.1.4 + ignore: 5.2.4 + js-yaml: 4.1.0 + jsonc-parser: 3.2.0 + lines-and-columns: 2.0.4 + minimatch: 3.0.5 + npm-run-path: 4.0.1 + open: 8.4.2 + semver: 7.5.3 + string-width: 4.2.3 + strong-log-transformer: 2.1.0 + tar-stream: 2.2.0 + tmp: 0.2.1 + tsconfig-paths: 4.2.0 + tslib: 2.8.1 + v8-compile-cache: 2.3.0 + yargs: 17.7.2 + yargs-parser: 21.1.1 + optionalDependencies: + '@nx/nx-darwin-arm64': 16.5.1 + '@nx/nx-darwin-x64': 16.5.1 + '@nx/nx-freebsd-x64': 16.5.1 + '@nx/nx-linux-arm-gnueabihf': 16.5.1 + '@nx/nx-linux-arm64-gnu': 16.5.1 + '@nx/nx-linux-arm64-musl': 16.5.1 + '@nx/nx-linux-x64-gnu': 16.5.1 + '@nx/nx-linux-x64-musl': 16.5.1 + '@nx/nx-win32-arm64-msvc': 16.5.1 + '@nx/nx-win32-x64-msvc': 16.5.1 + '@swc/core': 1.13.1(@swc/helpers@0.5.17) + transitivePeerDependencies: + - debug + + oas-kit-common@1.0.8: + dependencies: + fast-safe-stringify: 2.1.1 + + oas-linter@3.2.2: + dependencies: + '@exodus/schemasafe': 1.3.0 + should: 13.2.3 + yaml: 1.10.2 + + oas-resolver-browser@2.5.6: + dependencies: + node-fetch-h2: 2.3.0 + oas-kit-common: 1.0.8 + path-browserify: 1.0.1 + reftools: 1.1.9 + yaml: 1.10.2 + yargs: 17.7.2 + + oas-resolver@2.5.6: + dependencies: + node-fetch-h2: 2.3.0 + oas-kit-common: 1.0.8 + reftools: 1.1.9 + yaml: 1.10.2 + yargs: 17.7.2 + + oas-schema-walker@1.1.5: {} + + oas-validator@5.0.8: + dependencies: + call-me-maybe: 1.0.2 + oas-kit-common: 1.0.8 + oas-linter: 3.2.2 + oas-resolver: 2.5.6 + oas-schema-walker: 1.1.5 + reftools: 1.1.9 + should: 13.2.3 + yaml: 1.10.2 + + object-assign@4.1.1: {} + + object-hash@3.0.0: {} + + object-inspect@1.13.4: {} + + object-keys@1.1.1: {} + + object-path@0.11.8: {} + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + has-symbols: 1.1.0 + object-keys: 1.1.1 + + object.entries@1.1.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + object.fromentries@2.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-object-atoms: 1.1.1 + + object.groupby@1.0.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + + object.values@1.2.1: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + obuf@1.1.2: {} + + on-finished@2.3.0: + dependencies: + ee-first: 1.1.1 + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + on-headers@1.1.0: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + onetime@6.0.0: + dependencies: + mimic-fn: 4.0.0 + + onetime@7.0.0: + dependencies: + mimic-function: 5.0.1 + + open@8.4.2: + dependencies: + define-lazy-prop: 2.0.0 + is-docker: 2.2.1 + is-wsl: 2.2.0 + + openai@4.78.1(encoding@0.1.13)(zod@3.25.76): + dependencies: + '@types/node': 18.19.120 + '@types/node-fetch': 2.6.12 + abort-controller: 3.0.0 + agentkeepalive: 4.6.0 + form-data-encoder: 1.7.2 + formdata-node: 4.4.1 + node-fetch: 2.7.0(encoding@0.1.13) + optionalDependencies: + zod: 3.25.76 + transitivePeerDependencies: + - encoding + + openapi-to-postmanv2@4.25.0(encoding@0.1.13): + dependencies: + ajv: 8.11.0 + ajv-draft-04: 1.0.0(ajv@8.11.0) + ajv-formats: 2.1.1(ajv@8.11.0) + async: 3.2.4 + commander: 2.20.3 + graphlib: 2.1.8 + js-yaml: 4.1.0 + json-pointer: 0.6.2 + json-schema-merge-allof: 0.8.1 + lodash: 4.17.21 + neotraverse: 0.6.15 + oas-resolver-browser: 2.5.6 + object-hash: 3.0.0 + path-browserify: 1.0.1 + postman-collection: 4.5.0 + swagger2openapi: 7.0.8(encoding@0.1.13) + yaml: 1.10.2 + transitivePeerDependencies: + - encoding + + opener@1.5.2: {} + + opentype.js@1.3.4: + dependencies: + string.prototype.codepointat: 0.2.1 + tiny-inflate: 1.0.3 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + ora@5.4.1: + dependencies: + bl: 4.1.0 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-spinners: 2.9.2 + is-interactive: 1.0.0 + is-unicode-supported: 0.1.0 + log-symbols: 4.1.0 + strip-ansi: 6.0.1 + wcwidth: 1.0.1 + + os-tmpdir@1.0.2: {} + + ospath@1.2.2: {} + + outdent@0.5.0: {} + + own-keys@1.0.1: + dependencies: + get-intrinsic: 1.3.0 + object-keys: 1.1.1 + safe-push-apply: 1.0.0 + + oxc-resolver@11.6.0: + dependencies: + napi-postinstall: 0.3.2 + optionalDependencies: + '@oxc-resolver/binding-android-arm-eabi': 11.6.0 + '@oxc-resolver/binding-android-arm64': 11.6.0 + '@oxc-resolver/binding-darwin-arm64': 11.6.0 + '@oxc-resolver/binding-darwin-x64': 11.6.0 + '@oxc-resolver/binding-freebsd-x64': 11.6.0 + '@oxc-resolver/binding-linux-arm-gnueabihf': 11.6.0 + '@oxc-resolver/binding-linux-arm-musleabihf': 11.6.0 + '@oxc-resolver/binding-linux-arm64-gnu': 11.6.0 + '@oxc-resolver/binding-linux-arm64-musl': 11.6.0 + '@oxc-resolver/binding-linux-ppc64-gnu': 11.6.0 + '@oxc-resolver/binding-linux-riscv64-gnu': 11.6.0 + '@oxc-resolver/binding-linux-riscv64-musl': 11.6.0 + '@oxc-resolver/binding-linux-s390x-gnu': 11.6.0 + '@oxc-resolver/binding-linux-x64-gnu': 11.6.0 + '@oxc-resolver/binding-linux-x64-musl': 11.6.0 + '@oxc-resolver/binding-wasm32-wasi': 11.6.0 + '@oxc-resolver/binding-win32-arm64-msvc': 11.6.0 + '@oxc-resolver/binding-win32-ia32-msvc': 11.6.0 + '@oxc-resolver/binding-win32-x64-msvc': 11.6.0 + + p-cancelable@3.0.0: {} + + p-filter@2.1.0: + dependencies: + p-map: 2.1.0 + + p-filter@4.1.0: + dependencies: + p-map: 7.0.3 + + p-finally@1.0.0: {} + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-limit@4.0.0: + dependencies: + yocto-queue: 1.2.1 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + p-locate@6.0.0: + dependencies: + p-limit: 4.0.0 + + p-map@2.1.0: {} + + p-map@4.0.0: + dependencies: + aggregate-error: 3.1.0 + + p-map@7.0.3: {} + + p-queue@6.6.2: + dependencies: + eventemitter3: 4.0.7 + p-timeout: 3.2.0 + + p-retry@4.6.2: + dependencies: + '@types/retry': 0.12.0 + retry: 0.13.1 + + p-timeout@3.2.0: + dependencies: + p-finally: 1.0.0 + + p-try@2.2.0: {} + + pac-proxy-agent@7.2.0: + dependencies: + '@tootallnate/quickjs-emscripten': 0.23.0 + agent-base: 7.1.4 + debug: 4.4.1(supports-color@5.5.0) + get-uri: 6.0.5 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + pac-resolver: 7.0.1 + socks-proxy-agent: 8.0.5 + transitivePeerDependencies: + - supports-color + optional: true + + pac-resolver@7.0.1: + dependencies: + degenerator: 5.0.1 + netmask: 2.0.2 + optional: true + + package-json-from-dist@1.0.1: {} + + package-json@8.1.1: + dependencies: + got: 12.6.1 + registry-auth-token: 5.1.0 + registry-url: 6.0.1 + semver: 7.7.2 + + package-manager-detector@0.2.11: + dependencies: + quansync: 0.2.10 + + package-manager-detector@1.3.0: {} + + pacote@15.2.0: + dependencies: + '@npmcli/git': 4.1.0 + '@npmcli/installed-package-contents': 2.1.0 + '@npmcli/promise-spawn': 6.0.2 + '@npmcli/run-script': 6.0.2 + cacache: 17.1.4 + fs-minipass: 3.0.3 + minipass: 5.0.0 + npm-package-arg: 10.1.0 + npm-packlist: 7.0.4 + npm-pick-manifest: 8.0.1 + npm-registry-fetch: 14.0.5 + proc-log: 3.0.0 + promise-retry: 2.0.1 + read-package-json: 6.0.4 + read-package-json-fast: 3.0.2 + sigstore: 1.9.0 + ssri: 10.0.6 + tar: 6.2.1 + transitivePeerDependencies: + - bluebird + - supports-color + + pako@1.0.11: {} + + pako@2.1.0: {} + + param-case@3.0.4: + dependencies: + dot-case: 3.0.4 + tslib: 2.8.1 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-entities@4.0.2: + dependencies: + '@types/unist': 2.0.11 + character-entities-legacy: 3.0.0 + character-reference-invalid: 2.0.1 + decode-named-character-reference: 1.2.0 + is-alphanumerical: 2.0.1 + is-decimal: 2.0.1 + is-hexadecimal: 2.0.1 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.27.1 + error-ex: 1.3.2 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + parse-node-version@1.0.1: {} + + parse-numeric-range@1.3.0: {} + + parse5-html-rewriting-stream@7.0.0: + dependencies: + entities: 4.5.0 + parse5: 7.3.0 + parse5-sax-parser: 7.0.0 + + parse5-htmlparser2-tree-adapter@7.1.0: + dependencies: + domhandler: 5.0.3 + parse5: 7.3.0 + + parse5-sax-parser@7.0.0: + dependencies: + parse5: 7.3.0 + + parse5@6.0.1: {} + + parse5@7.3.0: + dependencies: + entities: 6.0.1 + + parseurl@1.3.3: {} + + pascal-case@3.1.2: + dependencies: + no-case: 3.0.4 + tslib: 2.8.1 + + path-browserify@1.0.1: {} + + path-data-parser@0.1.0: {} + + path-exists@4.0.0: {} + + path-exists@5.0.0: {} + + path-is-absolute@1.0.1: {} + + path-is-inside@1.0.2: {} + + path-key@3.1.1: {} + + path-key@4.0.0: {} + + path-parse@1.0.7: {} + + path-root-regex@0.1.2: {} + + path-root@0.1.1: + dependencies: + path-root-regex: 0.1.2 + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.2 + + path-scurry@2.0.0: + dependencies: + lru-cache: 11.1.0 + minipass: 7.1.2 + + path-to-regexp@0.1.12: {} + + path-to-regexp@1.9.0: + dependencies: + isarray: 0.0.1 + + path-to-regexp@3.3.0: {} + + path-type@4.0.0: {} + + path@0.12.7: + dependencies: + process: 0.11.10 + util: 0.10.4 + + pathe@1.1.2: {} + + pathe@2.0.3: {} + + pathval@2.0.1: {} + + pause-stream@0.0.11: + dependencies: + through: 2.3.8 + + pend@1.2.0: {} + + performance-now@2.1.0: {} + + pg-cloudflare@1.2.7: + optional: true + + pg-connection-string@2.9.1: {} + + pg-int8@1.0.1: {} + + pg-pool@3.10.1(pg@8.16.3): + dependencies: + pg: 8.16.3 + + pg-protocol@1.10.3: {} + + pg-types@2.2.0: + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.0 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + + pg@8.16.3: + dependencies: + pg-connection-string: 2.9.1 + pg-pool: 3.10.1(pg@8.16.3) + pg-protocol: 1.10.3 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.2.7 + + pgpass@1.0.5: + dependencies: + split2: 4.2.0 + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.3: {} + + pidtree@0.6.0: {} + + pify@2.3.0: {} + + pify@4.0.1: {} + + pirates@4.0.7: {} + + piscina@4.0.0: + dependencies: + eventemitter-asyncresource: 1.0.0 + hdr-histogram-js: 2.0.3 + hdr-histogram-percentiles-obj: 3.0.0 + optionalDependencies: + nice-napi: 1.0.2 + + pkg-dir@7.0.0: + dependencies: + find-up: 6.3.0 + + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.7.4 + pathe: 2.0.3 + + pkg-types@2.2.0: + dependencies: + confbox: 0.2.2 + exsolve: 1.0.7 + pathe: 2.0.3 + + playwright-core@1.54.1: {} + + playwright@1.54.1: + dependencies: + playwright-core: 1.54.1 + optionalDependencies: + fsevents: 2.3.2 + + pluralize@8.0.0: {} + + pngjs@5.0.0: {} + + points-on-curve@0.2.0: {} + + points-on-path@0.2.1: + dependencies: + path-data-parser: 0.1.0 + points-on-curve: 0.2.0 + + possible-typed-array-names@1.1.0: {} + + postcss-attribute-case-insensitive@7.0.1(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 7.1.0 + + postcss-calc@9.0.1(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + postcss-selector-parser: 6.1.2 + postcss-value-parser: 4.2.0 + + postcss-calc@9.0.1(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 6.1.2 + postcss-value-parser: 4.2.0 + + postcss-clamp@4.1.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-color-functional-notation@7.0.10(postcss@8.5.6): + dependencies: + '@csstools/css-color-parser': 3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/postcss-progressive-custom-properties': 4.1.0(postcss@8.5.6) + '@csstools/utilities': 2.0.0(postcss@8.5.6) + postcss: 8.5.6 + + postcss-color-hex-alpha@10.0.0(postcss@8.5.6): + dependencies: + '@csstools/utilities': 2.0.0(postcss@8.5.6) + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-color-rebeccapurple@10.0.0(postcss@8.5.6): + dependencies: + '@csstools/utilities': 2.0.0(postcss@8.5.6) + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-colormin@6.1.0(postcss@8.5.3): + dependencies: + browserslist: 4.25.1 + caniuse-api: 3.0.0 + colord: 2.9.3 + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + + postcss-colormin@6.1.0(postcss@8.5.6): + dependencies: + browserslist: 4.25.1 + caniuse-api: 3.0.0 + colord: 2.9.3 + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-convert-values@6.1.0(postcss@8.5.3): + dependencies: + browserslist: 4.25.1 + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + + postcss-convert-values@6.1.0(postcss@8.5.6): + dependencies: + browserslist: 4.25.1 + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-custom-media@11.0.6(postcss@8.5.6): + dependencies: + '@csstools/cascade-layer-name-parser': 2.0.5(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/media-query-list-parser': 4.0.3(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + postcss: 8.5.6 + + postcss-custom-properties@14.0.6(postcss@8.5.6): + dependencies: + '@csstools/cascade-layer-name-parser': 2.0.5(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/utilities': 2.0.0(postcss@8.5.6) + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-custom-selectors@8.0.5(postcss@8.5.6): + dependencies: + '@csstools/cascade-layer-name-parser': 2.0.5(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + postcss: 8.5.6 + postcss-selector-parser: 7.1.0 + + postcss-dir-pseudo-class@9.0.1(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 7.1.0 + + postcss-discard-comments@6.0.2(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + + postcss-discard-comments@6.0.2(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-discard-duplicates@6.0.3(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + + postcss-discard-duplicates@6.0.3(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-discard-empty@6.0.3(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + + postcss-discard-empty@6.0.3(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-discard-overridden@6.0.2(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + + postcss-discard-overridden@6.0.2(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-discard-unused@6.0.5(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 6.1.2 + + postcss-double-position-gradients@6.0.2(postcss@8.5.6): + dependencies: + '@csstools/postcss-progressive-custom-properties': 4.1.0(postcss@8.5.6) + '@csstools/utilities': 2.0.0(postcss@8.5.6) + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-focus-visible@10.0.1(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 7.1.0 + + postcss-focus-within@9.0.1(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 7.1.0 + + postcss-font-variant@5.0.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-gap-properties@6.0.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-image-set-function@7.0.0(postcss@8.5.6): + dependencies: + '@csstools/utilities': 2.0.0(postcss@8.5.6) + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-import@15.1.0(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + read-cache: 1.0.0 + resolve: 1.22.10 + + postcss-js@4.0.1(postcss@8.5.3): + dependencies: + camelcase-css: 2.0.1 + postcss: 8.5.3 + + postcss-lab-function@7.0.10(postcss@8.5.6): + dependencies: + '@csstools/css-color-parser': 3.0.10(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/postcss-progressive-custom-properties': 4.1.0(postcss@8.5.6) + '@csstools/utilities': 2.0.0(postcss@8.5.6) + postcss: 8.5.6 + + postcss-load-config@4.0.2(postcss@8.5.3): + dependencies: + lilconfig: 3.1.3 + yaml: 2.8.0 + optionalDependencies: + postcss: 8.5.3 + + postcss-load-config@6.0.1(jiti@2.4.2)(postcss@8.5.6)(yaml@2.8.0): + dependencies: + lilconfig: 3.1.3 + optionalDependencies: + jiti: 2.4.2 + postcss: 8.5.6 + yaml: 2.8.0 + + postcss-loader@7.3.3(postcss@8.4.31)(typescript@5.1.6)(webpack@5.94.0(@swc/core@1.13.1)(esbuild@0.18.17)): + dependencies: + cosmiconfig: 8.3.6(typescript@5.1.6) + jiti: 1.21.7 + postcss: 8.4.31 + semver: 7.7.2 + webpack: 5.94.0(@swc/core@1.13.1)(esbuild@0.18.17) + transitivePeerDependencies: + - typescript + + postcss-loader@7.3.4(postcss@8.5.6)(typescript@5.8.3)(webpack@5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17))): + dependencies: + cosmiconfig: 8.3.6(typescript@5.8.3) + jiti: 1.21.7 + postcss: 8.5.6 + semver: 7.7.2 + webpack: 5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17)) + transitivePeerDependencies: + - typescript + + postcss-logical@8.1.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-merge-idents@6.0.3(postcss@8.5.6): + dependencies: + cssnano-utils: 4.0.2(postcss@8.5.6) + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-merge-longhand@6.0.5(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + stylehacks: 6.1.1(postcss@8.5.3) + + postcss-merge-longhand@6.0.5(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + stylehacks: 6.1.1(postcss@8.5.6) + + postcss-merge-rules@6.1.1(postcss@8.5.3): + dependencies: + browserslist: 4.25.1 + caniuse-api: 3.0.0 + cssnano-utils: 4.0.2(postcss@8.5.3) + postcss: 8.5.3 + postcss-selector-parser: 6.1.2 + + postcss-merge-rules@6.1.1(postcss@8.5.6): + dependencies: + browserslist: 4.25.1 + caniuse-api: 3.0.0 + cssnano-utils: 4.0.2(postcss@8.5.6) + postcss: 8.5.6 + postcss-selector-parser: 6.1.2 + + postcss-minify-font-values@6.1.0(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + + postcss-minify-font-values@6.1.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-minify-gradients@6.0.3(postcss@8.5.3): + dependencies: + colord: 2.9.3 + cssnano-utils: 4.0.2(postcss@8.5.3) + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + + postcss-minify-gradients@6.0.3(postcss@8.5.6): + dependencies: + colord: 2.9.3 + cssnano-utils: 4.0.2(postcss@8.5.6) + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-minify-params@6.1.0(postcss@8.5.3): + dependencies: + browserslist: 4.25.1 + cssnano-utils: 4.0.2(postcss@8.5.3) + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + + postcss-minify-params@6.1.0(postcss@8.5.6): + dependencies: + browserslist: 4.25.1 + cssnano-utils: 4.0.2(postcss@8.5.6) + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-minify-selectors@6.0.4(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + postcss-selector-parser: 6.1.2 + + postcss-minify-selectors@6.0.4(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 6.1.2 + + postcss-modules-extract-imports@3.1.0(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + + postcss-modules-local-by-default@4.2.0(postcss@8.5.3): + dependencies: + icss-utils: 5.1.0(postcss@8.5.3) + postcss: 8.5.3 + postcss-selector-parser: 7.1.0 + postcss-value-parser: 4.2.0 + + postcss-modules-scope@3.2.1(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + postcss-selector-parser: 7.1.0 + + postcss-modules-values@4.0.0(postcss@8.5.3): + dependencies: + icss-utils: 5.1.0(postcss@8.5.3) + postcss: 8.5.3 + + postcss-nested@6.2.0(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + postcss-selector-parser: 6.1.2 + + postcss-nesting@13.0.2(postcss@8.5.6): + dependencies: + '@csstools/selector-resolve-nested': 3.1.0(postcss-selector-parser@7.1.0) + '@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.1.0) + postcss: 8.5.6 + postcss-selector-parser: 7.1.0 + + postcss-normalize-charset@6.0.2(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + + postcss-normalize-charset@6.0.2(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-normalize-display-values@6.0.2(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + + postcss-normalize-display-values@6.0.2(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-normalize-positions@6.0.2(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + + postcss-normalize-positions@6.0.2(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-normalize-repeat-style@6.0.2(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + + postcss-normalize-repeat-style@6.0.2(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-normalize-string@6.0.2(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + + postcss-normalize-string@6.0.2(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-normalize-timing-functions@6.0.2(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + + postcss-normalize-timing-functions@6.0.2(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-normalize-unicode@6.1.0(postcss@8.5.3): + dependencies: + browserslist: 4.25.1 + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + + postcss-normalize-unicode@6.1.0(postcss@8.5.6): + dependencies: + browserslist: 4.25.1 + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-normalize-url@6.0.2(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + + postcss-normalize-url@6.0.2(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-normalize-whitespace@6.0.2(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + + postcss-normalize-whitespace@6.0.2(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-opacity-percentage@3.0.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-ordered-values@6.0.2(postcss@8.5.3): + dependencies: + cssnano-utils: 4.0.2(postcss@8.5.3) + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + + postcss-ordered-values@6.0.2(postcss@8.5.6): + dependencies: + cssnano-utils: 4.0.2(postcss@8.5.6) + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-overflow-shorthand@6.0.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-page-break@3.0.4(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-place@10.0.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-preset-env@10.2.4(postcss@8.5.6): + dependencies: + '@csstools/postcss-cascade-layers': 5.0.2(postcss@8.5.6) + '@csstools/postcss-color-function': 4.0.10(postcss@8.5.6) + '@csstools/postcss-color-mix-function': 3.0.10(postcss@8.5.6) + '@csstools/postcss-color-mix-variadic-function-arguments': 1.0.0(postcss@8.5.6) + '@csstools/postcss-content-alt-text': 2.0.6(postcss@8.5.6) + '@csstools/postcss-exponential-functions': 2.0.9(postcss@8.5.6) + '@csstools/postcss-font-format-keywords': 4.0.0(postcss@8.5.6) + '@csstools/postcss-gamut-mapping': 2.0.10(postcss@8.5.6) + '@csstools/postcss-gradients-interpolation-method': 5.0.10(postcss@8.5.6) + '@csstools/postcss-hwb-function': 4.0.10(postcss@8.5.6) + '@csstools/postcss-ic-unit': 4.0.2(postcss@8.5.6) + '@csstools/postcss-initial': 2.0.1(postcss@8.5.6) + '@csstools/postcss-is-pseudo-class': 5.0.3(postcss@8.5.6) + '@csstools/postcss-light-dark-function': 2.0.9(postcss@8.5.6) + '@csstools/postcss-logical-float-and-clear': 3.0.0(postcss@8.5.6) + '@csstools/postcss-logical-overflow': 2.0.0(postcss@8.5.6) + '@csstools/postcss-logical-overscroll-behavior': 2.0.0(postcss@8.5.6) + '@csstools/postcss-logical-resize': 3.0.0(postcss@8.5.6) + '@csstools/postcss-logical-viewport-units': 3.0.4(postcss@8.5.6) + '@csstools/postcss-media-minmax': 2.0.9(postcss@8.5.6) + '@csstools/postcss-media-queries-aspect-ratio-number-values': 3.0.5(postcss@8.5.6) + '@csstools/postcss-nested-calc': 4.0.0(postcss@8.5.6) + '@csstools/postcss-normalize-display-values': 4.0.0(postcss@8.5.6) + '@csstools/postcss-oklab-function': 4.0.10(postcss@8.5.6) + '@csstools/postcss-progressive-custom-properties': 4.1.0(postcss@8.5.6) + '@csstools/postcss-random-function': 2.0.1(postcss@8.5.6) + '@csstools/postcss-relative-color-syntax': 3.0.10(postcss@8.5.6) + '@csstools/postcss-scope-pseudo-class': 4.0.1(postcss@8.5.6) + '@csstools/postcss-sign-functions': 1.1.4(postcss@8.5.6) + '@csstools/postcss-stepped-value-functions': 4.0.9(postcss@8.5.6) + '@csstools/postcss-text-decoration-shorthand': 4.0.2(postcss@8.5.6) + '@csstools/postcss-trigonometric-functions': 4.0.9(postcss@8.5.6) + '@csstools/postcss-unset-value': 4.0.0(postcss@8.5.6) + autoprefixer: 10.4.21(postcss@8.5.6) + browserslist: 4.25.1 + css-blank-pseudo: 7.0.1(postcss@8.5.6) + css-has-pseudo: 7.0.2(postcss@8.5.6) + css-prefers-color-scheme: 10.0.0(postcss@8.5.6) + cssdb: 8.3.1 + postcss: 8.5.6 + postcss-attribute-case-insensitive: 7.0.1(postcss@8.5.6) + postcss-clamp: 4.1.0(postcss@8.5.6) + postcss-color-functional-notation: 7.0.10(postcss@8.5.6) + postcss-color-hex-alpha: 10.0.0(postcss@8.5.6) + postcss-color-rebeccapurple: 10.0.0(postcss@8.5.6) + postcss-custom-media: 11.0.6(postcss@8.5.6) + postcss-custom-properties: 14.0.6(postcss@8.5.6) + postcss-custom-selectors: 8.0.5(postcss@8.5.6) + postcss-dir-pseudo-class: 9.0.1(postcss@8.5.6) + postcss-double-position-gradients: 6.0.2(postcss@8.5.6) + postcss-focus-visible: 10.0.1(postcss@8.5.6) + postcss-focus-within: 9.0.1(postcss@8.5.6) + postcss-font-variant: 5.0.0(postcss@8.5.6) + postcss-gap-properties: 6.0.0(postcss@8.5.6) + postcss-image-set-function: 7.0.0(postcss@8.5.6) + postcss-lab-function: 7.0.10(postcss@8.5.6) + postcss-logical: 8.1.0(postcss@8.5.6) + postcss-nesting: 13.0.2(postcss@8.5.6) + postcss-opacity-percentage: 3.0.0(postcss@8.5.6) + postcss-overflow-shorthand: 6.0.0(postcss@8.5.6) + postcss-page-break: 3.0.4(postcss@8.5.6) + postcss-place: 10.0.0(postcss@8.5.6) + postcss-pseudo-class-any-link: 10.0.1(postcss@8.5.6) + postcss-replace-overflow-wrap: 4.0.0(postcss@8.5.6) + postcss-selector-not: 8.0.1(postcss@8.5.6) + + postcss-pseudo-class-any-link@10.0.1(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 7.1.0 + + postcss-reduce-idents@6.0.3(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-reduce-initial@6.1.0(postcss@8.5.3): + dependencies: + browserslist: 4.25.1 + caniuse-api: 3.0.0 + postcss: 8.5.3 + + postcss-reduce-initial@6.1.0(postcss@8.5.6): + dependencies: + browserslist: 4.25.1 + caniuse-api: 3.0.0 + postcss: 8.5.6 + + postcss-reduce-transforms@6.0.2(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + + postcss-reduce-transforms@6.0.2(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + postcss-replace-overflow-wrap@4.0.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss-selector-not@8.0.1(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 7.1.0 + + postcss-selector-parser@6.1.2: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-selector-parser@7.1.0: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-sort-media-queries@5.2.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + sort-css-media-queries: 2.2.0 + + postcss-svgo@6.0.3(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + postcss-value-parser: 4.2.0 + svgo: 3.3.2 + + postcss-svgo@6.0.3(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + svgo: 3.3.2 + + postcss-unique-selectors@6.0.4(postcss@8.5.3): + dependencies: + postcss: 8.5.3 + postcss-selector-parser: 6.1.2 + + postcss-unique-selectors@6.0.4(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 6.1.2 + + postcss-value-parser@4.2.0: {} + + postcss-zindex@6.0.2(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + + postcss@8.4.31: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + postcss@8.5.3: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + postgres-array@2.0.0: {} + + postgres-bytea@1.0.0: {} + + postgres-date@1.0.7: {} + + postgres-interval@1.2.0: + dependencies: + xtend: 4.0.2 + + posthog-js@1.257.1: + dependencies: + core-js: 3.44.0 + fflate: 0.4.8 + preact: 10.26.9 + web-vitals: 4.2.4 + + postman-code-generators@1.14.2: + dependencies: + async: 3.2.2 + detect-package-manager: 3.0.2 + lodash: 4.17.21 + path: 0.12.7 + postman-collection: 4.5.0 + shelljs: 0.8.5 + + postman-collection@4.5.0: + dependencies: + '@faker-js/faker': 5.5.3 + file-type: 3.9.0 + http-reasons: 0.1.0 + iconv-lite: 0.6.3 + liquid-json: 0.3.1 + lodash: 4.17.21 + mime-format: 2.0.1 + mime-types: 2.1.35 + postman-url-encoder: 3.0.5 + semver: 7.6.3 + uuid: 8.3.2 + + postman-url-encoder@3.0.5: + dependencies: + punycode: 2.3.1 + + preact@10.26.9: {} + + prelude-ls@1.2.1: {} + + prettier-plugin-organize-imports@3.2.4(prettier@3.6.2)(typescript@5.8.3): + dependencies: + prettier: 3.6.2 + typescript: 5.8.3 + + prettier-plugin-organize-imports@4.2.0(prettier@3.6.2)(typescript@5.1.6): + dependencies: + prettier: 3.6.2 + typescript: 5.1.6 + + prettier-plugin-tailwindcss@0.6.11(prettier-plugin-organize-imports@3.2.4(prettier@3.6.2)(typescript@5.8.3))(prettier@3.6.2): + dependencies: + prettier: 3.6.2 + optionalDependencies: + prettier-plugin-organize-imports: 3.2.4(prettier@3.6.2)(typescript@5.8.3) + + prettier@2.8.8: {} + + prettier@3.6.2: {} + + pretty-bytes@5.6.0: {} + + pretty-error@4.0.0: + dependencies: + lodash: 4.17.21 + renderkid: 3.0.0 + + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + + pretty-time@1.1.0: {} + + prism-react-renderer@2.4.1(react@18.3.1): + dependencies: + '@types/prismjs': 1.26.5 + clsx: 2.1.1 + react: 18.3.1 + + prismjs@1.30.0: {} + + proc-log@3.0.0: {} + + process-nextick-args@2.0.1: {} + + process@0.11.10: {} + + progress@2.0.3: + optional: true + + promise-inflight@1.0.1: {} + + promise-retry@2.0.1: + dependencies: + err-code: 2.0.3 + retry: 0.12.0 + + prompts@2.4.2: + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + property-information@6.5.0: {} + + property-information@7.1.0: {} + + proto-list@1.2.4: {} + + protobufjs@7.5.3: + 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': 22.16.5 + long: 5.3.2 + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + proxy-agent@6.5.0: + dependencies: + agent-base: 7.1.4 + debug: 4.4.1(supports-color@5.5.0) + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + lru-cache: 7.18.3 + pac-proxy-agent: 7.2.0 + proxy-from-env: 1.1.0 + socks-proxy-agent: 8.0.5 + transitivePeerDependencies: + - supports-color + optional: true + + proxy-compare@3.0.1: {} + + proxy-from-env@1.0.0: {} + + proxy-from-env@1.1.0: {} + + prr@1.0.1: + optional: true + + ps-tree@1.2.0: + dependencies: + event-stream: 3.3.4 + + psl@1.15.0: + dependencies: + punycode: 2.3.1 + + pstree.remy@1.1.8: {} + + pump@3.0.3: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + + punycode@1.4.1: {} + + punycode@2.3.1: {} + + pupa@3.1.0: + dependencies: + escape-goat: 4.0.0 + + puppeteer-core@22.15.0: + dependencies: + '@puppeteer/browsers': 2.3.0 + chromium-bidi: 0.6.3(devtools-protocol@0.0.1312386) + debug: 4.4.1(supports-color@5.5.0) + devtools-protocol: 0.0.1312386 + ws: 8.18.3 + transitivePeerDependencies: + - bare-buffer + - bufferutil + - supports-color + - utf-8-validate + optional: true + + puppeteer@22.15.0(typescript@5.8.3): + dependencies: + '@puppeteer/browsers': 2.3.0 + cosmiconfig: 9.0.0(typescript@5.8.3) + devtools-protocol: 0.0.1312386 + puppeteer-core: 22.15.0 + transitivePeerDependencies: + - bare-buffer + - bufferutil + - supports-color + - typescript + - utf-8-validate + optional: true + + qjobs@1.2.0: {} + + qrcode.react@3.2.0(react@19.1.0): + dependencies: + react: 19.1.0 + + qrcode@1.5.3: + dependencies: + dijkstrajs: 1.0.3 + encode-utf8: 1.0.3 + pngjs: 5.0.0 + yargs: 15.4.1 + + qs@6.13.0: + dependencies: + side-channel: 1.1.0 + + qs@6.14.0: + dependencies: + side-channel: 1.1.0 + + quansync@0.2.10: {} + + querystringify@2.2.0: {} + + queue-microtask@1.2.3: {} + + quick-lru@5.1.1: {} + + randombytes@2.1.0: + dependencies: + safe-buffer: 5.2.1 + + range-parser@1.2.0: {} + + range-parser@1.2.1: {} + + raw-body@2.5.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + + raw-loader@4.0.2(webpack@5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17))): + dependencies: + loader-utils: 2.0.4 + schema-utils: 3.3.0 + webpack: 5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17)) + + rc@1.2.8: + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + + react-copy-to-clipboard@5.1.0(react@18.3.1): + dependencies: + copy-to-clipboard: 3.3.3 + prop-types: 15.8.1 + react: 18.3.1 + + react-dom@18.3.1(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + + react-dom@19.1.0(react@19.1.0): + dependencies: + react: 19.1.0 + scheduler: 0.26.0 + + react-error-boundary@6.0.0(react@18.3.1): + dependencies: + '@babel/runtime': 7.27.6 + react: 18.3.1 + + react-fast-compare@3.2.2: {} + + react-google-charts@5.2.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + react-hook-form@7.39.5(react@19.1.0): + dependencies: + react: 19.1.0 + + react-hook-form@7.54.2(react@18.3.1): + dependencies: + react: 18.3.1 + + react-hook-form@7.60.0(react@18.3.1): + dependencies: + react: 18.3.1 + + react-is@16.13.1: {} + + react-is@17.0.2: {} + + react-is@18.3.1: {} + + react-json-view-lite@2.4.1(react@18.3.1): + dependencies: + react: 18.3.1 + + react-lifecycles-compat@3.0.4: {} + + react-live@4.1.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + prism-react-renderer: 2.4.1(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + sucrase: 3.35.0 + use-editable: 2.3.3(react@18.3.1) + + react-loadable-ssr-addon-v5-slorber@1.0.1(@docusaurus/react-loadable@6.0.0(react@18.3.1))(webpack@5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17))): + dependencies: + '@babel/runtime': 7.27.6 + react-loadable: '@docusaurus/react-loadable@6.0.0(react@18.3.1)' + webpack: 5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17)) + + react-magic-dropzone@1.0.1: {} + + react-markdown@8.0.7(@types/react@19.1.2)(react@18.3.1): + dependencies: + '@types/hast': 2.3.10 + '@types/prop-types': 15.7.15 + '@types/react': 19.1.2 + '@types/unist': 2.0.11 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 2.0.1 + prop-types: 15.8.1 + property-information: 6.5.0 + react: 18.3.1 + react-is: 18.3.1 + remark-parse: 10.0.2 + remark-rehype: 10.1.0 + space-separated-tokens: 2.0.2 + style-to-object: 0.4.4 + unified: 10.1.2 + unist-util-visit: 4.1.2 + vfile: 5.3.7 + transitivePeerDependencies: + - supports-color + + react-markdown@9.0.3(@types/react@19.1.2)(react@18.3.1): + dependencies: + '@types/hast': 3.0.4 + '@types/react': 19.1.2 + devlop: 1.1.0 + hast-util-to-jsx-runtime: 2.3.6 + html-url-attributes: 3.0.1 + mdast-util-to-hast: 13.2.0 + react: 18.3.1 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + unified: 11.0.5 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + + react-modal@3.16.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + exenv: 1.2.2 + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-lifecycles-compat: 3.0.4 + warning: 4.0.3 + + react-player@2.16.1(react@18.3.1): + dependencies: + deepmerge: 4.3.1 + load-script: 1.0.0 + memoize-one: 5.2.1 + prop-types: 15.8.1 + react: 18.3.1 + react-fast-compare: 3.2.2 + + react-redux@7.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.27.6 + '@types/react-redux': 7.1.34 + hoist-non-react-statics: 3.3.2 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 18.3.1 + react-is: 17.0.2 + optionalDependencies: + react-dom: 18.3.1(react@18.3.1) + + react-refresh@0.17.0: {} + + react-remove-scroll-bar@2.3.8(@types/react@19.1.2)(react@18.3.1): + dependencies: + react: 18.3.1 + react-style-singleton: 2.2.3(@types/react@19.1.2)(react@18.3.1) + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.1.2 + + react-remove-scroll@2.7.1(@types/react@19.1.2)(react@18.3.1): + dependencies: + react: 18.3.1 + react-remove-scroll-bar: 2.3.8(@types/react@19.1.2)(react@18.3.1) + react-style-singleton: 2.2.3(@types/react@19.1.2)(react@18.3.1) + tslib: 2.8.1 + use-callback-ref: 1.3.3(@types/react@19.1.2)(react@18.3.1) + use-sidecar: 1.1.3(@types/react@19.1.2)(react@18.3.1) + optionalDependencies: + '@types/react': 19.1.2 + + react-router-config@5.1.1(react-router@5.3.4(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.27.6 + react: 18.3.1 + react-router: 5.3.4(react@18.3.1) + + react-router-dom@5.3.4(react@18.3.1): + dependencies: + '@babel/runtime': 7.27.6 + history: 4.10.1 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 18.3.1 + react-router: 5.3.4(react@18.3.1) + tiny-invariant: 1.3.3 + tiny-warning: 1.0.3 + + react-router@5.3.4(react@18.3.1): + dependencies: + '@babel/runtime': 7.27.6 + history: 4.10.1 + hoist-non-react-statics: 3.3.2 + loose-envify: 1.4.0 + path-to-regexp: 1.9.0 + prop-types: 15.8.1 + react: 18.3.1 + react-is: 16.13.1 + tiny-invariant: 1.3.3 + tiny-warning: 1.0.3 + + react-style-singleton@2.2.3(@types/react@19.1.2)(react@18.3.1): + dependencies: + get-nonce: 1.0.1 + react: 18.3.1 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.1.2 + + react-svg@16.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.27.6 + '@tanem/svg-injector': 10.1.68 + '@types/prop-types': 15.7.15 + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + react-textarea-autosize@8.5.7(@types/react@19.1.2)(react@18.3.1): + dependencies: + '@babel/runtime': 7.27.6 + react: 18.3.1 + use-composed-ref: 1.4.0(@types/react@19.1.2)(react@18.3.1) + use-latest: 1.3.0(@types/react@19.1.2)(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + + react@18.3.1: + dependencies: + loose-envify: 1.4.0 + + react@19.1.0: {} + + read-cache@1.0.0: + dependencies: + pify: 2.3.0 + + read-package-json-fast@3.0.2: + dependencies: + json-parse-even-better-errors: 3.0.2 + npm-normalize-package-bin: 3.0.1 + + read-package-json@6.0.4: + dependencies: + glob: 10.4.5 + json-parse-even-better-errors: 3.0.2 + normalize-package-data: 5.0.0 + npm-normalize-package-bin: 3.0.1 + + read-pkg-up@9.1.0: + dependencies: + find-up: 6.3.0 + read-pkg: 7.1.0 + type-fest: 2.19.0 + + read-pkg@7.1.0: + dependencies: + '@types/normalize-package-data': 2.4.4 + normalize-package-data: 3.0.3 + parse-json: 5.2.0 + type-fest: 2.19.0 + + read-yaml-file@1.1.0: + dependencies: + graceful-fs: 4.2.11 + js-yaml: 3.14.1 + pify: 4.0.1 + strip-bom: 3.0.0 + + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + readdirp@4.1.2: {} + + rechoir@0.6.2: + dependencies: + resolve: 1.22.10 + + recma-build-jsx@1.0.0: + dependencies: + '@types/estree': 1.0.8 + estree-util-build-jsx: 3.0.1 + vfile: 6.0.3 + + recma-jsx@1.0.0(acorn@8.15.0): + dependencies: + acorn-jsx: 5.3.2(acorn@8.15.0) + estree-util-to-js: 2.0.0 + recma-parse: 1.0.0 + recma-stringify: 1.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - acorn + + recma-parse@1.0.0: + dependencies: + '@types/estree': 1.0.8 + esast-util-from-js: 2.0.1 + unified: 11.0.5 + vfile: 6.0.3 + + recma-stringify@1.0.0: + dependencies: + '@types/estree': 1.0.8 + estree-util-to-js: 2.0.0 + unified: 11.0.5 + vfile: 6.0.3 + + redent@3.0.0: + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + + redux-thunk@2.4.2(redux@4.2.1): + dependencies: + redux: 4.2.1 + + redux@4.2.1: + dependencies: + '@babel/runtime': 7.27.6 + + reflect-metadata@0.1.14: {} + + reflect.getprototypeof@1.0.10: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + which-builtin-type: 1.2.1 + + reftools@1.1.9: {} + + regenerate-unicode-properties@10.2.0: + dependencies: + regenerate: 1.4.2 + + regenerate@1.4.2: {} + + regenerator-runtime@0.13.11: {} + + regex-parser@2.3.1: {} + + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + + regexpu-core@6.2.0: + dependencies: + regenerate: 1.4.2 + regenerate-unicode-properties: 10.2.0 + regjsgen: 0.8.0 + regjsparser: 0.12.0 + unicode-match-property-ecmascript: 2.0.0 + unicode-match-property-value-ecmascript: 2.2.0 + + registry-auth-token@5.1.0: + dependencies: + '@pnpm/npm-conf': 2.3.1 + + registry-url@6.0.1: + dependencies: + rc: 1.2.8 + + regjsgen@0.8.0: {} + + regjsparser@0.12.0: + dependencies: + jsesc: 3.0.2 + + rehype-minify-whitespace@6.0.2: + dependencies: + '@types/hast': 3.0.4 + hast-util-minify-whitespace: 1.0.1 + + rehype-parse@9.0.1: + dependencies: + '@types/hast': 3.0.4 + hast-util-from-html: 2.0.3 + unified: 11.0.5 + + rehype-raw@6.1.1: + dependencies: + '@types/hast': 2.3.10 + hast-util-raw: 7.2.3 + unified: 10.1.2 + + rehype-raw@7.0.0: + dependencies: + '@types/hast': 3.0.4 + hast-util-raw: 9.1.0 + vfile: 6.0.3 + + rehype-recma@1.0.0: + dependencies: + '@types/estree': 1.0.8 + '@types/hast': 3.0.4 + hast-util-to-estree: 3.1.3 + transitivePeerDependencies: + - supports-color + + rehype-remark@10.0.1: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + hast-util-to-mdast: 10.1.2 + unified: 11.0.5 + vfile: 6.0.3 + + relateurl@0.2.7: {} + + remark-directive@3.0.1: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-directive: 3.1.0 + micromark-extension-directive: 3.0.2 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-emoji@4.0.1: + dependencies: + '@types/mdast': 4.0.4 + emoticon: 4.1.0 + mdast-util-find-and-replace: 3.0.2 + node-emoji: 2.2.0 + unified: 11.0.5 + + remark-frontmatter@5.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-frontmatter: 2.0.1 + micromark-extension-frontmatter: 2.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-gfm@3.0.1: + dependencies: + '@types/mdast': 3.0.15 + mdast-util-gfm: 2.0.2 + micromark-extension-gfm: 2.0.3 + unified: 10.1.2 + transitivePeerDependencies: + - supports-color + + remark-gfm@4.0.1: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-gfm: 3.1.0 + micromark-extension-gfm: 3.0.0 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-mdx@3.1.0: + dependencies: + mdast-util-mdx: 3.0.0 + micromark-extension-mdxjs: 3.0.0 + transitivePeerDependencies: + - supports-color + + remark-parse@10.0.2: + dependencies: + '@types/mdast': 3.0.15 + mdast-util-from-markdown: 1.3.1 + unified: 10.1.2 + transitivePeerDependencies: + - supports-color + + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.2 + micromark-util-types: 2.0.2 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-rehype@10.1.0: + dependencies: + '@types/hast': 2.3.10 + '@types/mdast': 3.0.15 + mdast-util-to-hast: 12.3.0 + unified: 10.1.2 + + remark-rehype@11.1.2: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + mdast-util-to-hast: 13.2.0 + unified: 11.0.5 + vfile: 6.0.3 + + remark-stringify@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-to-markdown: 2.1.2 + unified: 11.0.5 + + renderkid@3.0.0: + dependencies: + css-select: 4.3.0 + dom-converter: 0.2.0 + htmlparser2: 6.1.0 + lodash: 4.17.21 + strip-ansi: 6.0.1 + + repeat-string@1.6.1: {} + + request-progress@3.0.0: + dependencies: + throttleit: 1.0.1 + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + require-like@0.1.2: {} + + require-main-filename@2.0.0: {} + + requires-port@1.0.0: {} + + reselect@4.1.8: {} + + resolve-alpn@1.2.1: {} + + resolve-from@4.0.0: {} + + resolve-from@5.0.0: {} + + resolve-package-path@4.0.3: + dependencies: + path-root: 0.1.1 + + resolve-pathname@3.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + resolve-url-loader@5.0.0: + dependencies: + adjust-sourcemap-loader: 4.0.0 + convert-source-map: 1.9.0 + loader-utils: 2.0.4 + postcss: 8.5.3 + source-map: 0.6.1 + + resolve@1.22.10: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + resolve@1.22.2: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + resolve@2.0.0-next.5: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + responselike@3.0.0: + dependencies: + lowercase-keys: 3.0.0 + + restore-cursor@3.1.0: + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + + restore-cursor@5.1.0: + dependencies: + onetime: 7.0.0 + signal-exit: 4.1.0 + + retry@0.12.0: {} + + retry@0.13.1: {} + + reusify@1.1.0: {} + + rfdc@1.4.1: {} + + rimraf@2.7.1: + dependencies: + glob: 7.2.3 + + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + + robust-predicates@3.0.2: {} + + rollup@3.29.5: + optionalDependencies: + fsevents: 2.3.3 + + rollup@4.45.1: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.45.1 + '@rollup/rollup-android-arm64': 4.45.1 + '@rollup/rollup-darwin-arm64': 4.45.1 + '@rollup/rollup-darwin-x64': 4.45.1 + '@rollup/rollup-freebsd-arm64': 4.45.1 + '@rollup/rollup-freebsd-x64': 4.45.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.45.1 + '@rollup/rollup-linux-arm-musleabihf': 4.45.1 + '@rollup/rollup-linux-arm64-gnu': 4.45.1 + '@rollup/rollup-linux-arm64-musl': 4.45.1 + '@rollup/rollup-linux-loongarch64-gnu': 4.45.1 + '@rollup/rollup-linux-powerpc64le-gnu': 4.45.1 + '@rollup/rollup-linux-riscv64-gnu': 4.45.1 + '@rollup/rollup-linux-riscv64-musl': 4.45.1 + '@rollup/rollup-linux-s390x-gnu': 4.45.1 + '@rollup/rollup-linux-x64-gnu': 4.45.1 + '@rollup/rollup-linux-x64-musl': 4.45.1 + '@rollup/rollup-win32-arm64-msvc': 4.45.1 + '@rollup/rollup-win32-ia32-msvc': 4.45.1 + '@rollup/rollup-win32-x64-msvc': 4.45.1 + fsevents: 2.3.3 + + roughjs@4.6.6: + dependencies: + hachure-fill: 0.5.2 + path-data-parser: 0.1.0 + points-on-curve: 0.2.0 + points-on-path: 0.2.1 + + rrweb-cssom@0.8.0: {} + + rtlcss@4.3.0: + dependencies: + escalade: 3.2.0 + picocolors: 1.1.1 + postcss: 8.5.3 + strip-json-comments: 3.1.1 + + run-async@2.4.1: {} + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + rw@1.3.3: {} + + rxjs@7.8.1: + dependencies: + tslib: 2.8.1 + + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + + sade@1.8.1: + dependencies: + mri: 1.2.0 + + safe-array-concat@1.1.3: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + isarray: 2.0.5 + + safe-buffer@5.1.2: {} + + safe-buffer@5.2.1: {} + + safe-push-apply@1.0.0: + dependencies: + es-errors: 1.3.0 + isarray: 2.0.5 + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + + safer-buffer@2.1.2: {} + + safevalues@0.3.4: {} + + sass-loader@13.3.2(sass@1.64.1)(webpack@5.94.0(@swc/core@1.13.1)(esbuild@0.18.17)): + dependencies: + neo-async: 2.6.2 + webpack: 5.94.0(@swc/core@1.13.1)(esbuild@0.18.17) + optionalDependencies: + sass: 1.64.1 + + sass-loader@16.0.5(@rspack/core@1.4.9(@swc/helpers@0.5.17))(sass@1.89.2)(webpack@5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17))): + dependencies: + neo-async: 2.6.2 + optionalDependencies: + '@rspack/core': 1.4.9(@swc/helpers@0.5.17) + sass: 1.89.2 + webpack: 5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17)) + + sass@1.64.1: + dependencies: + chokidar: 3.5.3 + immutable: 4.3.7 + source-map-js: 1.2.1 + + sass@1.89.2: + dependencies: + chokidar: 4.0.3 + immutable: 5.1.3 + source-map-js: 1.2.1 + optionalDependencies: + '@parcel/watcher': 2.5.1 + + sax@1.4.1: {} + + saxes@5.0.1: + dependencies: + xmlchars: 2.2.0 + + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + + scheduler@0.23.2: + dependencies: + loose-envify: 1.4.0 + + scheduler@0.26.0: {} + + schema-dts@1.1.5: {} + + schema-utils@3.3.0: + dependencies: + '@types/json-schema': 7.0.15 + ajv: 6.12.6 + ajv-keywords: 3.5.2(ajv@6.12.6) + + schema-utils@4.3.2: + dependencies: + '@types/json-schema': 7.0.15 + ajv: 8.17.1 + ajv-formats: 2.1.1(ajv@8.17.1) + ajv-keywords: 5.1.0(ajv@8.17.1) + + search-insights@2.17.3: {} + + section-matter@1.0.0: + dependencies: + extend-shallow: 2.0.1 + kind-of: 6.0.3 + + select-hose@2.0.0: {} + + selfsigned@2.4.1: + dependencies: + '@types/node-forge': 1.3.13 + node-forge: 1.3.1 + + semver-diff@4.0.0: + dependencies: + semver: 7.7.2 + + semver@5.7.2: {} + + semver@6.3.1: {} + + semver@7.5.3: + dependencies: + lru-cache: 6.0.0 + + semver@7.5.4: + dependencies: + lru-cache: 6.0.0 + + semver@7.6.3: {} + + semver@7.7.2: {} + + send@0.19.0: + 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 + transitivePeerDependencies: + - supports-color + + serialize-javascript@6.0.2: + dependencies: + randombytes: 2.1.0 + + serve-handler@6.1.6: + dependencies: + bytes: 3.0.0 + content-disposition: 0.5.2 + mime-types: 2.1.18 + minimatch: 3.1.2 + path-is-inside: 1.0.2 + path-to-regexp: 3.3.0 + range-parser: 1.2.0 + + serve-index@1.9.1: + dependencies: + accepts: 1.3.8 + batch: 0.6.1 + debug: 2.6.9 + escape-html: 1.0.3 + http-errors: 1.6.3 + mime-types: 2.1.35 + parseurl: 1.3.3 + transitivePeerDependencies: + - supports-color + + serve-static@1.16.2: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.19.0 + transitivePeerDependencies: + - supports-color + + set-blocking@2.0.0: {} + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + set-proto@1.0.0: + dependencies: + dunder-proto: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + + setprototypeof@1.1.0: {} + + setprototypeof@1.2.0: {} + + shallow-clone@3.0.1: + dependencies: + kind-of: 6.0.3 + + shallowequal@1.1.0: {} + + sharp@0.34.3: + dependencies: + color: 4.2.3 + detect-libc: 2.0.4 + semver: 7.7.2 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.3 + '@img/sharp-darwin-x64': 0.34.3 + '@img/sharp-libvips-darwin-arm64': 1.2.0 + '@img/sharp-libvips-darwin-x64': 1.2.0 + '@img/sharp-libvips-linux-arm': 1.2.0 + '@img/sharp-libvips-linux-arm64': 1.2.0 + '@img/sharp-libvips-linux-ppc64': 1.2.0 + '@img/sharp-libvips-linux-s390x': 1.2.0 + '@img/sharp-libvips-linux-x64': 1.2.0 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.0 + '@img/sharp-libvips-linuxmusl-x64': 1.2.0 + '@img/sharp-linux-arm': 0.34.3 + '@img/sharp-linux-arm64': 0.34.3 + '@img/sharp-linux-ppc64': 0.34.3 + '@img/sharp-linux-s390x': 0.34.3 + '@img/sharp-linux-x64': 0.34.3 + '@img/sharp-linuxmusl-arm64': 0.34.3 + '@img/sharp-linuxmusl-x64': 0.34.3 + '@img/sharp-wasm32': 0.34.3 + '@img/sharp-win32-arm64': 0.34.3 + '@img/sharp-win32-ia32': 0.34.3 + '@img/sharp-win32-x64': 0.34.3 + optional: true + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + shell-quote@1.8.3: {} + + shelljs@0.8.5: + dependencies: + glob: 7.2.3 + interpret: 1.4.0 + rechoir: 0.6.2 + + should-equal@2.0.0: + dependencies: + should-type: 1.4.0 + + should-format@3.0.3: + dependencies: + should-type: 1.4.0 + should-type-adaptors: 1.1.0 + + should-type-adaptors@1.1.0: + dependencies: + should-type: 1.4.0 + should-util: 1.0.1 + + should-type@1.4.0: {} + + should-util@1.0.1: {} + + should@13.2.3: + dependencies: + should-equal: 2.0.0 + should-format: 3.0.3 + should-type: 1.4.0 + should-type-adaptors: 1.1.0 + should-util: 1.0.1 + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + siginfo@2.0.0: {} + + signal-exit@3.0.7: {} + + signal-exit@4.1.0: {} + + sigstore@1.9.0: + dependencies: + '@sigstore/bundle': 1.1.0 + '@sigstore/protobuf-specs': 0.2.1 + '@sigstore/sign': 1.0.0 + '@sigstore/tuf': 1.0.3 + make-fetch-happen: 11.1.1 + transitivePeerDependencies: + - supports-color + + simple-swizzle@0.2.2: + dependencies: + is-arrayish: 0.3.2 + optional: true + + simple-update-notifier@2.0.0: + dependencies: + semver: 7.7.2 + + sirv@2.0.4: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + + sisteransi@1.0.5: {} + + sitemap@7.1.2: + dependencies: + '@types/node': 17.0.45 + '@types/sax': 1.2.7 + arg: 5.0.2 + sax: 1.4.1 + + skin-tone@2.0.0: + dependencies: + unicode-emoji-modifier-base: 1.0.0 + + slash@3.0.0: {} + + slash@4.0.0: {} + + slice-ansi@3.0.0: + dependencies: + ansi-styles: 4.3.0 + astral-regex: 2.0.0 + is-fullwidth-code-point: 3.0.0 + + slice-ansi@4.0.0: + dependencies: + ansi-styles: 4.3.0 + astral-regex: 2.0.0 + is-fullwidth-code-point: 3.0.0 + + slice-ansi@5.0.0: + dependencies: + ansi-styles: 6.2.1 + is-fullwidth-code-point: 4.0.0 + + slice-ansi@7.1.0: + dependencies: + ansi-styles: 6.2.1 + is-fullwidth-code-point: 5.0.0 + + slugify@1.6.6: {} + + smart-buffer@4.2.0: {} + + smol-toml@1.4.1: {} + + snake-case@3.0.4: + dependencies: + dot-case: 3.0.4 + tslib: 2.8.1 + + socket.io-adapter@2.5.5: + dependencies: + debug: 4.3.7 + ws: 8.17.1 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + socket.io-parser@4.2.4: + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.3.7 + transitivePeerDependencies: + - supports-color + + socket.io@4.8.1: + dependencies: + accepts: 1.3.8 + base64id: 2.0.0 + cors: 2.8.5 + debug: 4.3.7 + engine.io: 6.6.4 + socket.io-adapter: 2.5.5 + socket.io-parser: 4.2.4 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + sockjs@0.3.24: + dependencies: + faye-websocket: 0.11.4 + uuid: 8.3.2 + websocket-driver: 0.7.4 + + socks-proxy-agent@7.0.0: + dependencies: + agent-base: 6.0.2 + debug: 4.4.1(supports-color@5.5.0) + socks: 2.8.6 + transitivePeerDependencies: + - supports-color + + socks-proxy-agent@8.0.5: + dependencies: + agent-base: 7.1.4 + debug: 4.4.1(supports-color@5.5.0) + socks: 2.8.6 + transitivePeerDependencies: + - supports-color + optional: true + + socks@2.8.6: + dependencies: + ip-address: 9.0.5 + smart-buffer: 4.2.0 + + sort-css-media-queries@2.2.0: {} + + source-map-js@1.2.1: {} + + source-map-loader@4.0.1(webpack@5.94.0(@swc/core@1.13.1)(esbuild@0.18.17)): + dependencies: + abab: 2.0.6 + iconv-lite: 0.6.3 + source-map-js: 1.2.1 + webpack: 5.94.0(@swc/core@1.13.1)(esbuild@0.18.17) + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + source-map@0.7.4: {} + + source-map@0.8.0-beta.0: + dependencies: + whatwg-url: 7.1.0 + + space-separated-tokens@2.0.2: {} + + spawndamnit@3.0.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + spdx-correct@3.2.0: + dependencies: + spdx-expression-parse: 3.0.1 + spdx-license-ids: 3.0.21 + + spdx-exceptions@2.5.0: {} + + spdx-expression-parse@3.0.1: + dependencies: + spdx-exceptions: 2.5.0 + spdx-license-ids: 3.0.21 + + spdx-license-ids@3.0.21: {} + + spdy-transport@3.0.0: + dependencies: + debug: 4.4.1(supports-color@5.5.0) + detect-node: 2.1.0 + hpack.js: 2.1.6 + obuf: 1.1.2 + readable-stream: 3.6.2 + wbuf: 1.7.3 + transitivePeerDependencies: + - supports-color + + spdy@4.0.2: + dependencies: + debug: 4.4.1(supports-color@5.5.0) + handle-thing: 2.0.1 + http-deceiver: 1.2.7 + select-hose: 2.0.0 + spdy-transport: 3.0.0 + transitivePeerDependencies: + - supports-color + + split2@4.2.0: {} + + split@0.3.3: + dependencies: + through: 2.3.8 + + sprintf-js@1.0.3: {} + + sprintf-js@1.1.3: {} + + srcset@4.0.0: {} + + sshpk@1.18.0: + dependencies: + asn1: 0.2.6 + assert-plus: 1.0.0 + bcrypt-pbkdf: 1.0.2 + dashdash: 1.14.1 + ecc-jsbn: 0.1.2 + getpass: 0.1.7 + jsbn: 0.1.1 + safer-buffer: 2.1.2 + tweetnacl: 0.14.5 + + ssri@10.0.6: + dependencies: + minipass: 7.1.2 + + ssri@9.0.1: + dependencies: + minipass: 3.3.6 + + stable-hash@0.0.5: {} + + stackback@0.0.2: {} + + start-server-and-test@2.0.12: + dependencies: + arg: 5.0.2 + bluebird: 3.7.2 + check-more-types: 2.24.0 + debug: 4.4.1(supports-color@5.5.0) + execa: 5.1.1 + lazy-ass: 1.6.0 + ps-tree: 1.2.0 + wait-on: 8.0.3(debug@4.4.1) + transitivePeerDependencies: + - supports-color + + statuses@1.5.0: {} + + statuses@2.0.1: {} + + std-env@3.9.0: {} + + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + + stream-combiner@0.0.4: + dependencies: + duplexer: 0.1.2 + + streamroller@3.1.5: + dependencies: + date-format: 4.0.14 + debug: 4.4.1(supports-color@5.5.0) + fs-extra: 8.1.0 + transitivePeerDependencies: + - supports-color + + streamx@2.22.1: + dependencies: + fast-fifo: 1.3.2 + text-decoder: 1.2.3 + optionalDependencies: + bare-events: 2.6.0 + optional: true + + string-argv@0.3.2: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + + string-width@7.2.0: + dependencies: + emoji-regex: 10.4.0 + get-east-asian-width: 1.3.0 + strip-ansi: 7.1.0 + + string.prototype.codepointat@0.2.1: {} + + string.prototype.includes@2.0.1: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + + string.prototype.matchall@4.0.12: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + regexp.prototype.flags: 1.5.4 + set-function-name: 2.0.2 + side-channel: 1.1.0 + + string.prototype.repeat@1.0.0: + dependencies: + define-properties: 1.2.1 + es-abstract: 1.24.0 + + string.prototype.trim@1.2.10: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-data-property: 1.1.4 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-object-atoms: 1.1.1 + has-property-descriptors: 1.0.2 + + string.prototype.trimend@1.0.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + + stringify-object@3.3.0: + dependencies: + get-own-enumerable-property-symbols: 3.0.2 + is-obj: 1.0.1 + is-regexp: 1.0.0 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.0: + dependencies: + ansi-regex: 6.1.0 + + strip-bom-string@1.0.0: {} + + strip-bom@3.0.0: {} + + strip-final-newline@2.0.0: {} + + strip-final-newline@3.0.0: {} + + strip-indent@3.0.0: + dependencies: + min-indent: 1.0.1 + + strip-json-comments@2.0.1: {} + + strip-json-comments@3.1.1: {} + + strip-json-comments@5.0.2: {} + + strong-log-transformer@2.1.0: + dependencies: + duplexer: 0.1.2 + minimist: 1.2.8 + through: 2.3.8 + + style-to-js@1.1.17: + dependencies: + style-to-object: 1.0.9 + + style-to-object@0.4.4: + dependencies: + inline-style-parser: 0.1.1 + + style-to-object@1.0.9: + dependencies: + inline-style-parser: 0.2.4 + + styled-jsx@5.1.6(@babel/core@7.28.0)(react@19.1.0): + dependencies: + client-only: 0.0.1 + react: 19.1.0 + optionalDependencies: + '@babel/core': 7.28.0 + + stylehacks@6.1.1(postcss@8.5.3): + dependencies: + browserslist: 4.25.1 + postcss: 8.5.3 + postcss-selector-parser: 6.1.2 + + stylehacks@6.1.1(postcss@8.5.6): + dependencies: + browserslist: 4.25.1 + postcss: 8.5.6 + postcss-selector-parser: 6.1.2 + + stylis@4.3.6: {} + + sucrase@3.35.0: + dependencies: + '@jridgewell/gen-mapping': 0.3.12 + commander: 4.1.1 + glob: 10.4.5 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.7 + ts-interface-checker: 0.1.13 + + supports-color@5.5.0: + dependencies: + has-flag: 3.0.0 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + svg-parser@2.0.4: {} + + svgo@3.3.2: + dependencies: + '@trysound/sax': 0.2.0 + commander: 7.2.0 + css-select: 5.2.2 + css-tree: 2.3.1 + css-what: 6.2.2 + csso: 5.0.5 + picocolors: 1.1.1 + + swagger2openapi@7.0.8(encoding@0.1.13): + dependencies: + call-me-maybe: 1.0.2 + node-fetch: 2.7.0(encoding@0.1.13) + node-fetch-h2: 2.3.0 + node-readfiles: 0.2.0 + oas-kit-common: 1.0.8 + oas-resolver: 2.5.6 + oas-schema-walker: 1.1.5 + oas-validator: 5.0.8 + reftools: 1.1.9 + yaml: 1.10.2 + yargs: 17.7.2 + transitivePeerDependencies: + - encoding + + swc-loader@0.2.6(@swc/core@1.13.1(@swc/helpers@0.5.17))(webpack@5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17))): + dependencies: + '@swc/core': 1.13.1(@swc/helpers@0.5.17) + '@swc/counter': 0.1.3 + webpack: 5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17)) + + symbol-observable@4.0.0: {} + + symbol-tree@3.2.4: {} + + tabbable@6.2.0: {} + + tailwind-merge@2.6.0: {} + + tailwindcss@3.4.14: + dependencies: + '@alloc/quick-lru': 5.2.0 + arg: 5.0.2 + chokidar: 3.6.0 + didyoumean: 1.2.2 + dlv: 1.1.3 + fast-glob: 3.3.3 + glob-parent: 6.0.2 + is-glob: 4.0.3 + jiti: 1.21.7 + lilconfig: 2.1.0 + micromatch: 4.0.8 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.1.1 + postcss: 8.5.3 + postcss-import: 15.1.0(postcss@8.5.3) + postcss-js: 4.0.1(postcss@8.5.3) + postcss-load-config: 4.0.2(postcss@8.5.3) + postcss-nested: 6.2.0(postcss@8.5.3) + postcss-selector-parser: 6.1.2 + resolve: 1.22.10 + sucrase: 3.35.0 + transitivePeerDependencies: + - ts-node + + tapable@2.2.2: {} + + tar-fs@3.1.0: + dependencies: + pump: 3.0.3 + tar-stream: 3.1.7 + optionalDependencies: + bare-fs: 4.1.6 + bare-path: 3.0.0 + transitivePeerDependencies: + - bare-buffer + optional: true + + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.5 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + + tar-stream@3.1.7: + dependencies: + b4a: 1.6.7 + fast-fifo: 1.3.2 + streamx: 2.22.1 + optional: true + + tar@6.2.1: + dependencies: + chownr: 2.0.0 + fs-minipass: 2.1.0 + minipass: 5.0.0 + minizlib: 2.1.2 + mkdirp: 1.0.4 + yallist: 4.0.0 + + tcomb-validation@3.4.1: + dependencies: + tcomb: 3.2.29 + + tcomb@3.2.29: {} + + term-size@2.2.1: {} + + terser-webpack-plugin@5.3.14(@swc/core@1.13.1(@swc/helpers@0.5.17))(webpack@5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17))): + dependencies: + '@jridgewell/trace-mapping': 0.3.29 + jest-worker: 27.5.1 + schema-utils: 4.3.2 + serialize-javascript: 6.0.2 + terser: 5.43.1 + webpack: 5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17)) + optionalDependencies: + '@swc/core': 1.13.1(@swc/helpers@0.5.17) + + terser-webpack-plugin@5.3.14(@swc/core@1.13.1)(esbuild@0.18.17)(webpack@5.94.0(@swc/core@1.13.1)(esbuild@0.18.17)): + dependencies: + '@jridgewell/trace-mapping': 0.3.29 + jest-worker: 27.5.1 + schema-utils: 4.3.2 + serialize-javascript: 6.0.2 + terser: 5.43.1 + webpack: 5.94.0(@swc/core@1.13.1)(esbuild@0.18.17) + optionalDependencies: + '@swc/core': 1.13.1(@swc/helpers@0.5.17) + esbuild: 0.18.17 + + terser@5.19.2: + dependencies: + '@jridgewell/source-map': 0.3.10 + acorn: 8.15.0 + commander: 2.20.3 + source-map-support: 0.5.21 + + terser@5.43.1: + dependencies: + '@jridgewell/source-map': 0.3.10 + acorn: 8.15.0 + commander: 2.20.3 + source-map-support: 0.5.21 + + test-exclude@6.0.0: + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 7.2.3 + minimatch: 3.1.2 + + text-decoder@1.2.3: + dependencies: + b4a: 1.6.7 + optional: true + + text-table@0.2.0: {} + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + thirty-two@1.0.2: {} + + throttleit@1.0.1: {} + + through@2.3.8: {} + + thunky@1.1.0: {} + + tiny-inflate@1.0.3: {} + + tiny-invariant@1.3.3: {} + + tiny-warning@1.0.3: {} + + tinybench@2.9.0: {} + + tinycolor2@1.4.2: {} + + tinycolor2@1.6.0: {} + + tinyexec@0.3.2: {} + + tinyexec@1.0.1: {} + + tinyglobby@0.2.14: + dependencies: + fdir: 6.4.6(picomatch@4.0.3) + picomatch: 4.0.3 + + tinypool@1.1.1: {} + + tinyrainbow@1.2.0: {} + + tinyspy@3.0.2: {} + + tldts-core@6.1.86: {} + + tldts@6.1.86: + dependencies: + tldts-core: 6.1.86 + + tmp@0.0.33: + dependencies: + os-tmpdir: 1.0.2 + + tmp@0.2.1: + dependencies: + rimraf: 3.0.2 + + tmp@0.2.3: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + toggle-selection@1.0.6: {} + + toidentifier@1.0.1: {} + + totalist@3.0.1: {} + + touch@3.1.1: {} + + tough-cookie@4.1.4: + dependencies: + psl: 1.15.0 + punycode: 2.3.1 + universalify: 0.2.0 + url-parse: 1.5.10 + + tough-cookie@5.1.2: + dependencies: + tldts: 6.1.86 + + tr46@0.0.3: {} + + tr46@1.0.1: + dependencies: + punycode: 2.3.1 + + tr46@2.1.0: + dependencies: + punycode: 2.3.1 + + tr46@5.1.1: + dependencies: + punycode: 2.3.1 + + tree-kill@1.2.2: {} + + trim-lines@3.0.1: {} + + trim-trailing-lines@2.1.0: {} + + trough@2.2.0: {} + + ts-api-utils@1.4.3(typescript@5.1.6): + dependencies: + typescript: 5.1.6 + + ts-api-utils@1.4.3(typescript@5.8.3): + dependencies: + typescript: 5.8.3 + + ts-api-utils@2.1.0(typescript@5.1.6): + dependencies: + typescript: 5.1.6 + + ts-api-utils@2.1.0(typescript@5.8.3): + dependencies: + typescript: 5.8.3 + + ts-dedent@2.2.0: {} + + ts-error@1.0.6: {} + + ts-interface-checker@0.1.13: {} + + ts-poet@6.12.0: + dependencies: + dprint-node: 1.0.8 + + ts-proto-descriptors@2.0.0: + dependencies: + '@bufbuild/protobuf': 2.6.1 + + ts-proto@2.7.5: + dependencies: + '@bufbuild/protobuf': 2.6.1 + case-anything: 2.1.13 + ts-poet: 6.12.0 + ts-proto-descriptors: 2.0.0 + + tsconfck@3.1.6(typescript@5.8.3): + optionalDependencies: + typescript: 5.8.3 + + tsconfig-paths@3.15.0: + dependencies: + '@types/json5': 0.0.29 + json5: 1.0.2 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tsconfig-paths@4.2.0: + dependencies: + json5: 2.2.3 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tslib@1.14.1: {} + + tslib@2.6.1: {} + + tslib@2.8.1: {} + + tsup@8.5.0(@swc/core@1.13.1)(jiti@2.4.2)(postcss@8.5.6)(typescript@5.8.3)(yaml@2.8.0): + dependencies: + bundle-require: 5.1.0(esbuild@0.25.8) + cac: 6.7.14 + chokidar: 4.0.3 + consola: 3.4.2 + debug: 4.4.1(supports-color@5.5.0) + esbuild: 0.25.8 + fix-dts-default-cjs-exports: 1.0.1 + joycon: 3.1.1 + picocolors: 1.1.1 + postcss-load-config: 6.0.1(jiti@2.4.2)(postcss@8.5.6)(yaml@2.8.0) + resolve-from: 5.0.0 + rollup: 4.45.1 + source-map: 0.8.0-beta.0 + sucrase: 3.35.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.14 + tree-kill: 1.2.2 + optionalDependencies: + '@swc/core': 1.13.1(@swc/helpers@0.5.17) + postcss: 8.5.6 + typescript: 5.8.3 + transitivePeerDependencies: + - jiti + - supports-color + - tsx + - yaml + + tsutils@3.21.0(typescript@5.1.6): + dependencies: + tslib: 1.14.1 + typescript: 5.1.6 + + tuf-js@1.1.7: + dependencies: + '@tufjs/models': 1.0.4 + debug: 4.4.1(supports-color@5.5.0) + make-fetch-happen: 11.1.1 + transitivePeerDependencies: + - supports-color + + tunnel-agent@0.6.0: + dependencies: + safe-buffer: 5.2.1 + + turbo-darwin-64@2.5.5: + optional: true + + turbo-darwin-arm64@2.5.5: + optional: true + + turbo-linux-64@2.5.5: + optional: true + + turbo-linux-arm64@2.5.5: + optional: true + + turbo-windows-64@2.5.5: + optional: true + + turbo-windows-arm64@2.5.5: + optional: true + + turbo@2.5.5: + optionalDependencies: + turbo-darwin-64: 2.5.5 + turbo-darwin-arm64: 2.5.5 + turbo-linux-64: 2.5.5 + turbo-linux-arm64: 2.5.5 + turbo-windows-64: 2.5.5 + turbo-windows-arm64: 2.5.5 + + tweetnacl@0.14.5: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-fest@0.20.2: {} + + type-fest@0.21.3: {} + + type-fest@0.8.1: {} + + type-fest@1.4.0: {} + + type-fest@2.19.0: {} + + type-is@1.6.18: + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + + typed-array-byte-length@1.0.3: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + + typed-array-byte-offset@1.0.4: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + reflect.getprototypeof: 1.0.10 + + typed-array-length@1.0.7: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + is-typed-array: 1.1.15 + possible-typed-array-names: 1.1.0 + reflect.getprototypeof: 1.0.10 + + typed-assert@1.0.9: {} + + typedarray-to-buffer@3.1.5: + dependencies: + is-typedarray: 1.0.0 + + typescript@5.1.6: {} + + typescript@5.8.3: {} + + ua-parser-js@0.7.40: {} + + ufo@1.6.1: {} + + unbox-primitive@1.1.0: + dependencies: + call-bound: 1.0.4 + has-bigints: 1.1.0 + has-symbols: 1.1.0 + which-boxed-primitive: 1.1.1 + + unbzip2-stream@1.4.3: + dependencies: + buffer: 5.7.1 + through: 2.3.8 + optional: true + + undefsafe@2.0.5: {} + + undici-types@5.26.5: {} + + undici-types@6.21.0: {} + + undici-types@7.8.0: {} + + unicode-canonical-property-names-ecmascript@2.0.1: {} + + unicode-emoji-modifier-base@1.0.0: {} + + unicode-match-property-ecmascript@2.0.0: + dependencies: + unicode-canonical-property-names-ecmascript: 2.0.1 + unicode-property-aliases-ecmascript: 2.1.0 + + unicode-match-property-value-ecmascript@2.2.0: {} + + unicode-property-aliases-ecmascript@2.1.0: {} + + unified@10.1.2: + dependencies: + '@types/unist': 2.0.11 + bail: 2.0.2 + extend: 3.0.2 + is-buffer: 2.0.5 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 5.3.7 + + unified@11.0.5: + dependencies: + '@types/unist': 3.0.3 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.3 + + unique-filename@2.0.1: + dependencies: + unique-slug: 3.0.0 + + unique-filename@3.0.0: + dependencies: + unique-slug: 4.0.0 + + unique-slug@3.0.0: + dependencies: + imurmurhash: 0.1.4 + + unique-slug@4.0.0: + dependencies: + imurmurhash: 0.1.4 + + unique-string@3.0.0: + dependencies: + crypto-random-string: 4.0.0 + + unist-util-find-after@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + + unist-util-generated@2.0.1: {} + + unist-util-is@5.2.1: + dependencies: + '@types/unist': 2.0.11 + + unist-util-is@6.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position-from-estree@1.1.2: + dependencies: + '@types/unist': 2.0.11 + optional: true + + unist-util-position-from-estree@2.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@4.0.4: + dependencies: + '@types/unist': 2.0.11 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-remove-position@4.0.2: + dependencies: + '@types/unist': 2.0.11 + unist-util-visit: 4.1.2 + optional: true + + unist-util-stringify-position@3.0.3: + dependencies: + '@types/unist': 2.0.11 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@5.1.3: + dependencies: + '@types/unist': 2.0.11 + unist-util-is: 5.2.1 + + unist-util-visit-parents@6.0.1: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + + unist-util-visit@4.1.2: + dependencies: + '@types/unist': 2.0.11 + unist-util-is: 5.2.1 + unist-util-visit-parents: 5.1.3 + + unist-util-visit@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + unist-util-visit-parents: 6.0.1 + + universalify@0.1.2: {} + + universalify@0.2.0: {} + + universalify@2.0.1: {} + + unpipe@1.0.0: {} + + unrs-resolver@1.11.1: + dependencies: + napi-postinstall: 0.3.2 + optionalDependencies: + '@unrs/resolver-binding-android-arm-eabi': 1.11.1 + '@unrs/resolver-binding-android-arm64': 1.11.1 + '@unrs/resolver-binding-darwin-arm64': 1.11.1 + '@unrs/resolver-binding-darwin-x64': 1.11.1 + '@unrs/resolver-binding-freebsd-x64': 1.11.1 + '@unrs/resolver-binding-linux-arm-gnueabihf': 1.11.1 + '@unrs/resolver-binding-linux-arm-musleabihf': 1.11.1 + '@unrs/resolver-binding-linux-arm64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-arm64-musl': 1.11.1 + '@unrs/resolver-binding-linux-ppc64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-riscv64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-riscv64-musl': 1.11.1 + '@unrs/resolver-binding-linux-s390x-gnu': 1.11.1 + '@unrs/resolver-binding-linux-x64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-x64-musl': 1.11.1 + '@unrs/resolver-binding-wasm32-wasi': 1.11.1 + '@unrs/resolver-binding-win32-arm64-msvc': 1.11.1 + '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 + '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 + + untildify@4.0.0: {} + + update-browserslist-db@1.1.3(browserslist@4.25.1): + dependencies: + browserslist: 4.25.1 + escalade: 3.2.0 + picocolors: 1.1.1 + + update-notifier@6.0.2: + dependencies: + boxen: 7.1.1 + chalk: 5.4.1 + configstore: 6.0.0 + has-yarn: 3.0.0 + import-lazy: 4.0.0 + is-ci: 3.0.1 + is-installed-globally: 0.4.0 + is-npm: 6.0.0 + is-yarn-global: 0.4.1 + latest-version: 7.0.0 + pupa: 3.1.0 + semver: 7.7.2 + semver-diff: 4.0.0 + xdg-basedir: 5.1.0 + + uri-js-replace@1.0.1: {} + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + url-loader@4.1.1(file-loader@6.2.0(webpack@5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17))))(webpack@5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17))): + dependencies: + loader-utils: 2.0.4 + mime-types: 2.1.35 + schema-utils: 3.3.0 + webpack: 5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17)) + optionalDependencies: + file-loader: 6.2.0(webpack@5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17))) + + url-parse@1.5.10: + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + + url@0.11.4: + dependencies: + punycode: 1.4.1 + qs: 6.14.0 + + urlpattern-polyfill@10.0.0: + optional: true + + use-callback-ref@1.3.3(@types/react@19.1.2)(react@18.3.1): + dependencies: + react: 18.3.1 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.1.2 + + use-composed-ref@1.4.0(@types/react@19.1.2)(react@18.3.1): + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 19.1.2 + + use-editable@2.3.3(react@18.3.1): + dependencies: + react: 18.3.1 + + use-intl@3.26.5(react@19.1.0): + dependencies: + '@formatjs/fast-memoize': 2.2.7 + intl-messageformat: 10.7.16 + react: 19.1.0 + + use-isomorphic-layout-effect@1.2.1(@types/react@19.1.2)(react@18.3.1): + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 19.1.2 + + use-latest@1.3.0(@types/react@19.1.2)(react@18.3.1): + dependencies: + react: 18.3.1 + use-isomorphic-layout-effect: 1.2.1(@types/react@19.1.2)(react@18.3.1) + optionalDependencies: + '@types/react': 19.1.2 + + use-sidecar@1.1.3(@types/react@19.1.2)(react@18.3.1): + dependencies: + detect-node-es: 1.1.0 + react: 18.3.1 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.1.2 + + use-sync-external-store@1.5.0(react@18.3.1): + dependencies: + react: 18.3.1 + + use-sync-external-store@1.5.0(react@19.1.0): + dependencies: + react: 19.1.0 + + util-deprecate@1.0.2: {} + + util@0.10.4: + dependencies: + inherits: 2.0.3 + + utila@0.4.0: {} + + utility-types@3.11.0: {} + + utils-merge@1.0.1: {} + + uuid@10.0.0: {} + + uuid@11.1.0: {} + + uuid@8.3.2: {} + + uvu@0.5.6: + dependencies: + dequal: 2.0.3 + diff: 5.2.0 + kleur: 4.1.5 + sade: 1.8.1 + + v8-compile-cache@2.3.0: {} + + validate-npm-package-license@3.0.4: + dependencies: + spdx-correct: 3.2.0 + spdx-expression-parse: 3.0.1 + + validate-npm-package-name@5.0.1: {} + + validate-peer-dependencies@2.2.0: + dependencies: + resolve-package-path: 4.0.3 + semver: 7.7.2 + + validate.io-array@1.0.6: {} + + validate.io-function@1.0.2: {} + + validate.io-integer-array@1.0.0: + dependencies: + validate.io-array: 1.0.6 + validate.io-integer: 1.0.5 + + validate.io-integer@1.0.5: + dependencies: + validate.io-number: 1.0.3 + + validate.io-number@1.0.3: {} + + validator@13.15.15: {} + + value-equal@1.0.1: {} + + vary@1.1.2: {} + + verror@1.10.0: + dependencies: + assert-plus: 1.0.0 + core-util-is: 1.0.2 + extsprintf: 1.3.0 + + vfile-location@4.1.0: + dependencies: + '@types/unist': 2.0.11 + vfile: 5.3.7 + + vfile-location@5.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile: 6.0.3 + + vfile-message@3.1.4: + dependencies: + '@types/unist': 2.0.11 + unist-util-stringify-position: 3.0.3 + + vfile-message@4.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@5.3.7: + dependencies: + '@types/unist': 2.0.11 + is-buffer: 2.0.5 + unist-util-stringify-position: 3.0.3 + vfile-message: 3.1.4 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.2 + + vite-node@2.1.9(@types/node@22.16.5)(less@4.1.3)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1): + dependencies: + cac: 6.7.14 + debug: 4.4.1(supports-color@5.5.0) + es-module-lexer: 1.7.0 + pathe: 1.1.2 + vite: 5.4.19(@types/node@22.16.5)(less@4.1.3)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vite-node@2.1.9(@types/node@24.1.0)(less@4.1.3)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1): + dependencies: + cac: 6.7.14 + debug: 4.4.1(supports-color@5.5.0) + es-module-lexer: 1.7.0 + pathe: 1.1.2 + vite: 5.4.19(@types/node@24.1.0)(less@4.1.3)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vite-tsconfig-paths@5.1.4(typescript@5.8.3)(vite@5.4.19(@types/node@22.16.5)(less@4.1.3)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)): + dependencies: + debug: 4.4.1(supports-color@5.5.0) + globrex: 0.1.2 + tsconfck: 3.1.6(typescript@5.8.3) + optionalDependencies: + vite: 5.4.19(@types/node@22.16.5)(less@4.1.3)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1) + transitivePeerDependencies: + - supports-color + - typescript + + vite@4.5.5(@types/node@22.16.5)(less@4.1.3)(lightningcss@1.30.1)(sass@1.64.1)(terser@5.19.2): + dependencies: + esbuild: 0.18.17 + postcss: 8.5.3 + rollup: 3.29.5 + optionalDependencies: + '@types/node': 22.16.5 + fsevents: 2.3.3 + less: 4.1.3 + lightningcss: 1.30.1 + sass: 1.64.1 + terser: 5.19.2 + + vite@5.4.19(@types/node@22.16.5)(less@4.1.3)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.3 + rollup: 4.45.1 + optionalDependencies: + '@types/node': 22.16.5 + fsevents: 2.3.3 + less: 4.1.3 + lightningcss: 1.30.1 + sass: 1.89.2 + terser: 5.43.1 + + vite@5.4.19(@types/node@24.1.0)(less@4.1.3)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.3 + rollup: 4.45.1 + optionalDependencies: + '@types/node': 24.1.0 + fsevents: 2.3.3 + less: 4.1.3 + lightningcss: 1.30.1 + sass: 1.89.2 + terser: 5.43.1 + + vitest@2.1.9(@types/node@22.16.5)(jsdom@26.1.0)(less@4.1.3)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1): + dependencies: + '@vitest/expect': 2.1.9 + '@vitest/mocker': 2.1.9(vite@5.4.19(@types/node@22.16.5)(less@4.1.3)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)) + '@vitest/pretty-format': 2.1.9 + '@vitest/runner': 2.1.9 + '@vitest/snapshot': 2.1.9 + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.2.1 + debug: 4.4.1(supports-color@5.5.0) + expect-type: 1.2.2 + magic-string: 0.30.17 + pathe: 1.1.2 + std-env: 3.9.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinypool: 1.1.1 + tinyrainbow: 1.2.0 + vite: 5.4.19(@types/node@22.16.5)(less@4.1.3)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1) + vite-node: 2.1.9(@types/node@22.16.5)(less@4.1.3)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.16.5 + jsdom: 26.1.0 + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vitest@2.1.9(@types/node@24.1.0)(jsdom@26.1.0)(less@4.1.3)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1): + dependencies: + '@vitest/expect': 2.1.9 + '@vitest/mocker': 2.1.9(vite@5.4.19(@types/node@24.1.0)(less@4.1.3)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1)) + '@vitest/pretty-format': 2.1.9 + '@vitest/runner': 2.1.9 + '@vitest/snapshot': 2.1.9 + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.2.1 + debug: 4.4.1(supports-color@5.5.0) + expect-type: 1.2.2 + magic-string: 0.30.17 + pathe: 1.1.2 + std-env: 3.9.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinypool: 1.1.1 + tinyrainbow: 1.2.0 + vite: 5.4.19(@types/node@24.1.0)(less@4.1.3)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1) + vite-node: 2.1.9(@types/node@24.1.0)(less@4.1.3)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.43.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 24.1.0 + jsdom: 26.1.0 + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + void-elements@2.0.1: {} + + vscode-jsonrpc@8.2.0: {} + + vscode-languageserver-protocol@3.17.5: + dependencies: + vscode-jsonrpc: 8.2.0 + vscode-languageserver-types: 3.17.5 + + vscode-languageserver-textdocument@1.0.12: {} + + vscode-languageserver-types@3.17.5: {} + + vscode-languageserver@9.0.1: + dependencies: + vscode-languageserver-protocol: 3.17.5 + + vscode-uri@3.0.8: {} + + w3c-hr-time@1.0.2: + dependencies: + browser-process-hrtime: 1.0.0 + + w3c-xmlserializer@2.0.0: + dependencies: + xml-name-validator: 3.0.0 + + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + + wait-on@7.2.0: + dependencies: + axios: 1.10.0(debug@4.4.1) + joi: 17.13.3 + lodash: 4.17.21 + minimist: 1.2.8 + rxjs: 7.8.2 + transitivePeerDependencies: + - debug + + wait-on@8.0.3(debug@4.4.1): + dependencies: + axios: 1.10.0(debug@4.4.1) + joi: 17.13.3 + lodash: 4.17.21 + minimist: 1.2.8 + rxjs: 7.8.2 + transitivePeerDependencies: + - debug + + walk-up-path@4.0.0: {} + + warning@4.0.3: + dependencies: + loose-envify: 1.4.0 + + watchpack@2.4.4: + dependencies: + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + + wbuf@1.7.3: + dependencies: + minimalistic-assert: 1.0.1 + + wcwidth@1.0.1: + dependencies: + defaults: 1.0.4 + + web-namespaces@2.0.1: {} + + web-streams-polyfill@3.3.3: {} + + web-streams-polyfill@4.0.0-beta.3: {} + + web-vitals@4.2.4: {} + + webidl-conversions@3.0.1: {} + + webidl-conversions@4.0.2: {} + + webidl-conversions@5.0.0: {} + + webidl-conversions@6.1.0: {} + + webidl-conversions@7.0.0: {} + + webpack-bundle-analyzer@4.10.2: + dependencies: + '@discoveryjs/json-ext': 0.5.7 + acorn: 8.15.0 + acorn-walk: 8.3.4 + commander: 7.2.0 + debounce: 1.2.1 + escape-string-regexp: 4.0.0 + gzip-size: 6.0.0 + html-escaper: 2.0.2 + opener: 1.5.2 + picocolors: 1.1.1 + sirv: 2.0.4 + ws: 7.5.10 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + webpack-dev-middleware@5.3.4(webpack@5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17))): + dependencies: + colorette: 2.0.20 + memfs: 3.5.3 + mime-types: 2.1.35 + range-parser: 1.2.1 + schema-utils: 4.3.2 + webpack: 5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17)) + + webpack-dev-middleware@5.3.4(webpack@5.94.0(@swc/core@1.13.1)(esbuild@0.18.17)): + dependencies: + colorette: 2.0.20 + memfs: 3.5.3 + mime-types: 2.1.35 + range-parser: 1.2.1 + schema-utils: 4.3.2 + webpack: 5.94.0(@swc/core@1.13.1)(esbuild@0.18.17) + + webpack-dev-middleware@6.1.2(webpack@5.94.0(@swc/core@1.13.1)(esbuild@0.18.17)): + dependencies: + colorette: 2.0.20 + memfs: 3.5.3 + mime-types: 2.1.35 + range-parser: 1.2.1 + schema-utils: 4.3.2 + optionalDependencies: + webpack: 5.94.0(@swc/core@1.13.1)(esbuild@0.18.17) + + webpack-dev-server@4.15.1(webpack@5.94.0(@swc/core@1.13.1)(esbuild@0.18.17)): + dependencies: + '@types/bonjour': 3.5.13 + '@types/connect-history-api-fallback': 1.5.4 + '@types/express': 4.17.23 + '@types/serve-index': 1.9.4 + '@types/serve-static': 1.15.8 + '@types/sockjs': 0.3.36 + '@types/ws': 8.18.1 + ansi-html-community: 0.0.8 + bonjour-service: 1.3.0 + chokidar: 3.5.3 + colorette: 2.0.20 + compression: 1.8.1 + connect-history-api-fallback: 2.0.0 + default-gateway: 6.0.3 + express: 4.21.2 + graceful-fs: 4.2.11 + html-entities: 2.6.0 + http-proxy-middleware: 2.0.9(@types/express@4.17.23) + ipaddr.js: 2.2.0 + launch-editor: 2.10.0 + open: 8.4.2 + p-retry: 4.6.2 + rimraf: 3.0.2 + schema-utils: 4.3.2 + selfsigned: 2.4.1 + serve-index: 1.9.1 + sockjs: 0.3.24 + spdy: 4.0.2 + webpack-dev-middleware: 5.3.4(webpack@5.94.0(@swc/core@1.13.1)(esbuild@0.18.17)) + ws: 8.18.3 + optionalDependencies: + webpack: 5.94.0(@swc/core@1.13.1)(esbuild@0.18.17) + transitivePeerDependencies: + - bufferutil + - debug + - supports-color + - utf-8-validate + + webpack-dev-server@4.15.2(webpack@5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17))): + dependencies: + '@types/bonjour': 3.5.13 + '@types/connect-history-api-fallback': 1.5.4 + '@types/express': 4.17.23 + '@types/serve-index': 1.9.4 + '@types/serve-static': 1.15.8 + '@types/sockjs': 0.3.36 + '@types/ws': 8.18.1 + ansi-html-community: 0.0.8 + bonjour-service: 1.3.0 + chokidar: 3.6.0 + colorette: 2.0.20 + compression: 1.8.1 + connect-history-api-fallback: 2.0.0 + default-gateway: 6.0.3 + express: 4.21.2 + graceful-fs: 4.2.11 + html-entities: 2.6.0 + http-proxy-middleware: 2.0.9(@types/express@4.17.23) + ipaddr.js: 2.2.0 + launch-editor: 2.10.0 + open: 8.4.2 + p-retry: 4.6.2 + rimraf: 3.0.2 + schema-utils: 4.3.2 + selfsigned: 2.4.1 + serve-index: 1.9.1 + sockjs: 0.3.24 + spdy: 4.0.2 + webpack-dev-middleware: 5.3.4(webpack@5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17))) + ws: 8.18.3 + optionalDependencies: + webpack: 5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17)) + transitivePeerDependencies: + - bufferutil + - debug + - supports-color + - utf-8-validate + + webpack-merge@5.10.0: + dependencies: + clone-deep: 4.0.1 + flat: 5.0.2 + wildcard: 2.0.1 + + webpack-merge@5.9.0: + dependencies: + clone-deep: 4.0.1 + wildcard: 2.0.1 + + webpack-merge@6.0.1: + dependencies: + clone-deep: 4.0.1 + flat: 5.0.2 + wildcard: 2.0.1 + + webpack-sources@3.3.3: {} + + webpack-subresource-integrity@5.1.0(html-webpack-plugin@5.6.3(webpack@5.94.0(@swc/core@1.13.1)(esbuild@0.18.17)))(webpack@5.94.0(@swc/core@1.13.1)(esbuild@0.18.17)): + dependencies: + typed-assert: 1.0.9 + webpack: 5.94.0(@swc/core@1.13.1)(esbuild@0.18.17) + optionalDependencies: + html-webpack-plugin: 5.6.3(webpack@5.94.0(@swc/core@1.13.1)(esbuild@0.18.17)) + + webpack@5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17)): + dependencies: + '@types/eslint-scope': 3.7.7 + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/wasm-edit': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + acorn: 8.15.0 + acorn-import-phases: 1.0.4(acorn@8.15.0) + browserslist: 4.25.1 + chrome-trace-event: 1.0.4 + enhanced-resolve: 5.18.2 + es-module-lexer: 1.7.0 + eslint-scope: 5.1.1 + events: 3.3.0 + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + json-parse-even-better-errors: 2.3.1 + loader-runner: 4.3.0 + mime-types: 2.1.35 + neo-async: 2.6.2 + schema-utils: 4.3.2 + tapable: 2.2.2 + terser-webpack-plugin: 5.3.14(@swc/core@1.13.1(@swc/helpers@0.5.17))(webpack@5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17))) + watchpack: 2.4.4 + webpack-sources: 3.3.3 + transitivePeerDependencies: + - '@swc/core' + - esbuild + - uglify-js + + webpack@5.94.0(@swc/core@1.13.1)(esbuild@0.18.17): + dependencies: + '@types/estree': 1.0.8 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/wasm-edit': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + acorn: 8.15.0 + acorn-import-attributes: 1.9.5(acorn@8.15.0) + browserslist: 4.25.1 + chrome-trace-event: 1.0.4 + enhanced-resolve: 5.18.2 + es-module-lexer: 1.7.0 + eslint-scope: 5.1.1 + events: 3.3.0 + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + json-parse-even-better-errors: 2.3.1 + loader-runner: 4.3.0 + mime-types: 2.1.35 + neo-async: 2.6.2 + schema-utils: 3.3.0 + tapable: 2.2.2 + terser-webpack-plugin: 5.3.14(@swc/core@1.13.1)(esbuild@0.18.17)(webpack@5.94.0(@swc/core@1.13.1)(esbuild@0.18.17)) + watchpack: 2.4.4 + webpack-sources: 3.3.3 + transitivePeerDependencies: + - '@swc/core' + - esbuild + - uglify-js + + webpackbar@6.0.1(webpack@5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17))): + dependencies: + ansi-escapes: 4.3.2 + chalk: 4.1.2 + consola: 3.4.2 + figures: 3.2.0 + markdown-table: 2.0.0 + pretty-time: 1.1.0 + std-env: 3.9.0 + webpack: 5.100.2(@swc/core@1.13.1(@swc/helpers@0.5.17)) + wrap-ansi: 7.0.0 + + websocket-driver@0.7.4: + dependencies: + http-parser-js: 0.5.10 + safe-buffer: 5.2.1 + websocket-extensions: 0.1.4 + + websocket-extensions@0.1.4: {} + + whatwg-encoding@1.0.5: + dependencies: + iconv-lite: 0.4.24 + + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + + whatwg-mimetype@2.3.0: {} + + whatwg-mimetype@4.0.0: {} + + whatwg-url@14.2.0: + dependencies: + tr46: 5.1.1 + webidl-conversions: 7.0.0 + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + whatwg-url@7.1.0: + dependencies: + lodash.sortby: 4.7.0 + tr46: 1.0.1 + webidl-conversions: 4.0.2 + + whatwg-url@8.7.0: + dependencies: + lodash: 4.17.21 + tr46: 2.1.0 + webidl-conversions: 6.1.0 + + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-builtin-type@1.2.1: + dependencies: + call-bound: 1.0.4 + function.prototype.name: 1.1.8 + has-tostringtag: 1.0.2 + is-async-function: 2.1.1 + is-date-object: 1.1.0 + is-finalizationregistry: 1.1.1 + is-generator-function: 1.1.0 + is-regex: 1.2.1 + is-weakref: 1.1.1 + isarray: 2.0.5 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.19 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-module@2.0.1: {} + + which-typed-array@1.1.19: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + + which@1.3.1: + dependencies: + isexe: 2.0.0 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + which@3.0.1: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + wide-align@1.1.5: + dependencies: + string-width: 4.2.3 + + widest-line@4.0.1: + dependencies: + string-width: 5.1.2 + + wildcard@2.0.1: {} + + word-wrap@1.2.5: {} + + workerpool@9.3.3: {} + + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + + wrap-ansi@9.0.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 7.2.0 + strip-ansi: 7.1.0 + + wrappy@1.0.2: {} + + write-file-atomic@3.0.3: + dependencies: + imurmurhash: 0.1.4 + is-typedarray: 1.0.0 + signal-exit: 3.0.7 + typedarray-to-buffer: 3.1.5 + + ws@7.5.10: {} + + ws@8.17.1: {} + + ws@8.18.3: {} + + xdg-basedir@5.1.0: {} + + xml-formatter@2.6.1: + dependencies: + xml-parser-xo: 3.2.0 + + xml-js@1.6.11: + dependencies: + sax: 1.4.1 + + xml-name-validator@3.0.0: {} + + xml-name-validator@5.0.0: {} + + xml-parser-xo@3.2.0: {} + + xmlchars@2.2.0: {} + + xtend@4.0.2: {} + + y18n@4.0.3: {} + + y18n@5.0.8: {} + + yallist@3.1.1: {} + + yallist@4.0.0: {} + + yaml-ast-parser@0.0.43: {} + + yaml@1.10.2: {} + + yaml@2.8.0: {} + + yargs-parser@18.1.3: + dependencies: + camelcase: 5.3.1 + decamelize: 1.2.0 + + yargs-parser@20.2.9: {} + + yargs-parser@21.1.1: {} + + yargs-unparser@2.0.0: + dependencies: + camelcase: 6.3.0 + decamelize: 4.0.0 + flat: 5.0.2 + is-plain-obj: 2.1.0 + + yargs@15.4.1: + dependencies: + cliui: 6.0.0 + decamelize: 1.2.0 + find-up: 4.1.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + require-main-filename: 2.0.0 + set-blocking: 2.0.0 + string-width: 4.2.3 + which-module: 2.0.1 + y18n: 4.0.3 + yargs-parser: 18.1.3 + + yargs@16.2.0: + dependencies: + cliui: 7.0.4 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 20.2.9 + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yauzl@2.10.0: + dependencies: + buffer-crc32: 0.2.13 + fd-slicer: 1.1.0 + + yocto-queue@0.1.0: {} + + yocto-queue@1.2.1: {} + + zod-validation-error@3.5.3(zod@3.25.76): + dependencies: + zod: 3.25.76 + + zod@3.23.8: + optional: true + + zod@3.25.76: {} + + zone.js@0.13.3: + dependencies: + tslib: 2.8.1 + + zwitch@2.0.4: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000000..ea2e2ec1f9 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,6 @@ +packages: + - "console" + - "docs" + - "e2e" + - "packages/*" + - "apps/*" diff --git a/proto/buf.yaml b/proto/buf.yaml index 31bc7b4ccc..abe35b3055 100644 --- a/proto/buf.yaml +++ b/proto/buf.yaml @@ -40,4 +40,4 @@ lint: - zitadel/system.proto - zitadel/text.proto - zitadel/user.proto - - zitadel/v1.proto \ No newline at end of file + - zitadel/v1.proto diff --git a/proto/zitadel/action/v2/action_service.proto b/proto/zitadel/action/v2/action_service.proto new file mode 100644 index 0000000000..d9e57a83a5 --- /dev/null +++ b/proto/zitadel/action/v2/action_service.proto @@ -0,0 +1,728 @@ +syntax = "proto3"; + +package zitadel.action.v2; + +import "google/api/annotations.proto"; +import "google/api/field_behavior.proto"; +import "google/protobuf/duration.proto"; +import "google/protobuf/struct.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; + +import "zitadel/protoc_gen_zitadel/v2/options.proto"; + +import "zitadel/action/v2/target.proto"; +import "zitadel/action/v2/execution.proto"; +import "zitadel/action/v2/query.proto"; +import "google/protobuf/timestamp.proto"; +import "zitadel/filter/v2/filter.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/action/v2;action"; + +option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { + info: { + title: "Action Service"; + version: "2.0"; + description: "This API is intended to manage custom executions (previously known as actions) in a ZITADEL instance."; + contact:{ + name: "ZITADEL" + url: "https://zitadel.com" + email: "hi@zitadel.com" + } + license: { + name: "Apache 2.0", + url: "https://github.com/zitadel/zitadel/blob/main/LICENSING.md"; + }; + }; + schemes: HTTPS; + schemes: HTTP; + + consumes: "application/json"; + consumes: "application/grpc"; + + produces: "application/json"; + produces: "application/grpc"; + + consumes: "application/grpc-web+proto"; + produces: "application/grpc-web+proto"; + + host: "$CUSTOM-DOMAIN"; + base_path: "/"; + + external_docs: { + description: "Detailed information about ZITADEL", + url: "https://zitadel.com/docs" + } + security_definitions: { + security: { + key: "OAuth2"; + value: { + type: TYPE_OAUTH2; + flow: FLOW_ACCESS_CODE; + authorization_url: "$CUSTOM-DOMAIN/oauth/v2/authorize"; + token_url: "$CUSTOM-DOMAIN/oauth/v2/token"; + scopes: { + scope: { + key: "openid"; + value: "openid"; + } + scope: { + key: "urn:zitadel:iam:org:project:id:zitadel:aud"; + value: "urn:zitadel:iam:org:project:id:zitadel:aud"; + } + } + } + } + } + security: { + security_requirement: { + key: "OAuth2"; + value: { + scope: "openid"; + scope: "urn:zitadel:iam:org:project:id:zitadel:aud"; + } + } + } + responses: { + key: "403"; + value: { + description: "Returned when the user does not have permission to access the resource."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + } + } + } + } + responses: { + key: "404"; + value: { + description: "Returned when the resource does not exist."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + } + } + } + } +}; + +// Service to manage custom executions. +// The service provides methods to create, update, delete and list targets and executions. +service ActionService { + + // Create Target + // + // Create a new target to your endpoint, which can be used in executions. + // + // Required permission: + // - `action.target.write` + // + // Required feature flag: + // - `actions` + rpc CreateTarget (CreateTargetRequest) returns (CreateTargetResponse) { + option (google.api.http) = { + post: "/v2/actions/targets" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "action.target.write" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "Target created successfully"; + }; + }; + responses: { + key: "409" + value: { + description: "The target to create already exists."; + } + }; + responses: { + key: "400" + value: { + description: "The feature flag `actions` is not enabled."; + } + }; + }; + } + + // Update Target + // + // Update an existing target. + // To generate a new signing key set the optional expirationSigningKey. + // + // Required permission: + // - `action.target.write` + // + // Required feature flag: + // - `actions` + rpc UpdateTarget (UpdateTargetRequest) returns (UpdateTargetResponse) { + option (google.api.http) = { + post: "/v2/actions/targets/{id}" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "action.target.write" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "Target successfully updated or left unchanged"; + }; + }; + responses: { + key: "404" + value: { + description: "The target to update does not exist."; + } + }; + responses: { + key: "400" + value: { + description: "The feature flag `actions` is not enabled."; + } + }; + }; + } + + // Delete Target + // + // Delete an existing target. This will remove it from any configured execution as well. + // In case the target is not found, the request will return a successful response as + // the desired state is already achieved. + // + // Required permission: + // - `action.target.delete` + // + // Required feature flag: + // - `actions` + rpc DeleteTarget (DeleteTargetRequest) returns (DeleteTargetResponse) { + option (google.api.http) = { + delete: "/v2/actions/targets/{id}" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "action.target.delete" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "Target deleted successfully"; + }; + }; + responses: { + key: "400" + value: { + description: "The feature flag `actions` is not enabled."; + } + }; + }; + } + + // Get Target + // + // Returns the target identified by the requested ID. + // + // Required permission: + // - `action.target.read` + // + // Required feature flag: + // - `actions` + rpc GetTarget (GetTargetRequest) returns (GetTargetResponse) { + option (google.api.http) = { + get: "/v2/actions/targets/{id}" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "action.target.read" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200" + value: { + description: "Target retrieved successfully"; + } + }; + responses: { + key: "404" + value: { + description: "The target to update does not exist."; + } + }; + responses: { + key: "400" + value: { + description: "The feature flag `actions` is not enabled."; + } + }; + }; + } + + // List targets + // + // List all matching targets. By default all targets of the instance are returned. + // Make sure to include a limit and sorting for pagination. + // + // Required permission: + // - `action.target.read` + // + // Required feature flag: + // - `actions` + rpc ListTargets (ListTargetsRequest) returns (ListTargetsResponse) { + option (google.api.http) = { + post: "/v2/actions/targets/search", + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "action.target.read" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "A list of all targets matching the query"; + }; + }; + responses: { + key: "400"; + value: { + description: "invalid list query"; + }; + }; + responses: { + key: "400" + value: { + description: "The feature flag `actions` is not enabled."; + } + }; + }; + } + + // Set Execution + // + // Sets an execution to call a target or include the targets of another execution. + // Setting an empty list of targets will remove all targets from the execution, making it a noop. + // + // Required permission: + // - `action.execution.write` + // + // Required feature flag: + // - `actions` + rpc SetExecution (SetExecutionRequest) returns (SetExecutionResponse) { + option (google.api.http) = { + put: "/v2/actions/executions" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "action.execution.write" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "Execution successfully updated or left unchanged"; + }; + }; + responses: { + key: "400" + value: { + description: "Condition to set execution does not exist or the feature flag `actions` is not enabled."; + } + }; + }; + } + + // List Executions + // + // List all matching executions. By default all executions of the instance are returned that have at least one execution target. + // Make sure to include a limit and sorting for pagination. + // + // Required permission: + // - `action.execution.read` + // + // Required feature flag: + // - `actions` + rpc ListExecutions (ListExecutionsRequest) returns (ListExecutionsResponse) { + option (google.api.http) = { + post: "/v2/actions/executions/search" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "action.execution.read" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "A list of all non noop executions matching the query"; + }; + }; + responses: { + key: "400"; + value: { + description: "Invalid list query or the feature flag `actions` is not enabled."; + }; + }; + }; + } + + // List Execution Functions + // + // List all available functions which can be used as condition for executions. + rpc ListExecutionFunctions (ListExecutionFunctionsRequest) returns (ListExecutionFunctionsResponse) { + option (google.api.http) = { + get: "/v2/actions/executions/functions" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "List all functions successfully"; + }; + }; + }; + } + + // List Execution Methods + // + // List all available methods which can be used as condition for executions. + rpc ListExecutionMethods (ListExecutionMethodsRequest) returns (ListExecutionMethodsResponse) { + option (google.api.http) = { + get: "/v2/actions/executions/methods" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "List all methods successfully"; + }; + }; + }; + } + + // List Execution Services + // + // List all available services which can be used as condition for executions. + rpc ListExecutionServices (ListExecutionServicesRequest) returns (ListExecutionServicesResponse) { + option (google.api.http) = { + get: "/v2/actions/executions/services" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "List all services successfully"; + }; + }; + }; + } +} + +message CreateTargetRequest { + string name = 1 [ + (validate.rules).string = {min_len: 1, max_len: 1000}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"ip_allow_list\""; + min_length: 1 + max_length: 1000 + } + ]; + // Defines the target type and how the response of the target is treated. + oneof target_type { + option (validate.required) = true; + // Wait for response but response body is ignored, status is checked, call is sent as post. + RESTWebhook rest_webhook = 2; + // Wait for response and response body is used, status is checked, call is sent as post. + RESTCall rest_call = 3; + // Call is executed in parallel to others, ZITADEL does not wait until the call is finished. The state is ignored, call is sent as post. + RESTAsync rest_async = 4; + } + // Timeout defines the duration until ZITADEL cancels the execution. + // If the target doesn't respond before this timeout expires, then the connection is closed and the action fails. Depending on the target type and possible setting on `interrupt_on_error` following targets will not be called. In case of a `rest_async` target only this specific target will fail, without any influence on other targets of the same execution. + google.protobuf.Duration timeout = 5 [ + (validate.rules).duration = {gte: {}, lte: {seconds: 270}}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"10s\""; + } + ]; + string endpoint = 6 [ + (validate.rules).string = {min_len: 1, max_len: 1000}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"https://example.com/hooks/ip_check\"" + min_length: 1 + max_length: 1000 + } + ]; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + example: "{\"name\": \"ip_allow_list\",\"restWebhook\":{\"interruptOnError\":true},\"timeout\":\"10s\",\"endpoint\":\"https://example.com/hooks/ip_check\"}"; + }; +} + +message CreateTargetResponse { + // The unique identifier of the newly created target. + string id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629012906488334\""; + } + ]; + // The timestamp of the target creation. + google.protobuf.Timestamp creation_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + // Key used to sign and check payload sent to the target. + string signing_key = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"98KmsU67\"" + } + ]; +} + +message UpdateTargetRequest { + string id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"69629026806489455\""; + } + ]; + optional string name = 2 [ + (validate.rules).string = {min_len: 1, max_len: 1000}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"ip_allow_list\"" + min_length: 1 + max_length: 1000 + } + ]; + // Defines the target type and how the response of the target is treated. + oneof target_type { + // Wait for response but response body is ignored, status is checked, call is sent as post. + RESTWebhook rest_webhook = 3; + // Wait for response and response body is used, status is checked, call is sent as post. + RESTCall rest_call = 4; + // Call is executed in parallel to others, ZITADEL does not wait until the call is finished. The state is ignored, call is sent as post. + RESTAsync rest_async = 5; + } + // Timeout defines the duration until ZITADEL cancels the execution. + // If the target doesn't respond before this timeout expires, then the connection is closed and the action fails. Depending on the target type and possible setting on `interrupt_on_error` following targets will not be called. In case of a `rest_async` target only this specific target will fail, without any influence on other targets of the same execution. + optional google.protobuf.Duration timeout = 6 [ + (validate.rules).duration = {gte: {}, lte: {seconds: 270}}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"10s\""; + } + ]; + optional string endpoint = 7 [ + (validate.rules).string = {min_len: 1, max_len: 1000}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"https://example.com/hooks/ip_check\"" + min_length: 1 + max_length: 1000 + } + ]; + // Regenerate the key used for signing and checking the payload sent to the target. + // Set the graceful period for the existing key. During that time, the previous + // signing key and the new one will be used to sign the request to allow you a smooth + // transition onf your API. + // + // Note that we currently only allow an immediate rotation ("0s") and will support + // longer expirations in the future. + optional google.protobuf.Duration expiration_signing_key = 8 [ + (validate.rules).duration = {const: {seconds: 0, nanos: 0}}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"0s\"" + minimum: 0 + maximum: 0 + } + ]; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + example: "{\"name\": \"ip_allow_list\",\"restCall\":{\"interruptOnError\":true},\"timeout\":\"10s\",\"endpoint\":\"https://example.com/hooks/ip_check\",\"expirationSigningKey\":\"0s\"}"; + }; +} + +message UpdateTargetResponse { + // The timestamp of the change of the target. + google.protobuf.Timestamp change_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; + // Key used to sign and check payload sent to the target. + optional string signing_key = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"98KmsU67\"" + } + ]; +} + +message DeleteTargetRequest { + string id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"69629026806489455\""; + } + ]; +} + +message DeleteTargetResponse { + // The timestamp of the deletion of the target. + // Note that the deletion date is only guaranteed to be set if the deletion was successful during the request. + // In case the deletion occurred in a previous request, the deletion date might be empty. + google.protobuf.Timestamp deletion_date = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message GetTargetRequest { + string id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"69629026806489455\""; + } + ]; +} + +message GetTargetResponse { + Target target = 1; +} + +message ListTargetsRequest { + // List limitations and ordering. + optional zitadel.filter.v2.PaginationRequest pagination = 1; + // The field the result is sorted by. The default is the creation date. Beware that if you change this, your result pagination might be inconsistent. + optional TargetFieldName sorting_column = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"TARGET_FIELD_NAME_CREATION_DATE\"" + } + ]; + // Define the criteria to query for. + repeated TargetSearchFilter filters = 3; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + example: "{\"pagination\":{\"offset\":0,\"limit\":0,\"asc\":true},\"sortingColumn\":\"TARGET_FIELD_NAME_CREATION_DATE\",\"filters\":[{\"targetNameFilter\":{\"targetName\":\"ip_allow_list\",\"method\":\"TEXT_FILTER_METHOD_EQUALS\"}},{\"inTargetIdsFilter\":{\"targetIds\":[\"69629023906488334\",\"69622366012355662\"]}}]}"; + }; +} + +message ListTargetsResponse { + reserved 'result'; + zitadel.filter.v2.PaginationResponse pagination = 1; + repeated Target targets = 2; +} + +message SetExecutionRequest { + // Condition defining when the execution should be used. + Condition condition = 1; + // Ordered list of targets called during the execution. + repeated string targets = 2; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + example: "{\"condition\":{\"request\":{\"method\":\"zitadel.session.v2.SessionService/ListSessions\"}},\"targets\":[{\"target\":\"69629026806489455\"}]}"; + }; +} + +message SetExecutionResponse { + // The timestamp of the execution set. + google.protobuf.Timestamp set_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; +} + +message ListExecutionsRequest { + // List limitations and ordering. + optional zitadel.filter.v2.PaginationRequest pagination = 1; + // The field the result is sorted by. The default is the creation date. Beware that if you change this, your result pagination might be inconsistent. + optional ExecutionFieldName sorting_column = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"EXECUTION_FIELD_NAME_CREATION_DATE\"" + } + ]; + // Define the criteria to query for. + repeated ExecutionSearchFilter filters = 3; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + example: "{\"pagination\":{\"offset\":0,\"limit\":0,\"asc\":true},\"sortingColumn\":\"EXECUTION_FIELD_NAME_ID\",\"filters\":[{\"targetFilter\":{\"targetId\":\"69629023906488334\"}}]}"; + }; +} + +message ListExecutionsResponse { + reserved 'result'; + zitadel.filter.v2.PaginationResponse pagination = 1; + repeated Execution executions = 2; +} + +message ListExecutionFunctionsRequest{} +message ListExecutionFunctionsResponse{ + // All available methods + repeated string functions = 1; +} +message ListExecutionMethodsRequest{} +message ListExecutionMethodsResponse{ + // All available methods + repeated string methods = 1; +} + +message ListExecutionServicesRequest{} +message ListExecutionServicesResponse{ + // All available methods + repeated string services = 1; +} diff --git a/proto/zitadel/action/v2/execution.proto b/proto/zitadel/action/v2/execution.proto new file mode 100644 index 0000000000..9f166a4c2c --- /dev/null +++ b/proto/zitadel/action/v2/execution.proto @@ -0,0 +1,135 @@ +syntax = "proto3"; + +package zitadel.action.v2; + +import "google/api/annotations.proto"; +import "google/api/field_behavior.proto"; +import "google/protobuf/duration.proto"; +import "google/protobuf/struct.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; +import "zitadel/protoc_gen_zitadel/v2/options.proto"; + +import "google/protobuf/timestamp.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/action/v2;action"; + +message Execution { + Condition condition = 1; + // The timestamp of the execution creation. + google.protobuf.Timestamp creation_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + // The timestamp of the last change to the execution. + google.protobuf.Timestamp change_date = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; + // Ordered list of targets called during the execution. + repeated string targets = 4; +} + +message Condition { + // Condition-types under which conditions the execution should happen, only one possible. + oneof condition_type { + option (validate.required) = true; + + // Condition-type to execute if a request on the defined API point happens. + RequestExecution request = 1; + // Condition-type to execute on response if a request on the defined API point happens. + ResponseExecution response = 2; + // Condition-type to execute if function is used, replaces actions v1. + FunctionExecution function = 3; + // Condition-type to execute if an event is created in the system. + EventExecution event = 4; + } +} + +message RequestExecution { + // Condition for the request execution. Only one is possible. + oneof condition{ + option (validate.required) = true; + // GRPC-method as condition. + string method = 1 [ + (validate.rules).string = {min_len: 1, max_len: 1000}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 1000, + example: "\"/zitadel.session.v2.SessionService/ListSessions\""; + } + ]; + // GRPC-service as condition. + string service = 2 [ + (validate.rules).string = {min_len: 1, max_len: 1000}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 1000, + example: "\"zitadel.session.v2.SessionService\""; + } + ]; + // All calls to any available services and methods as condition. + bool all = 3 [(validate.rules).bool = {const: true}]; + } +} + +message ResponseExecution { + // Condition for the response execution. Only one is possible. + oneof condition{ + option (validate.required) = true; + // GRPC-method as condition. + string method = 1 [ + (validate.rules).string = {min_len: 1, max_len: 1000}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 1000, + example: "\"/zitadel.session.v2.SessionService/ListSessions\""; + } + ]; + // GRPC-service as condition. + string service = 2 [ + (validate.rules).string = {min_len: 1, max_len: 1000}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 1000, + example: "\"zitadel.session.v2.SessionService\""; + } + ]; + // All calls to any available services and methods as condition. + bool all = 3 [(validate.rules).bool = {const: true}]; + } +} + +// Executed on the specified function +message FunctionExecution { + string name = 1 [(validate.rules).string = {min_len: 1, max_len: 1000}]; +} + +message EventExecution { + // Condition for the event execution. Only one is possible. + oneof condition{ + option (validate.required) = true; + // Event name as condition. + string event = 1 [ + (validate.rules).string = {min_len: 1, max_len: 1000}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 1000, + example: "\"user.human.added\""; + } + ]; + // Event group as condition, all events under this group. + string group = 2 [ + (validate.rules).string = {min_len: 1, max_len: 1000}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 1000, + example: "\"user.human\""; + } + ]; + // all events as condition. + bool all = 3 [(validate.rules).bool = {const: true}]; + } +} diff --git a/proto/zitadel/action/v2/query.proto b/proto/zitadel/action/v2/query.proto new file mode 100644 index 0000000000..39e94bd826 --- /dev/null +++ b/proto/zitadel/action/v2/query.proto @@ -0,0 +1,108 @@ +syntax = "proto3"; + +package zitadel.action.v2; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/action/v2;action"; + +import "google/api/field_behavior.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; +import "google/protobuf/timestamp.proto"; + +import "zitadel/action/v2/execution.proto"; +import "zitadel/filter/v2/filter.proto"; + +message ExecutionSearchFilter { + oneof filter { + option (validate.required) = true; + + InConditionsFilter in_conditions_filter = 1; + ExecutionTypeFilter execution_type_filter = 2; + TargetFilter target_filter = 3; + } +} + +message InConditionsFilter { + // Defines the conditions to query for. + repeated Condition conditions = 1; +} + +message ExecutionTypeFilter { + // Defines the type to query for. + ExecutionType execution_type = 1; +} + +message TargetFilter { + // Defines the id to query for. + string target_id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "the id of the targets to include" + example: "\"69629023906488334\""; + } + ]; +} + +enum TargetFieldName { + TARGET_FIELD_NAME_UNSPECIFIED = 0; + TARGET_FIELD_NAME_ID = 1; + TARGET_FIELD_NAME_CREATED_DATE = 2; + TARGET_FIELD_NAME_CHANGED_DATE = 3; + TARGET_FIELD_NAME_NAME = 4; + TARGET_FIELD_NAME_TARGET_TYPE = 5; + TARGET_FIELD_NAME_URL = 6; + TARGET_FIELD_NAME_TIMEOUT = 7; + TARGET_FIELD_NAME_INTERRUPT_ON_ERROR = 8; +} + +message TargetSearchFilter { + oneof filter { + option (validate.required) = true; + + TargetNameFilter target_name_filter = 1; + InTargetIDsFilter in_target_ids_filter = 2; + } +} + +message TargetNameFilter { + // Defines the name of the target to query for. + string target_name = 1 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + max_length: 200; + example: "\"ip_allow_list\""; + } + ]; + // Defines which text comparison method used for the name query. + zitadel.filter.v2.TextFilterMethod method = 2 [ + (validate.rules).enum.defined_only = true, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "defines which text equality method is used"; + } + ]; +} + +message InTargetIDsFilter { + // Defines the ids to query for. + repeated string target_ids = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "the ids of the targets to include" + example: "[\"69629023906488334\",\"69622366012355662\"]"; + } + ]; +} + +enum ExecutionType { + EXECUTION_TYPE_UNSPECIFIED = 0; + EXECUTION_TYPE_REQUEST = 1; + EXECUTION_TYPE_RESPONSE = 2; + EXECUTION_TYPE_EVENT = 3; + EXECUTION_TYPE_FUNCTION = 4; +} + + +enum ExecutionFieldName { + EXECUTION_FIELD_NAME_UNSPECIFIED = 0; + EXECUTION_FIELD_NAME_ID = 1; + EXECUTION_FIELD_NAME_CREATED_DATE = 2; + EXECUTION_FIELD_NAME_CHANGED_DATE = 3; +} \ No newline at end of file diff --git a/proto/zitadel/action/v2/target.proto b/proto/zitadel/action/v2/target.proto new file mode 100644 index 0000000000..ee98b35149 --- /dev/null +++ b/proto/zitadel/action/v2/target.proto @@ -0,0 +1,75 @@ +syntax = "proto3"; + +package zitadel.action.v2; + +import "google/api/annotations.proto"; +import "google/api/field_behavior.proto"; +import "google/protobuf/duration.proto"; +import "google/protobuf/struct.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; +import "zitadel/protoc_gen_zitadel/v2/options.proto"; +import "google/protobuf/timestamp.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/action/v2;action"; + +message Target { + // The unique identifier of the target. + string id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629012906488334\""; + } + ]; + // The timestamp of the target creation. + google.protobuf.Timestamp creation_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + // The timestamp of the last change to the target (e.g. creation, activation, deactivation). + google.protobuf.Timestamp change_date = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; + string name = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"ip_allow_list\""; + } + ]; + // Defines the target type and how the response of the target is treated. + oneof target_type { + RESTWebhook rest_webhook = 5; + RESTCall rest_call = 6; + RESTAsync rest_async = 7; + } + // Timeout defines the duration until ZITADEL cancels the execution. + // If the target doesn't respond before this timeout expires, the the connection is closed and the action fails. Depending on the target type and possible setting on `interrupt_on_error` following targets will not be called. In case of a `rest_async` target only this specific target will fail, without any influence on other targets of the same execution. + google.protobuf.Duration timeout = 8 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"10s\""; + } + ]; + string endpoint = 9 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"https://example.com/hooks/ip_check\"" + } + ]; + string signing_key = 10 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"98KmsU67\"" + } + ]; +} + +message RESTWebhook { + // Define if any error stops the whole execution. By default the process continues as normal. + bool interrupt_on_error = 1; +} + +message RESTCall { + // Define if any error stops the whole execution. By default the process continues as normal. + bool interrupt_on_error = 1; +} + +message RESTAsync {} diff --git a/proto/zitadel/action/v2beta/action_service.proto b/proto/zitadel/action/v2beta/action_service.proto index f225905225..1cf5c210ee 100644 --- a/proto/zitadel/action/v2beta/action_service.proto +++ b/proto/zitadel/action/v2beta/action_service.proto @@ -290,7 +290,7 @@ service ActionService { // - `actions` rpc ListTargets (ListTargetsRequest) returns (ListTargetsResponse) { option (google.api.http) = { - post: "/v2beta/actions/targets/_search", + post: "/v2beta/actions/targets/search", body: "*" }; @@ -372,7 +372,8 @@ service ActionService { // - `actions` rpc ListExecutions (ListExecutionsRequest) returns (ListExecutionsResponse) { option (google.api.http) = { - post: "/v2beta/actions/executions/_search" + post: "/v2beta/actions/executions/search" + body: "*" }; option (zitadel.protoc_gen_zitadel.v2.options) = { @@ -663,8 +664,9 @@ message ListTargetsRequest { } message ListTargetsResponse { + reserved 'result'; zitadel.filter.v2beta.PaginationResponse pagination = 1; - repeated Target result = 2; + repeated Target targets = 2; } message SetExecutionRequest { @@ -673,7 +675,7 @@ message SetExecutionRequest { // Ordered list of targets called during the execution. repeated string targets = 2; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { - example: "{\"condition\":{\"request\":{\"method\":\"zitadel.session.v2.SessionService/ListSessions\"}},\"targets\":[{\"target\":\"69629026806489455\"}]}"; + example: "{\"condition\":{\"request\":{\"method\":\"zitadel.session.v2.SessionService/ListSessions\"}},\"targets\":[\"69629026806489455\"]}"; }; } @@ -703,8 +705,9 @@ message ListExecutionsRequest { } message ListExecutionsResponse { + reserved 'result'; zitadel.filter.v2beta.PaginationResponse pagination = 1; - repeated Execution result = 2; + repeated Execution executions = 2; } message ListExecutionFunctionsRequest{} diff --git a/proto/zitadel/action/v2beta/execution.proto b/proto/zitadel/action/v2beta/execution.proto index e93470e5dc..61f535abcc 100644 --- a/proto/zitadel/action/v2beta/execution.proto +++ b/proto/zitadel/action/v2beta/execution.proto @@ -11,7 +11,6 @@ import "validate/validate.proto"; import "zitadel/protoc_gen_zitadel/v2/options.proto"; import "google/protobuf/timestamp.proto"; -import "zitadel/object/v3alpha/object.proto"; option go_package = "github.com/zitadel/zitadel/pkg/grpc/action/v2beta;action"; diff --git a/proto/zitadel/admin.proto b/proto/zitadel/admin.proto index c173da9181..da496b7c7d 100644 --- a/proto/zitadel/admin.proto +++ b/proto/zitadel/admin.proto @@ -1237,6 +1237,7 @@ service AdminService { }; } + // Deprecated: use ListOrganization [apis/resources/org_service_v2beta/organization-service-list-organizations.api.mdx] API instead rpc ListOrgs(ListOrgsRequest) returns (ListOrgsResponse) { option (google.api.http) = { post: "/orgs/_search"; @@ -1256,7 +1257,8 @@ service AdminService { value: { description: "list of organizations matching the query"; }; - }; + } + deprecated: true; responses: { key: "400"; value: { @@ -1271,6 +1273,7 @@ service AdminService { }; } + // Deprecated: use CreateOrganization [apis/resources/org_service_v2beta/organization-service-create-organization.api.mdx] API instead rpc SetUpOrg(SetUpOrgRequest) returns (SetUpOrgResponse) { option (google.api.http) = { post: "/orgs/_setup"; @@ -1290,7 +1293,8 @@ service AdminService { value: { description: "org, user and user membership were created successfully"; }; - }; + } + deprecated: true; responses: { key: "400"; value: { @@ -1305,6 +1309,7 @@ service AdminService { }; } + // Deprecated: use DeleteOrganization [apis/resources/org_service_v2beta/organization-service-delete-organization.api.mdx] API instead rpc RemoveOrg(RemoveOrgRequest) returns (RemoveOrgResponse) { option (google.api.http) = { delete: "/orgs/{org_id}" @@ -1322,7 +1327,8 @@ service AdminService { value: { description: "org removed successfully"; }; - }; + } + deprecated: true; responses: { key: "400"; value: { @@ -8999,6 +9005,7 @@ message DataOrg { repeated zitadel.management.v1.SetCustomVerifySMSOTPMessageTextRequest verify_sms_otp_messages = 37; repeated zitadel.management.v1.SetCustomVerifyEmailOTPMessageTextRequest verify_email_otp_messages = 38; repeated zitadel.management.v1.SetCustomInviteUserMessageTextRequest invite_user_messages = 39; + zitadel.org.v1.OrgState org_state = 40; } message ImportDataResponse{ diff --git a/proto/zitadel/analytics/v2beta/telemetry.proto b/proto/zitadel/analytics/v2beta/telemetry.proto new file mode 100644 index 0000000000..f0e1537f9a --- /dev/null +++ b/proto/zitadel/analytics/v2beta/telemetry.proto @@ -0,0 +1,48 @@ +syntax = "proto3"; + +package zitadel.analytics.v2beta; + +import "google/protobuf/timestamp.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/analytics/v2beta;analytics"; + + +message InstanceInformation { + // The unique identifier of the instance. + string id = 1; + // The custom domains (incl. generated ones) of the instance. + repeated string domains = 2; + // The creation date of the instance. + google.protobuf.Timestamp created_at = 3; +} + +message ResourceCount { + // The ID of the instance for which the resource counts are reported. + string instance_id = 3; + // The parent type of the resource counts (e.g. organization or instance). + // For example, reporting the amount of users per organization would use + // `COUNT_PARENT_TYPE_ORGANIZATION` as parent type and the organization ID as parent ID. + CountParentType parent_type = 4; + // The parent ID of the resource counts (e.g. organization or instance ID). + // For example, reporting the amount of users per organization would use + // `COUNT_PARENT_TYPE_ORGANIZATION` as parent type and the organization ID as parent ID. + string parent_id = 5; + // The resource counts to report, e.g. amount of `users`, `organizations`, etc. + string resource_name = 6; + // The name of the table in the database, which was used to calculate the counts. + // This can be used to deduplicate counts in case of multiple reports. + // For example, if the counts were calculated from the `users14` table, + // the table name would be `users14`, where there could also be a `users15` table + // reported at the same time as the system is rolling out a new version. + string table_name = 7; + // The timestamp when the count was last updated. + google.protobuf.Timestamp updated_at = 8; + // The actual amount of the resource. + uint32 amount = 9; +} + +enum CountParentType { + COUNT_PARENT_TYPE_UNSPECIFIED = 0; + COUNT_PARENT_TYPE_INSTANCE = 1; + COUNT_PARENT_TYPE_ORGANIZATION = 2; +} diff --git a/proto/zitadel/analytics/v2beta/telemetry_service.proto b/proto/zitadel/analytics/v2beta/telemetry_service.proto new file mode 100644 index 0000000000..e71536a811 --- /dev/null +++ b/proto/zitadel/analytics/v2beta/telemetry_service.proto @@ -0,0 +1,79 @@ +syntax = "proto3"; + +package zitadel.analytics.v2beta; + +import "google/protobuf/timestamp.proto"; +import "zitadel/analytics/v2beta/telemetry.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/analytics/v2beta;analytics"; + +// The TelemetryService is used to report telemetry such as usage statistics of the ZITADEL instance(s). +// back to a central storage. +// It is used to collect anonymized data about the usage of ZITADEL features, capabilities, and configurations. +// ZITADEL acts as a client of the TelemetryService. +// +// Reports are sent periodically based on the system's runtime configuration. +// The content of the reports, respectively the data collected, can be configured in the system's runtime configuration. +// +// All endpoints follow the same error and retry handling: +// In case of a failure to report the usage, ZITADEL will retry to report the usage +// based on the configured retry policy and error type: +// - Client side errors will not be retried, as they indicate a misconfiguration or an invalid request: +// - `INVALID_ARGUMENT`: The request was malformed. +// - `NOT_FOUND`: The TelemetryService's endpoint is likely misconfigured. +// - Connection / transfer errors will be retried based on the retry policy configured in the system's runtime configuration: +// - `DEADLINE_EXCEEDED`: The request took too long to complete, it will be retried. +// - `RESOURCE_EXHAUSTED`: The request was rejected due to resource exhaustion, it will be retried after a backoff period. +// - `UNAVAILABLE`: The TelemetryService is currently unavailable, it will be retried after a backoff period. +// Server side errors will also be retried based on the information provided by the server: +// - `FAILED_PRECONDITION`: The request failed due to a precondition, e.g. the report ID does not exists, +// does not correspond to the same system ID or previous reporting is too old, do not retry. +// - `INTERNAL`: An internal error occurred. Check details and logs. +service TelemetryService { + + // ReportBaseInformation is used to report the base information of the ZITADEL system, + // including the version, instances, their creation date and domains. + // The response contains a report ID to link it to the resource counts or other reports. + // The report ID is only valid for the same system ID. + rpc ReportBaseInformation (ReportBaseInformationRequest) returns (ReportBaseInformationResponse) {} + + // ReportResourceCounts is used to report the resource counts such as amount of organizations + // or users per organization and much more. + // Since the resource counts can be reported in multiple batches, + // the response contains a report ID to continue reporting. + // The report ID is only valid for the same system ID. + rpc ReportResourceCounts (ReportResourceCountsRequest) returns (ReportResourceCountsResponse) {} +} + +message ReportBaseInformationRequest { + // The system ID is a unique identifier for the ZITADEL system. + string system_id = 1; + // The current version of the ZITADEL system. + string version = 2; + // A list of instances in the ZITADEL system and their information. + repeated InstanceInformation instances = 3; +} + +message ReportBaseInformationResponse { + // The report ID is a unique identifier for the report. + // It is used to identify the report to be able to link it to the resource counts or other reports. + // Note that the report ID is only valid for the same system ID. + string report_id = 1; +} + +message ReportResourceCountsRequest { + // The system ID is a unique identifier for the ZITADEL system. + string system_id = 1; + // The previously returned report ID from the server to continue reporting. + // Note that the report ID is only valid for the same system ID. + optional string report_id = 2; + // A list of resource counts to report. + repeated ResourceCount resource_counts = 3; +} + +message ReportResourceCountsResponse { + // The report ID is a unique identifier for the report. + // It is used to identify the report in case of additional data / pagination. + // Note that the report ID is only valid for the same system ID. + string report_id = 1; +} \ No newline at end of file diff --git a/proto/zitadel/app/v2beta/api.proto b/proto/zitadel/app/v2beta/api.proto new file mode 100644 index 0000000000..9ef09d5ad8 --- /dev/null +++ b/proto/zitadel/app/v2beta/api.proto @@ -0,0 +1,26 @@ +syntax = "proto3"; + +package zitadel.app.v2beta; + +import "protoc-gen-openapiv2/options/annotations.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/app/v2beta;app"; + +enum APIAuthMethodType { + API_AUTH_METHOD_TYPE_BASIC = 0; + API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT = 1; +} + +message APIConfig { + string client_id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334@ZITADEL\""; + description: "generated oauth2/oidc client_id"; + } + ]; + APIAuthMethodType auth_method_type = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "defines how the API passes the login credentials"; + } + ]; +} \ No newline at end of file diff --git a/proto/zitadel/app/v2beta/app.proto b/proto/zitadel/app/v2beta/app.proto new file mode 100644 index 0000000000..f108f3bacb --- /dev/null +++ b/proto/zitadel/app/v2beta/app.proto @@ -0,0 +1,121 @@ +syntax = "proto3"; + +package zitadel.app.v2beta; + +import "zitadel/app/v2beta/oidc.proto"; +import "zitadel/app/v2beta/saml.proto"; +import "zitadel/app/v2beta/api.proto"; +import "zitadel/filter/v2/filter.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "google/protobuf/duration.proto"; +import "google/protobuf/timestamp.proto"; +import "validate/validate.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/app/v2beta;app"; + +message Application { + string id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\""; + } + ]; + + // The timestamp of the app creation. + google.protobuf.Timestamp creation_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + + // The timestamp of the app update. + google.protobuf.Timestamp change_date = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + + AppState state = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "current state of the application"; + } + ]; + string name = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"Console\""; + } + ]; + oneof config { + OIDCConfig oidc_config = 6; + APIConfig api_config = 7; + SAMLConfig saml_config = 8; + } +} + +enum AppState { + APP_STATE_UNSPECIFIED = 0; + APP_STATE_ACTIVE = 1; + APP_STATE_INACTIVE = 2; + APP_STATE_REMOVED = 3; +} + +enum AppSorting { + APP_SORT_BY_ID = 0; + APP_SORT_BY_NAME = 1; + APP_SORT_BY_STATE = 2; + APP_SORT_BY_CREATION_DATE = 3; + APP_SORT_BY_CHANGE_DATE = 4; +} + +message ApplicationSearchFilter { + oneof filter { + option (validate.required) = true; + ApplicationNameQuery name_filter = 1; + AppState state_filter = 2; + bool api_app_only = 3; + bool oidc_app_only = 4; + bool saml_app_only = 5; + } +} + +message ApplicationNameQuery { + string name = 1 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"Conso\"" + } + ]; + + zitadel.filter.v2.TextFilterMethod method = 2 [ + (validate.rules).enum.defined_only = true, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "defines which text equality method is used" + } + ]; +} + +enum ApplicationKeysSorting { + APPLICATION_KEYS_SORT_BY_ID = 0; + APPLICATION_KEYS_SORT_BY_PROJECT_ID = 1; + APPLICATION_KEYS_SORT_BY_APPLICATION_ID = 2; + APPLICATION_KEYS_SORT_BY_CREATION_DATE = 3; + APPLICATION_KEYS_SORT_BY_ORGANIZATION_ID = 4; + APPLICATION_KEYS_SORT_BY_EXPIRATION = 5; + APPLICATION_KEYS_SORT_BY_TYPE = 6; +} + +message ApplicationKey { + string id = 1; + string application_id = 2; + string project_id = 3; + google.protobuf.Timestamp creation_date = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + string organization_id = 5; + google.protobuf.Timestamp expiration_date = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; +} \ No newline at end of file diff --git a/proto/zitadel/app/v2beta/app_service.proto b/proto/zitadel/app/v2beta/app_service.proto new file mode 100644 index 0000000000..61cde73696 --- /dev/null +++ b/proto/zitadel/app/v2beta/app_service.proto @@ -0,0 +1,994 @@ +syntax = "proto3"; + +package zitadel.app.v2beta; + +import "google/api/annotations.proto"; +import "google/api/field_behavior.proto"; +import "google/protobuf/duration.proto"; +import "google/protobuf/struct.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; +import "zitadel/app/v2beta/login.proto"; +import "zitadel/app/v2beta/oidc.proto"; +import "zitadel/app/v2beta/api.proto"; +import "zitadel/app/v2beta/app.proto"; +import "google/protobuf/timestamp.proto"; +import "zitadel/protoc_gen_zitadel/v2/options.proto"; +import "zitadel/filter/v2/filter.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/app/v2beta;app"; + +option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { + info: { + title: "Application Service"; + version: "2.0-beta"; + description: "This API is intended to manage apps (SAML, OIDC, etc..) in a ZITADEL instance. This service is in beta state. It can AND will continue breaking until a stable version is released."; + contact:{ + name: "ZITADEL" + url: "https://zitadel.com" + email: "hi@zitadel.com" + } + license: { + name: "Apache 2.0", + url: "https://github.com/zitadel/zitadel/blob/main/LICENSING.md"; + }; + }; + schemes: HTTPS; + schemes: HTTP; + + consumes: "application/json"; + consumes: "application/grpc"; + + produces: "application/json"; + produces: "application/grpc"; + + consumes: "application/grpc-web+proto"; + produces: "application/grpc-web+proto"; + + host: "$CUSTOM-DOMAIN"; + base_path: "/"; + + external_docs: { + description: "Detailed information about ZITADEL", + url: "https://zitadel.com/docs" + } + security_definitions: { + security: { + key: "OAuth2"; + value: { + type: TYPE_OAUTH2; + flow: FLOW_ACCESS_CODE; + authorization_url: "$CUSTOM-DOMAIN/oauth/v2/authorize"; + token_url: "$CUSTOM-DOMAIN/oauth/v2/token"; + scopes: { + scope: { + key: "openid"; + value: "openid"; + } + scope: { + key: "urn:zitadel:iam:org:project:id:zitadel:aud"; + value: "urn:zitadel:iam:org:project:id:zitadel:aud"; + } + } + } + } + } + security: { + security_requirement: { + key: "OAuth2"; + value: { + scope: "openid"; + scope: "urn:zitadel:iam:org:project:id:zitadel:aud"; + } + } + } + responses: { + key: "403"; + value: { + description: "Returned when the user does not have permission to access the resource."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + } + } + } + } + responses: { + key: "404"; + value: { + description: "Returned when the resource does not exist."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + } + } + } + } +}; + +// Service to manage apps. +// The service provides methods to create, update, delete and list apps and app keys. +service AppService { + + // Create Application + // + // Create an application. The application can be OIDC, API or SAML type, based on the input. + // + // Required permissions: + // - project.app.write + rpc CreateApplication(CreateApplicationRequest) returns (CreateApplicationResponse) { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "The created application"; + } + }; + }; + + option (google.api.http) = { + post: "/v2beta/applications" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + } + + // Update Application + // + // Changes the configuration of an OIDC, API or SAML type application, as well as + // the application name, based on the input provided. + // + // Required permissions: + // - project.app.write + rpc UpdateApplication(UpdateApplicationRequest) returns (UpdateApplicationResponse) { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "The updated app."; + } + }; + }; + + option (google.api.http) = { + patch: "/v2beta/applications/{id}" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + } + + // Get Application + // + // Retrieves the application matching the provided ID. + // + // Required permissions: + // - project.app.read + rpc GetApplication(GetApplicationRequest) returns (GetApplicationResponse) { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "The fetched app."; + } + }; + }; + + option (google.api.http) = { + get: "/v2beta/applications/{id}" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + } + + // Delete Application + // + // Deletes the application belonging to the input project and matching the provided + // application ID. + // + // Required permissions: + // - project.app.delete + rpc DeleteApplication(DeleteApplicationRequest) returns (DeleteApplicationResponse) { + option (google.api.http) = { + delete: "/v2beta/applications/{id}" + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "The time of deletion."; + } + }; + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + } + + // Deactivate Application + // + // Deactivates the application belonging to the input project and matching the provided + // application ID. + // + // Required permissions: + // - project.app.write + rpc DeactivateApplication(DeactivateApplicationRequest) returns (DeactivateApplicationResponse) { + option (google.api.http) = { + post: "/v2beta/applications/{id}/deactivate" + body: "*" + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "The time of deactivation."; + } + }; + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + } + + // Reactivate Application + // + // Reactivates the application belonging to the input project and matching the provided + // application ID. + // + // Required permissions: + // - project.app.write + rpc ReactivateApplication(ReactivateApplicationRequest) returns (ReactivateApplicationResponse) { + option (google.api.http) = { + post: "/v2beta/applications/{id}/reactivate" + body: "*" + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "The time of reactivation."; + } + }; + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + } + + + // Regenerate Client Secret + // + // Regenerates the client secret of an API or OIDC application that belongs to the input project. + // + // Required permissions: + // - project.app.write + rpc RegenerateClientSecret(RegenerateClientSecretRequest) returns (RegenerateClientSecretResponse) { + option (google.api.http) = { + post: "/v2beta/applications/{application_id}/generate_client_secret" + body: "*" + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "The regenerated client secret."; + } + }; + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + } + + // List Applications + // + // Returns a list of applications matching the input parameters that belong to the provided + // project. + // + // The result can be sorted by app id, name, creation date, change date or state. It can also + // be filtered by app state, app type and app name. + // + // Required permissions: + // - project.app.read + rpc ListApplications(ListApplicationsRequest) returns (ListApplicationsResponse) { + option (google.api.http) = { + post: "/v2beta/applications/search" + body: "*" + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "The matching applications"; + } + }; + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + } + + + // Create Application Key + // + // Create a new application key, which is used to authorize an API application. + // + // Key details are returned in the response. They must be stored safely, as it will not + // be possible to retrieve them again. + // + // Required permissions: + // - `project.app.write` + rpc CreateApplicationKey(CreateApplicationKeyRequest) returns (CreateApplicationKeyResponse) { + option (google.api.http) = { + post: "/v2beta/application_keys" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "The created application key"; + } + }; + }; + } + + // Delete Application Key + // + // Deletes an application key matching the provided ID. + // + // Organization ID is not mandatory, but helps with filtering/performance. + // + // The deletion time is returned in response message. + // + // Required permissions: + // - `project.app.write` + rpc DeleteApplicationKey(DeleteApplicationKeyRequest) returns (DeleteApplicationKeyResponse) { + option (google.api.http) = { + delete: "/v2beta/application_keys/{id}" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "The time of deletion."; + } + }; + }; + } + + // Get Application Key + // + // Retrieves the application key matching the provided ID. + // + // Specifying a project, organization and app ID is optional but help with filtering/performance. + // + // Required permissions: + // - project.app.read + rpc GetApplicationKey(GetApplicationKeyRequest) returns (GetApplicationKeyResponse) { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "The fetched app key."; + } + }; + }; + + option (google.api.http) = { + get: "/v2beta/application_keys/{id}" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + } + + // List Application Keys + // + // Returns a list of application keys matching the input parameters. + // + // The result can be sorted by id, aggregate, creation date, expiration date, resource owner or type. + // It can also be filtered by app, project or organization ID. + // + // Required permissions: + // - project.app.read + rpc ListApplicationKeys(ListApplicationKeysRequest) returns (ListApplicationKeysResponse) { + option (google.api.http) = { + post: "/v2beta/application_keys/search" + body: "*" + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "The matching applications"; + } + }; + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + } +} + +message CreateApplicationRequest { + string project_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string id = 2 [(validate.rules).string = {max_len: 200}]; + string name = 3 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"MyApp\""; + } + ]; + oneof creation_request_type { + option (validate.required) = true; + CreateOIDCApplicationRequest oidc_request = 4; + CreateSAMLApplicationRequest saml_request = 5; + CreateAPIApplicationRequest api_request = 6; + } +} + +message CreateApplicationResponse { + string app_id = 1; + // The timestamp of the app creation. + google.protobuf.Timestamp creation_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + + oneof creation_response_type { + CreateOIDCApplicationResponse oidc_response = 3; + CreateSAMLApplicationResponse saml_response = 4; + CreateAPIApplicationResponse api_response = 5; + } +} + +message CreateOIDCApplicationRequest { + // Callback URI of the authorization request where the code or tokens will be sent to + repeated string redirect_uris = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[\"http://localhost:4200/auth/callback\"]"; + description: "Callback URI of the authorization request where the code or tokens will be sent to"; + } + ]; + repeated OIDCResponseType response_types = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Determines whether a code, id_token token or just id_token will be returned" + } + ]; + repeated OIDCGrantType grant_types = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "The flow type the application uses to gain access"; + } + ]; + OIDCAppType app_type = 4 [ + (validate.rules).enum = {defined_only: true}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Determines the paradigm of the application"; + } + ]; + OIDCAuthMethodType auth_method_type = 5 [ + (validate.rules).enum = {defined_only: true}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Defines how the application passes login credentials"; + } + ]; + + // ZITADEL will redirect to this link after a successful logout + repeated string post_logout_redirect_uris = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[\"http://localhost:4200/signedout\"]"; + description: "ZITADEL will redirect to this link after a successful logout"; + } + ]; + OIDCVersion version = 7 [(validate.rules).enum = {defined_only: true}]; + bool dev_mode = 8 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Used for development, some checks of the OIDC specification will not be checked."; + } + ]; + OIDCTokenType access_token_type = 9 [ + (validate.rules).enum = {defined_only: true}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Type of the access token returned from ZITADEL"; + } + ]; + bool access_token_role_assertion = 10 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Adds roles to the claims of the access token (only if type == JWT) even if they are not requested by scopes"; + } + ]; + bool id_token_role_assertion = 11 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Adds roles to the claims of the id token even if they are not requested by scopes"; + } + ]; + bool id_token_userinfo_assertion = 12 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Claims of profile, email, address and phone scopes are added to the id token even if an access token is issued. Attention this violates the OIDC specification"; + } + ]; + google.protobuf.Duration clock_skew = 13 [ + (validate.rules).duration = {gte: {}, lte: {seconds: 5}}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Used to compensate time difference of servers. Duration added to the \"exp\" claim and subtracted from \"iat\", \"auth_time\" and \"nbf\" claims"; + example: "\"1s\""; + } + ]; + repeated string additional_origins = 14 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[\"scheme://localhost:8080\"]"; + description: "Additional origins (other than the redirect_uris) from where the API can be used, provided string has to be an origin (scheme://hostname[:port]) without path, query or fragment"; + } + ]; + bool skip_native_app_success_page = 15 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Skip the successful login page on native apps and directly redirect the user to the callback."; + } + ]; + string back_channel_logout_uri = 16 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[\"https://example.com/auth/backchannel\"]"; + description: "ZITADEL will use this URI to notify the application about terminated session according to the OIDC Back-Channel Logout (https://openid.net/specs/openid-connect-backchannel-1_0.html)"; + } + ]; + LoginVersion login_version = 17 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Specify the preferred login UI, where the user is redirected to for authentication. If unset, the login UI is chosen by the instance default."; + } + ]; +} + +message CreateOIDCApplicationResponse { + string client_id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"1035496534033449\""; + description: "generated client id for this config"; + } + ]; + string client_secret = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"gjoq34589uasgh\""; + description: "generated secret for this config"; + } + ]; + bool none_compliant = 3; + repeated OIDCLocalizedMessage compliance_problems = 4; +} + +message CreateSAMLApplicationRequest { + oneof metadata { + option (validate.required) = true; + bytes metadata_xml = 1 [(validate.rules).bytes.max_len = 500000]; + string metadata_url = 2 [(validate.rules).string.max_len = 200]; + } + LoginVersion login_version = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Specify the preferred login UI, where the user is redirected to for authentication. If unset, the login UI is chosen by the instance default."; + } + ]; +} + +message CreateSAMLApplicationResponse {} + +message CreateAPIApplicationRequest { + APIAuthMethodType auth_method_type = 1 [(validate.rules).enum = {defined_only: true}]; +} + +message CreateAPIApplicationResponse { + string client_id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"3950723409029374\""; + description: "generated secret for this config"; + } + ]; + string client_secret = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"gjoq34589uasgh\""; + description: "generated secret for this config"; + } + ]; +} + +message UpdateApplicationRequest { + string project_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string id = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"45984352431\""; + } + ]; + string name = 3 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"MyApplicationName\""; + min_length: 1; + max_length: 200; + } + ]; + + oneof update_request_type { + UpdateSAMLApplicationConfigurationRequest saml_configuration_request = 4; + UpdateOIDCApplicationConfigurationRequest oidc_configuration_request = 5; + UpdateAPIApplicationConfigurationRequest api_configuration_request = 6; + } +} + +message UpdateApplicationResponse { + // The timestamp of the app update. + google.protobuf.Timestamp change_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; +} + +message UpdateSAMLApplicationConfigurationRequest { + oneof metadata { + option (validate.required) = true; + bytes metadata_xml = 1 [(validate.rules).bytes.max_len = 500000]; + string metadata_url = 2 [(validate.rules).string.max_len = 200]; + } + optional LoginVersion login_version = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Specify the preferred login UI, where the user is redirected to for authentication. If unset, the login UI is chosen by the instance default."; + } + ]; +} + +message UpdateOIDCApplicationConfigurationRequest { + repeated string redirect_uris = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[\"http://localhost:4200/auth/callback\"]"; + description: "Callback URI of the authorization request where the code or tokens will be sent to"; + } + ]; + repeated OIDCResponseType response_types = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Determines whether a code, id_token token or just id_token will be returned" + } + ]; + repeated OIDCGrantType grant_types = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "The flow type the application uses to gain access"; + } + ]; + optional OIDCAppType app_type = 4 [ + (validate.rules).enum = {defined_only: true}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Determines the paradigm of the application"; + } + ]; + optional OIDCAuthMethodType auth_method_type = 5 [ + (validate.rules).enum = {defined_only: true}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Defines how the application passes login credentials"; + } + ]; + repeated string post_logout_redirect_uris = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[\"http://localhost:4200/signedout\"]"; + description: "ZITADEL will redirect to this link after a successful logout"; + } + ]; + optional OIDCVersion version = 7 [(validate.rules).enum = {defined_only: true}]; + optional bool dev_mode = 8 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Used for development, some checks of the OIDC specification will not be checked."; + } + ]; + optional OIDCTokenType access_token_type = 9 [ + (validate.rules).enum = {defined_only: true}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Type of the access token returned from ZITADEL"; + } + ]; + optional bool access_token_role_assertion = 10 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Adds roles to the claims of the access token (only if type == JWT) even if they are not requested by scopes"; + } + ]; + optional bool id_token_role_assertion = 11 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Adds roles to the claims of the id token even if they are not requested by scopes"; + } + ]; + optional bool id_token_userinfo_assertion = 12 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Claims of profile, email, address and phone scopes are added to the id token even if an access token is issued. Attention this violates the OIDC specification"; + } + ]; + optional google.protobuf.Duration clock_skew = 13 [ + (validate.rules).duration = {gte: {}, lte: {seconds: 5}}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Used to compensate time difference of servers. Duration added to the \"exp\" claim and subtracted from \"iat\", \"auth_time\" and \"nbf\" claims"; + example: "\"1s\""; + } + ]; + repeated string additional_origins = 14 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[\"scheme://localhost:8080\"]"; + description: "Additional origins (other than the redirect_uris) from where the API can be used, provided string has to be an origin (scheme://hostname[:port]) without path, query or fragment"; + } + ]; + optional bool skip_native_app_success_page = 15 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Skip the successful login page on native apps and directly redirect the user to the callback."; + } + ]; + optional string back_channel_logout_uri = 16 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[\"https://example.com/auth/backchannel\"]"; + description: "ZITADEL will use this URI to notify the application about terminated session according to the OIDC Back-Channel Logout (https://openid.net/specs/openid-connect-backchannel-1_0.html)"; + } + ]; + optional LoginVersion login_version = 17 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Specify the preferred login UI, where the user is redirected to for authentication. If unset, the login UI is chosen by the instance default."; + } + ]; +} + +message UpdateAPIApplicationConfigurationRequest { + APIAuthMethodType auth_method_type = 1 [(validate.rules).enum = {defined_only: true}]; +} + +message GetApplicationRequest { + string id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"45984352431\""; + } + ]; +} + +message GetApplicationResponse { + Application app = 1; +} + +message DeleteApplicationRequest { + string project_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string id = 2 [(validate.rules).string = {min_len: 1, max_len: 200}]; +} + +message DeleteApplicationResponse { + google.protobuf.Timestamp deletion_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message DeactivateApplicationRequest{ + string project_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string id = 2 [(validate.rules).string = {min_len: 1, max_len: 200}]; +} + +message DeactivateApplicationResponse{ + google.protobuf.Timestamp deactivation_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message ReactivateApplicationRequest{ + string project_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string id = 2 [(validate.rules).string = {min_len: 1, max_len: 200}]; +} + +message ReactivateApplicationResponse{ + google.protobuf.Timestamp reactivation_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message RegenerateClientSecretRequest{ + string project_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string application_id = 2 [(validate.rules).string = {min_len: 1, max_len: 200}]; + oneof app_type { + option (validate.required) = true; + bool is_oidc = 3; + bool is_api = 4; + } +} + +message RegenerateClientSecretResponse{ + string client_secret = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"gjoq34589uasgh\""; + description: "generated secret for the client"; + } + ]; + + // The timestamp of the creation of the new client secret + google.protobuf.Timestamp creation_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message ListApplicationsRequest { + string project_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + + // Pagination and sorting. + zitadel.filter.v2.PaginationRequest pagination = 2; + + //criteria the client is looking for + repeated ApplicationSearchFilter filters = 3; + + AppSorting sorting_column = 4; +} + +message ListApplicationsResponse { + repeated Application applications = 1; + + // Contains the total number of apps matching the query and the applied limit. + zitadel.filter.v2.PaginationResponse pagination = 2; +} + +message CreateApplicationKeyRequest { + string app_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + + string project_id = 2 [(validate.rules).string = {min_len: 1, max_len: 200}]; + + // The date the key will expire + google.protobuf.Timestamp expiration_date = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2519-04-01T08:45:00.000000Z\""; + description: "The date the key will expire"; + } + ]; +} + +message CreateApplicationKeyResponse { + string id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"28746028909593987\""; + } + ]; + + // The timestamp of the app creation. + google.protobuf.Timestamp creation_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + + bytes key_details = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"eyJ0eXBlIjoiYXBwbGljYXRpb24iLCJrZXlJZCI6IjIwMjcxMDE4NjYyMjcxNDExMyIsImtleSI6Ii0tLS0tQkVHSU4gUlNBIFBSSVZBVEUgS0VZLS0tLS1cbk1JSUVvd0lCQUFLQ0FRRUFuMUxyNStTV0pGRllURU1kaXQ2U0dNY0E2Yks5dG0xMmhlcm55V0wrZm9PWnA3eEVcbk9wcmsvWE81QVplSU5NY0x0ZVhxckJlK1NPdVVNMFpLU2xCMHFTNzNjVStDVTVMTGoycVB0UzhNOFI0N3BGdFhcbjJXRTFJNjNhZHB1N01TejA2SXduQ2lyNnJYOTVPQ2ZneHA3VU1Dd0pSTUZmYXJqdjVBRXY3NXpsSS9lYUV6bUJcbkxKWU1xanZFRmZoN2x3M2lPT3VsWW9kNjNpN3RDNWl5czNlYjNLZW4yWU0rN1FSbXB2dE5qcTJMVmlIMnkrUGJcbk9ESlI3MU9ib05TYVJDNTZDUFpWVytoWDByYXI3VzMwUjI2eGtIQ09oSytQbUpSeGtOY0g1VTdja0xXMEw0WEVcbnNNZkVUSmszeDR3Q0psbisxbElXUzkrNmw0R1E2TWRzWURyOU5RSURBUUFCQW9JQkFCSkx6WGQxMHFBZEQwekNcbnNGUFFOMnJNLzVmV3hONThONDR0YWF6QXg0VHp5K050UlZDTmxScGQvYkxuR2VjbHJIeVpDSmYycWcxcHNEMHJcbkowRGRlR2d0VXBFYWxsYk9scjNEZVBsUGkrYnNsK0RKOUk2c0VSUWwxTjZtQjVzZ0ZJZllBR3UwZjlFSXdIem9cblozR25yNnBRaEVmM0JPUVdsTVhVTlJNSksyOHp3M2E1L01nRmtKVUZUSTUzeXFwbGRtZ2hLajRZR1hLRk1LUGhcbkV3RkxrRncwK2s3K0xuSjFQNGp1ZVd1RXo3WlAyaFpvUWxCcXdSajVyTG9QZ05RbUU4UytFVDRuczlUYzByOFFcbnFyaHlacDZBczJrTDhGTytCZnF3SVpDZnpnWHN2cC9PLzRaSHIzVTB2Ymp3UW1sSzdVSm42U0J6T2hpWFpNU0lcbk5Wc0V5VUVDZ1lFQTFEaktkRGo3NTM1MWQzdlRNQlRFd2JSQ3hoUVZOdENFMnMwVUw4ckJQZ1I0K1dlblNUWmFcbnprWUprcEV0bE54VGxzYnN1Y0RTUXZqeWRYYk5nSHFBeDYzMm1vdTVkak9lR0VTUDFWVGtUdElsZFZQZWszQWxcbjVYbkpQa1dqWGVyVVJZNm5KeUQ5UWhlREx3MVp4NEFYVzNHWURiTFkrT05XV0VKUlJaQUloNjBDZ1lFQXdEQ2xcbnc1MHc4dkcvbEJ4RzNSYW9FaHdLOWNna1VXOHk2T25DekNwcEtjOEZUUmY1VE5iWjl5TzNXUmdYajhkeHRCakFcbkl5VGlzYk9NQk1VaFZKUUtGZHRQaDhoVDBwRkRjeE9ndzY0aHBtYzhyY2RTbXVKNzlYSVRTaHUySjA0N0UvNFZcbnJOTThpWVk5ZGR3VGdGUUlsdFNZL0l0RnFxWERmdjhqK1dVY25La0NnWUVBaENOUU80bDNuNjRucWR2WnBTaHBcblVrclJBTkJrWFJyOGZkZ1BaNnFSSS9KWStNSEhjVmg4dGM3NkN0NkdTUmZlbkJVRU5LeVF2czZPK1FDZCtBOU9cbnZBWGZkRjduZldlcVdtWG1RT2g0dDNNMWk1WkxFZlpVUWt2UU9BdllLcFFhMDZ4OCsyb1pCdHZvL0pVTmY2Q0xcbjZvNFNKUVZrLzZOZGtkckpDODBnNG9rQ2dZQkZsNWYrbkVYa1F0dWZVeG5wNXRGWE5XWldsM0ZuTjMvVXpRaW5cbmkxZm5OcnB4cnhPcjJrUzA4KzdwU1FzSEdpNDNDNXRQWG9UajJlTUN1eXNWaUVHYXBuNUc2YWhJb0NjdlhWVWlcblprUnpFQUR0NERZdU5ZS3pYdXBUTkhPaUNmYmtoMlhyM2RXVzZ0QUloSGRmU1k2T3AwNzZhNmYvWWVUSGNMWGpcbkVkVHBlUUtCZ0FPdnBqcDQ4TzRUWEZkU0JLSnYya005OHVhUjlSQURtdGxTWHd2cTlyQkhTV084NFk4bzE0L1Bcbkl1UmxUOHhROGRYKzhMR21UUCtjcUtiOFFRQ1grQk1YUWxMSEVtWnpnb0xFa0pGMUVIMm4vZEZ5bngxS3prdFNcbm9UZUdsRzZhbXhVOVh4eW9RVFlEVGJCbERwc2FZUlFBZ2FUQzM3UVZRUjhmK1ZoRzFHSFFcbi0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tXG4iLCJhcHBJZCI6IjIwMjcwNjM5ODgxMzg4MDU3NyIsImNsaWVudElkIjoiMjAyNzA2Mzk4ODEzOTQ2MTEzQG15dGVzdHByb2plY3QifQ==\""; + } + ]; +} + +message DeleteApplicationKeyRequest { + string id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string project_id = 2 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string application_id = 3 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string organization_id = 4 [(validate.rules).string = {max_len: 200}]; +} + +message DeleteApplicationKeyResponse { + google.protobuf.Timestamp deletion_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message GetApplicationKeyRequest { + string id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string project_id = 2 [(validate.rules).string = {max_len: 200}]; + string application_id = 3 [(validate.rules).string = {max_len: 200}]; + string organization_id = 4 [(validate.rules).string = {max_len: 200}]; +} + +message GetApplicationKeyResponse { + string id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\""; + } + ]; + + google.protobuf.Timestamp creation_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; + + // the date a key will expire + google.protobuf.Timestamp expiration_date = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "the date a key will expire"; + example: "\"3019-04-01T08:45:00.000000Z\""; + } + ]; +} + +message ListApplicationKeysRequest { + // Pagination and sorting. + zitadel.filter.v2.PaginationRequest pagination = 1; + + ApplicationKeysSorting sorting_column = 2; + + oneof resource_id { + string application_id = 3 [(validate.rules).string = {min_len: 1; max_len: 200}]; + string project_id = 4 [(validate.rules).string = {min_len: 1; max_len: 200}]; + string organization_id = 5 [(validate.rules).string = {min_len: 1; max_len: 200}]; + } +} + +message ListApplicationKeysResponse { + repeated ApplicationKey keys = 1; + + // Contains the total number of app keys matching the query and the applied limit. + zitadel.filter.v2.PaginationResponse pagination = 2; +} diff --git a/proto/zitadel/app/v2beta/login.proto b/proto/zitadel/app/v2beta/login.proto new file mode 100644 index 0000000000..567b4b5167 --- /dev/null +++ b/proto/zitadel/app/v2beta/login.proto @@ -0,0 +1,18 @@ +syntax = "proto3"; + +package zitadel.app.v2beta; +option go_package = "github.com/zitadel/zitadel/pkg/grpc/app/v2beta;app"; + +message LoginVersion { + oneof version { + LoginV1 login_v1 = 1; + LoginV2 login_v2 = 2; + } +} + +message LoginV1 {} + +message LoginV2 { + // Optionally specify a base uri of the login UI. If unspecified the default URI will be used. + optional string base_uri = 1; +} \ No newline at end of file diff --git a/proto/zitadel/app/v2beta/oidc.proto b/proto/zitadel/app/v2beta/oidc.proto new file mode 100644 index 0000000000..7cfd1dcc43 --- /dev/null +++ b/proto/zitadel/app/v2beta/oidc.proto @@ -0,0 +1,166 @@ +syntax = "proto3"; + +package zitadel.app.v2beta; +import "zitadel/app/v2beta/login.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "google/protobuf/duration.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/app/v2beta;app"; + +message OIDCLocalizedMessage { + string key = 1; + string localized_message = 2; +} + +enum OIDCResponseType { + OIDC_RESPONSE_TYPE_UNSPECIFIED = 0; + OIDC_RESPONSE_TYPE_CODE = 1; + OIDC_RESPONSE_TYPE_ID_TOKEN = 2; + OIDC_RESPONSE_TYPE_ID_TOKEN_TOKEN = 3; +} + +enum OIDCGrantType{ + OIDC_GRANT_TYPE_AUTHORIZATION_CODE = 0; + OIDC_GRANT_TYPE_IMPLICIT = 1; + OIDC_GRANT_TYPE_REFRESH_TOKEN = 2; + OIDC_GRANT_TYPE_DEVICE_CODE = 3; + OIDC_GRANT_TYPE_TOKEN_EXCHANGE = 4; +} + +enum OIDCAppType { + OIDC_APP_TYPE_WEB = 0; + OIDC_APP_TYPE_USER_AGENT = 1; + OIDC_APP_TYPE_NATIVE = 2; +} + +enum OIDCAuthMethodType { + OIDC_AUTH_METHOD_TYPE_BASIC = 0; + OIDC_AUTH_METHOD_TYPE_POST = 1; + OIDC_AUTH_METHOD_TYPE_NONE = 2; + OIDC_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT = 3; +} + +enum OIDCVersion { + OIDC_VERSION_1_0 = 0; +} + +enum OIDCTokenType { + OIDC_TOKEN_TYPE_BEARER = 0; + OIDC_TOKEN_TYPE_JWT = 1; +} + +message OIDCConfig { + repeated string redirect_uris = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[\"https://console.zitadel.ch/auth/callback\"]"; + description: "Callback URI of the authorization request where the code or tokens will be sent to"; + } + ]; + repeated OIDCResponseType response_types = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Determines whether a code, id_token token or just id_token will be returned" + } + ]; + repeated OIDCGrantType grant_types = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "The flow type the application uses to gain access"; + } + ]; + OIDCAppType app_type = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "determines the paradigm of the application"; + } + ]; + string client_id = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334@ZITADEL\""; + description: "generated oauth2/oidc client id"; + } + ]; + OIDCAuthMethodType auth_method_type = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "defines how the application passes login credentials"; + } + ]; + repeated string post_logout_redirect_uris = 7 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[\"https://console.zitadel.ch/logout\"]"; + description: "ZITADEL will redirect to this link after a successful logout"; + } + ]; + OIDCVersion version = 8 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "the OIDC version used by the application"; + } + ]; + bool none_compliant = 9 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "specifies whether the config is OIDC compliant. A production configuration SHOULD be compliant"; + } + ]; + repeated OIDCLocalizedMessage compliance_problems = 10 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "lists the problems for non-compliancy"; + } + ]; + bool dev_mode = 11 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "used for development"; + } + ]; + OIDCTokenType access_token_type = 12 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "type of the access token returned from ZITADEL"; + } + ]; + bool access_token_role_assertion = 13 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "adds roles to the claims of the access token (only if type == JWT) even if they are not requested by scopes"; + } + ]; + bool id_token_role_assertion = 14 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "adds roles to the claims of the id token even if they are not requested by scopes"; + } + ]; + bool id_token_userinfo_assertion = 15 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "claims of profile, email, address and phone scopes are added to the id token even if an access token is issued. Attention this violates the OIDC specification"; + } + ]; + google.protobuf.Duration clock_skew = 16 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Used to compensate time difference of servers. Duration added to the \"exp\" claim and subtracted from \"iat\", \"auth_time\" and \"nbf\" claims"; + // min: "0s"; + // max: "5s"; + } + ]; + repeated string additional_origins = 17 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[\"https://console.zitadel.ch/auth/callback\"]"; + description: "additional origins (other than the redirect_uris) from where the API can be used"; + } + ]; + repeated string allowed_origins = 18 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[\"https://console.zitadel.ch/auth/callback\"]"; + description: "all allowed origins from where the API can be used"; + } + ]; + bool skip_native_app_success_page = 19 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Skip the successful login page on native apps and directly redirect the user to the callback."; + } + ]; + string back_channel_logout_uri = 20 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[\"https://example.com/auth/backchannel\"]"; + description: "ZITADEL will use this URI to notify the application about terminated session according to the OIDC Back-Channel Logout (https://openid.net/specs/openid-connect-backchannel-1_0.html)"; + } + ]; + LoginVersion login_version = 21 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Specify the preferred login UI, where the user is redirected to for authentication. If unset, the login UI is chosen by the instance default."; + } + ]; +} \ No newline at end of file diff --git a/proto/zitadel/app/v2beta/saml.proto b/proto/zitadel/app/v2beta/saml.proto new file mode 100644 index 0000000000..7c85447880 --- /dev/null +++ b/proto/zitadel/app/v2beta/saml.proto @@ -0,0 +1,20 @@ +syntax = "proto3"; + +package zitadel.app.v2beta; + +import "zitadel/app/v2beta/login.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/app/v2beta;app"; + +message SAMLConfig { + oneof metadata{ + bytes metadata_xml = 1; + string metadata_url = 2; + } + LoginVersion login_version = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Specify the preferred login UI, where the user is redirected to for authentication. If unset, the login UI is chosen by the instance default."; + } + ]; +} \ No newline at end of file diff --git a/proto/zitadel/auth.proto b/proto/zitadel/auth.proto index 0ee6ad86d8..c995bce16a 100644 --- a/proto/zitadel/auth.proto +++ b/proto/zitadel/auth.proto @@ -859,6 +859,11 @@ service AuthService { }; } + // List My Authorizations / User Grants + // + // Deprecated: [List authorizations](apis/resources/authorization_service_v2/zitadel-authorization-v-2-beta-authorization-service-list-authorizations.api.mdx) and pass the user ID filter with your users ID to search for your authorizations on granted and owned projects. + // + // Returns a list of the authorizations/user grants the authenticated user has. User grants consist of an organization, a project and 1-n roles. rpc ListMyUserGrants(ListMyUserGrantsRequest) returns (ListMyUserGrantsResponse) { option (google.api.http) = { post: "/usergrants/me/_search" @@ -869,9 +874,8 @@ service AuthService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - tags: "User Authorizations/Grants" - summary: "List My Authorizations/Grants"; - description: "Returns a list of the authorizations/user grants the authenticated user has. User grants consist of an organization, a project and 1-n roles." + tags: "User Authorizations/Grants"; + deprecated: true; }; } @@ -908,6 +912,11 @@ service AuthService { }; } + // List My Project Roles + // + // Deprecated: [List authorizations](apis/resources/authorization_service_v2/zitadel-authorization-v-2-beta-authorization-service-list-authorizations.api.mdx) and pass the user ID filter with your users ID and the project ID filter to search for your authorizations on a granted and an owned project. + // + // Returns a list of roles for the authenticated user and for the requesting project. rpc ListMyProjectPermissions(ListMyProjectPermissionsRequest) returns (ListMyProjectPermissionsResponse) { option (google.api.http) = { post: "/permissions/me/_search" @@ -919,8 +928,7 @@ service AuthService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "User Authorizations/Grants" - summary: "List My Project Roles"; - description: "Returns a list of roles for the authenticated user and for the requesting project (based on the token)." + deprecated: true; }; } diff --git a/proto/zitadel/authorization/v2beta/authorization.proto b/proto/zitadel/authorization/v2beta/authorization.proto new file mode 100644 index 0000000000..aedd4c8b3c --- /dev/null +++ b/proto/zitadel/authorization/v2beta/authorization.proto @@ -0,0 +1,181 @@ +syntax = "proto3"; + +package zitadel.authorization.v2beta; + +import "protoc-gen-openapiv2/options/annotations.proto"; +import "google/protobuf/timestamp.proto"; +import "validate/validate.proto"; + +import "zitadel/filter/v2beta/filter.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/authorization/v2beta;authorization"; + +message Authorization { + // ID is the unique identifier of the authorization. + string id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629012906488334\""; + } + ]; + + // ID is the unique identifier of the project the user was granted the authorization for. + string project_id = 2; + // Name is the name of the project the user was granted the authorization for. + string project_name = 3; + // OrganizationID is the ID of the organization the project belongs to. + string project_organization_id = 4; + // ID of the granted project, only provided if it is a granted project. + optional string project_grant_id = 5; + // ID of the organization the project is granted to, only provided if it is a granted project. + optional string granted_organization_id = 6; + + // The unique identifier of the organization the authorization belongs to. + string organization_id = 7 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629012906488334\""; + } + ]; + // CreationDate is the timestamp when the authorization was created. + google.protobuf.Timestamp creation_date = 8 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + // ChangeDate is the timestamp when the authorization was last updated. + // In case the authorization was not updated, this field is equal to the creation date. + google.protobuf.Timestamp change_date = 9 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; + // State is the current state of the authorization. + State state = 10; + User user = 11; + // Roles contains the roles the user was granted for the project. + repeated string roles = 12; +} + +enum State { + STATE_UNSPECIFIED = 0; + // An active authorization grants the user access with the roles specified on the project. + STATE_ACTIVE = 1; + // An inactive authorization temporarily deactivates the granted access and roles. + // ZITADEL will not include the specific authorization in any authorization information like an access token. + // But the information can still be accessed using the API. + STATE_INACTIVE = 2; +} + +message User { + // ID represents the ID of the user who was granted the authorization. + string id = 1; + // PreferredLoginName represents the preferred login name of the granted user. + string preferred_login_name = 2; + // DisplayName represents the public display name of the granted user. + string display_name = 3; + // AvatarURL is the URL to the user's public avatar image. + string avatar_url = 4; + // The organization the user belong to. + // This does not have to correspond with the authorizations organization. + string organization_id = 5; +} + +message AuthorizationsSearchFilter { + oneof filter { + option (validate.required) = true; + + // Search for authorizations by their IDs. + zitadel.filter.v2beta.InIDsFilter authorization_ids = 1; + // Search for an organizations authorizations by its ID. + zitadel.filter.v2beta.IDFilter organization_id = 2; + // Search for authorizations by their state. + StateQuery state = 3; + // Search for authorizations by the ID of the user who was granted the authorization. + zitadel.filter.v2beta.IDFilter user_id = 4; + // Search for authorizations by the ID of the organisation the user is part of. + zitadel.filter.v2beta.IDFilter user_organization_id = 5; + // Search for authorizations by the preferred login name of the granted user. + UserPreferredLoginNameQuery user_preferred_login_name = 6; + // Search for authorizations by the public display name of the granted user. + UserDisplayNameQuery user_display_name = 7; + // Search for authorizations by the ID of the project the user was granted the authorization for. + // This will also include authorizations granted for project grants of the same project. + zitadel.filter.v2beta.IDFilter project_id = 8; + // Search for authorizations by the name of the project the user was granted the authorization for. + // This will also include authorizations granted for project grants of the same project. + ProjectNameQuery project_name = 9; + // Search for authorizations by the key of the role the user was granted. + RoleKeyQuery role_key = 10; + // Search for authorizations by the ID of the project grant the user was granted the authorization for. + // This will also include authorizations granted for project grants of the same project. + zitadel.filter.v2beta.IDFilter project_grant_id = 11; + } +} + +message StateQuery { + // Specify the state of the authorization to search for. + State state = 1 [(validate.rules).enum = {defined_only: true, not_in: [0]}]; +} + +message UserPreferredLoginNameQuery { + // Specify the preferred login name of the granted user to search for. + string login_name = 1 [(validate.rules).string = { + min_len: 1 + max_len: 200 + }]; + // Specify the method to search for the preferred login name. Default is EQUAL. + // For example, to search for all authorizations granted to a user with + // a preferred login name containing a specific string, use CONTAINS or CONTAINS_IGNORE_CASE. + zitadel.filter.v2beta.TextFilterMethod method = 2 [(validate.rules).enum.defined_only = true]; +} + +message UserDisplayNameQuery { + // Specify the public display name of the granted user to search for. + string display_name = 1 [(validate.rules).string = { + min_len: 1 + max_len: 200 + }]; + // Specify the method to search for the display name. Default is EQUAL. + // For example, to search for all authorizations granted to a user with + // a display name containing a specific string, use CONTAINS or CONTAINS_IGNORE_CASE. + zitadel.filter.v2beta.TextFilterMethod method = 2 [(validate.rules).enum.defined_only = true]; +} + +message ProjectNameQuery { + // Specify the name of the project the user was granted the authorization for to search for. + // Note that this will also include authorizations granted for project grants of the same project. + string name = 1 [(validate.rules).string = {max_len: 200}]; + // Specify the method to search for the project name. Default is EQUAL. + // For example, to search for all authorizations granted on a project with + // a name containing a specific string, use CONTAINS or CONTAINS_IGNORE_CASE. + zitadel.filter.v2beta.TextFilterMethod method = 2 [(validate.rules).enum.defined_only = true]; +} + +message OrganizationNameQuery { + // Specify the name of the organization the authorization was granted for to search for. + // This can either be the organization the project or the project grant is part of. + string name = 1 [(validate.rules).string = {max_len: 200}]; + // Specify the method to search for the organization name. Default is EQUAL. + // For example, to search for all authorizations with an organization name containing a specific string, + // use CONTAINS or CONTAINS_IGNORE_CASE. + zitadel.filter.v2beta.TextFilterMethod method = 2 [(validate.rules).enum.defined_only = true]; +} + +message RoleKeyQuery { + // Specify the key of the role the user was granted to search for. + string key = 1 [(validate.rules).string = {max_len: 200}]; + // Specify the method to search for the role key. Default is EQUAL. + // For example, to search for all authorizations starting with a specific role key, + // use STARTS_WITH or STARTS_WITH_IGNORE_CASE. + zitadel.filter.v2beta.TextFilterMethod method = 2 [(validate.rules).enum.defined_only = true]; +} + +enum AuthorizationFieldName { + AUTHORIZATION_FIELD_NAME_UNSPECIFIED = 0; + AUTHORIZATION_FIELD_NAME_CREATED_DATE = 1; + AUTHORIZATION_FIELD_NAME_CHANGED_DATE = 2; + AUTHORIZATION_FIELD_NAME_ID = 3; + AUTHORIZATION_FIELD_NAME_USER_ID = 4; + AUTHORIZATION_FIELD_NAME_PROJECT_ID = 5; + AUTHORIZATION_FIELD_NAME_ORGANIZATION_ID = 6; + AUTHORIZATION_FIELD_NAME_USER_ORGANIZATION_ID = 7; +} diff --git a/proto/zitadel/authorization/v2beta/authorization_service.proto b/proto/zitadel/authorization/v2beta/authorization_service.proto new file mode 100644 index 0000000000..5020154883 --- /dev/null +++ b/proto/zitadel/authorization/v2beta/authorization_service.proto @@ -0,0 +1,456 @@ +syntax = "proto3"; + +package zitadel.authorization.v2beta; + +import "protoc-gen-openapiv2/options/annotations.proto"; +import "google/protobuf/timestamp.proto"; +import "validate/validate.proto"; +import "google/api/annotations.proto"; + +import "zitadel/protoc_gen_zitadel/v2/options.proto"; +import "zitadel/authorization/v2beta/authorization.proto"; +import "zitadel/filter/v2beta/filter.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/authorization/v2beta;authorization"; + +// AuthorizationService provides methods to manage authorizations for users within your projects and applications. +// +// For managing permissions and roles for ZITADEL internal resources, like organizations, projects, +// users, etc., please use the InternalPermissionService. +service AuthorizationService { + + // List Authorizations + // + // ListAuthorizations returns all authorizations matching the request and necessary permissions. + // + // Required permissions: + // - "user.grant.read" + // - no permissions required for listing own authorizations + rpc ListAuthorizations(ListAuthorizationsRequest) returns (ListAuthorizationsResponse) { + option (google.api.http) = { + // The only reason why it is used here is to avoid a conflict with the ListUsers endpoint, which already handles POST /v2/users. + post: "/v2beta/authorizations/search" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "A list of all authorizations matching the query"; + }; + }; + responses: { + key: "400"; + value: { + description: "invalid list query"; + }; + }; + }; + } + + // Create Authorization + // + // CreateAuthorization creates a new authorization for a user in an owned or granted project. + // + // Required permissions: + // - "user.grant.write" + rpc CreateAuthorization(CreateAuthorizationRequest) returns (CreateAuthorizationResponse) { + option (google.api.http) = { + post: "/v2beta/authorizations" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "The newly created authorization"; + }; + }; + responses: { + key: "400"; + value: { + description: "invalid create request"; + }; + }; + responses: { + key: "409" + value: { + description: "The authorization already exists."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + }; + }; + } + }; + }; + } + + // Update Authorization + // + // UpdateAuthorization updates the authorization. + // + // Note that any role keys previously granted to the user and not present in the request will be revoked. + // + // Required permissions: + // - "user.grant.write" + rpc UpdateAuthorization(UpdateAuthorizationRequest) returns (UpdateAuthorizationResponse) { + option (google.api.http) = { + patch: "/v2beta/authorizations/{id}" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "OK"; + }; + }; + responses: { + key: "404"; + value: { + description: "Authorization or one of the roles do not exist."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + }; + }; + } + } + }; + } + + // Delete Authorization + // + // DeleteAuthorization deletes the authorization. + // + // In case the authorization is not found, the request will return a successful response as + // the desired state is already achieved. + // You can check the deletion date in the response to verify if the authorization was deleted by the request. + // + // Required permissions: + // - "user.grant.delete" + rpc DeleteAuthorization(DeleteAuthorizationRequest) returns (DeleteAuthorizationResponse) { + option (google.api.http) = { + delete: "/v2beta/authorizations/{id}" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "The authorization was deleted successfully."; + }; + }; + responses: { + key: "404"; + value: { + description: "Authorization not found."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + }; + }; + }; + }; + }; + } + + // Activate Authorization + // + // ActivateAuthorization activates an existing but inactive authorization. + // + // In case the authorization is already active, the request will return a successful response as + // the desired state is already achieved. + // You can check the change date in the response to verify if the authorization was activated by the request. + // + // Required permissions: + // - "user.grant.write" + rpc ActivateAuthorization(ActivateAuthorizationRequest) returns (ActivateAuthorizationResponse) { + option (google.api.http) = { + post: "/v2beta/authorizations/{id}/activate" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "The authorization was activated successfully."; + }; + }; + responses: { + key: "404"; + value: { + description: "Authorization not found."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + }; + }; + }; + }; + }; + } + + // Deactivate Authorization + // + // DeactivateAuthorization deactivates an existing and active authorization. + // + // In case the authorization is already inactive, the request will return a successful response as + // the desired state is already achieved. + // You can check the change date in the response to verify if the authorization was deactivated by the request. + // + // Required permissions: + // - "user.grant.write" + rpc DeactivateAuthorization(DeactivateAuthorizationRequest) returns (DeactivateAuthorizationResponse) { + option (google.api.http) = { + post: "/v2beta/authorizations/{id}/deactivate" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "The authorization was deactivated successfully."; + }; + }; + responses: { + key: "404"; + value: { + description: "Authorization not found."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + }; + }; + }; + }; + }; + } +} + +message ListAuthorizationsRequest { + // Paginate through the results using a limit, offset and sorting. + optional zitadel.filter.v2beta.PaginationRequest pagination = 1; + // The field the result is sorted by. The default is the creation date. Beware that if you change this, your result pagination might be inconsistent. + optional AuthorizationFieldName sorting_column = 2 [ + (validate.rules).enum = {defined_only: true}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"AUTHORIZATION_FIELD_NAME_CREATED_DATE\"" + } + ]; + // Define the criteria to query for. + repeated AuthorizationsSearchFilter filters = 3; +} + +message ListAuthorizationsResponse { + // Details contains the pagination information. + zitadel.filter.v2beta.PaginationResponse pagination = 1; + repeated Authorization authorizations = 2; +} + +message CreateAuthorizationRequest { + // UserID is the ID of the user who should be granted the authorization. + string user_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"163840776835432345\""; + } + ]; + // Project ID is the ID of the project the user should be authorized for. + string project_id = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"163840776835432345\""; + } + ]; + // OrganizationID is the ID of the organization on which the authorization should be created. + // The organization must either own the project or have a grant for the project. + // If omitted, the authorization is created on the projects organization. + optional string organization_id = 3 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + max_length: 200; + example: "\"163840776835432345\""; + } + ]; + // RoleKeys are the keys of the roles the user should be granted. + repeated string role_keys = 4 [ + (validate.rules).repeated = { + unique: true + items: { + string: { + min_len: 1 + max_len: 200 + } + } + }, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "[\"user\",\"admin\"]"; + } + ]; +} + +message CreateAuthorizationResponse { + // ID is the unique identifier of the newly created authorization. + string id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629012906488334\""; + } + ]; + // CreationDate is the timestamp when the authorization was created. + google.protobuf.Timestamp creation_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message UpdateAuthorizationRequest { + // ID is the unique identifier of the authorization. + string id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"163840776835432345\""; + } + ]; + // RoleKeys are the keys of the roles the user should be granted. + // Note that any role keys previously granted to the user and not present in the list will be revoked. + repeated string role_keys = 2 [ + (validate.rules).repeated = { + unique: true + items: { + string: { + min_len: 1 + max_len: 200 + } + } + }, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "[\"user\",\"admin\"]"; + } + ]; +} + +message UpdateAuthorizationResponse { + // ChangeDate is the timestamp when the authorization was last updated. + google.protobuf.Timestamp change_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; +} + +message DeleteAuthorizationRequest { + // ID is the unique identifier of the authorization that should be deleted. + string id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"163840776835432345\""; + } + ]; +} + +message DeleteAuthorizationResponse { + // DeletionDate is the timestamp when the authorization was deleted. + google.protobuf.Timestamp deletion_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; +} + +message ActivateAuthorizationRequest { + // ID is the unique identifier of the authorization that should be activated. + string id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"163840776835432345\""; + } + ]; +} + +message ActivateAuthorizationResponse { + // ChangeDate is the last timestamp when the authorization was changed / activated. + google.protobuf.Timestamp change_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; +} + +message DeactivateAuthorizationRequest { + // ID is the unique identifier of the authorization that should be deactivated. + string id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"163840776835432345\""; + } + ]; +} + +message DeactivateAuthorizationResponse { + // ChangeDate is the last timestamp when the authorization was changed / deactivated. + google.protobuf.Timestamp change_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; +} diff --git a/proto/zitadel/feature/v2/instance.proto b/proto/zitadel/feature/v2/instance.proto index fe8d3f7a39..f3467f723d 100644 --- a/proto/zitadel/feature/v2/instance.proto +++ b/proto/zitadel/feature/v2/instance.proto @@ -11,26 +11,14 @@ import "zitadel/feature/v2/feature.proto"; option go_package = "github.com/zitadel/zitadel/pkg/grpc/feature/v2;feature"; message SetInstanceFeaturesRequest{ - reserved 6; - reserved "actions"; + reserved 2, 3, 6, 8; + reserved "oidc_trigger_introspection_projections", "oidc_legacy_introspection", "actions", "web_key"; optional bool login_default_org = 1 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "true"; description: "The login UI will use the settings of the default org (and not from the instance) if no organization context is set"; } ]; - optional bool oidc_trigger_introspection_projections = 2 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "true"; - description: "Enable projection triggers during an introspection request. This can act as workaround if there are noticeable consistency issues in the introspection response but can have an impact on performance. We are planning to remove triggers for introspection requests in the future. Please raise an issue if you needed to enable this feature."; - } - ]; - optional bool oidc_legacy_introspection = 3 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "true"; - description: "We have recently refactored the introspection endpoint for performance reasons. This feature can be used to rollback to the legacy implementation if unexpected bugs arise. Please raise an issue if you needed to enable this feature."; - } - ]; optional bool user_schema = 4 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -55,13 +43,6 @@ message SetInstanceFeaturesRequest{ } ]; - optional bool web_key = 8 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "true"; - description: "Enable the webkey/v3alpha API. The first time this feature is enabled, web keys are generated and activated."; - } - ]; - optional bool debug_oidc_parent_error = 9 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "true"; @@ -131,8 +112,8 @@ message GetInstanceFeaturesRequest { } message GetInstanceFeaturesResponse { - reserved 7; - reserved "actions"; + reserved 3, 4, 7, 9; + reserved "oidc_trigger_introspection_projections", "oidc_legacy_introspection", "actions", "web_key"; zitadel.object.v2.Details details = 1; FeatureFlag login_default_org = 2 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -141,20 +122,6 @@ message GetInstanceFeaturesResponse { } ]; - FeatureFlag oidc_trigger_introspection_projections = 3 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "true"; - description: "Enable projection triggers during an introspection request. This can act as workaround if there are noticeable consistency issues in the introspection response but can have an impact on performance. We are planning to remove triggers for introspection requests in the future. Please raise an issue if you needed to enable this feature."; - } - ]; - - FeatureFlag oidc_legacy_introspection = 4 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "true"; - description: "We have recently refactored the introspection endpoint for performance reasons. This feature can be used to rollback to the legacy implementation if unexpected bugs arise. Please raise an issue if you needed to enable this feature."; - } - ]; - FeatureFlag user_schema = 5 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "true"; @@ -176,13 +143,6 @@ message GetInstanceFeaturesResponse { } ]; - FeatureFlag web_key = 9 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "true"; - description: "Enable the webkey/v3alpha API. The first time this feature is enabled, web keys are generated and activated."; - } - ]; - FeatureFlag debug_oidc_parent_error = 10 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "true"; diff --git a/proto/zitadel/feature/v2/system.proto b/proto/zitadel/feature/v2/system.proto index d222e2a90c..d3fbe6bccb 100644 --- a/proto/zitadel/feature/v2/system.proto +++ b/proto/zitadel/feature/v2/system.proto @@ -11,8 +11,8 @@ import "zitadel/feature/v2/feature.proto"; option go_package = "github.com/zitadel/zitadel/pkg/grpc/feature/v2;feature"; message SetSystemFeaturesRequest{ - reserved 6; - reserved "actions"; + reserved 2, 3, 6; + reserved "oidc_trigger_introspection_projections", "oidc_legacy_introspection", "actions"; optional bool login_default_org = 1 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "true"; @@ -20,20 +20,6 @@ message SetSystemFeaturesRequest{ } ]; - optional bool oidc_trigger_introspection_projections = 2 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "true"; - description: "Enable projection triggers during an introspection request. This can act as workaround if there are noticeable consistency issues in the introspection response but can have an impact on performance. We are planning to remove triggers for introspection requests in the future. Please raise an issue if you needed to enable this feature."; - } - ]; - - optional bool oidc_legacy_introspection = 3 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "true"; - description: "We have recently refactored the introspection endpoint for performance reasons. This feature can be used to rollback to the legacy implementation if unexpected bugs arise. Please raise an issue if you needed to enable this feature."; - } - ]; - optional bool user_schema = 4 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "true"; @@ -105,8 +91,8 @@ message ResetSystemFeaturesResponse { message GetSystemFeaturesRequest {} message GetSystemFeaturesResponse { - reserved 7; - reserved "actions"; + reserved 3, 4, 7; + reserved "oidc_trigger_introspection_projections", "oidc_legacy_introspection", "actions"; zitadel.object.v2.Details details = 1; FeatureFlag login_default_org = 2 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -115,20 +101,6 @@ message GetSystemFeaturesResponse { } ]; - FeatureFlag oidc_trigger_introspection_projections = 3 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "true"; - description: "Enable projection triggers during an introspection request. This can act as workaround if there are noticeable consistency issues in the introspection response but can have an impact on performance. We are planning to remove triggers for introspection requests in the future. Please raise an issue if you needed to enable this feature."; - } - ]; - - FeatureFlag oidc_legacy_introspection = 4 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "true"; - description: "We have recently refactored the introspection endpoint for performance reasons. This feature can be used to rollback to the legacy implementation if unexpected bugs arise. Please raise an issue if you needed to enable this feature."; - } - ]; - FeatureFlag user_schema = 5 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "true"; diff --git a/proto/zitadel/feature/v2beta/instance.proto b/proto/zitadel/feature/v2beta/instance.proto index 7717dd7556..ac7a6c9286 100644 --- a/proto/zitadel/feature/v2beta/instance.proto +++ b/proto/zitadel/feature/v2beta/instance.proto @@ -11,26 +11,14 @@ import "zitadel/feature/v2beta/feature.proto"; option go_package = "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta;feature"; message SetInstanceFeaturesRequest{ - reserved 6; - reserved "actions"; + reserved 2, 3, 6, 8; + reserved "oidc_trigger_introspection_projections", "oidc_legacy_introspection", "actions", "web_key"; optional bool login_default_org = 1 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "true"; description: "The login UI will use the settings of the default org (and not from the instance) if no organization context is set"; } ]; - optional bool oidc_trigger_introspection_projections = 2 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "true"; - description: "Enable projection triggers during an introspection request. This can act as workaround if there are noticeable consistency issues in the introspection response but can have an impact on performance. We are planning to remove triggers for introspection requests in the future. Please raise an issue if you needed to enable this feature."; - } - ]; - optional bool oidc_legacy_introspection = 3 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "true"; - description: "We have recently refactored the introspection endpoint for performance reasons. This feature can be used to rollback to the legacy implementation if unexpected bugs arise. Please raise an issue if you needed to enable this feature."; - } - ]; optional bool user_schema = 4 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -55,13 +43,6 @@ message SetInstanceFeaturesRequest{ } ]; - optional bool web_key = 8 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "true"; - description: "Enable the webkey/v3alpha API. The first time this feature is enabled, web keys are generated and activated."; - } - ]; - optional bool debug_oidc_parent_error = 9 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "true"; @@ -97,8 +78,8 @@ message GetInstanceFeaturesRequest { } message GetInstanceFeaturesResponse { - reserved 7; - reserved "actions"; + reserved 3, 4, 7, 9; + reserved "oidc_trigger_introspection_projections", "oidc_legacy_introspection", "actions", "web_key"; zitadel.object.v2beta.Details details = 1; FeatureFlag login_default_org = 2 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -107,20 +88,6 @@ message GetInstanceFeaturesResponse { } ]; - FeatureFlag oidc_trigger_introspection_projections = 3 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "true"; - description: "Enable projection triggers during an introspection request. This can act as workaround if there are noticeable consistency issues in the introspection response but can have an impact on performance. We are planning to remove triggers for introspection requests in the future. Please raise an issue if you needed to enable this feature."; - } - ]; - - FeatureFlag oidc_legacy_introspection = 4 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "true"; - description: "We have recently refactored the introspection endpoint for performance reasons. This feature can be used to rollback to the legacy implementation if unexpected bugs arise. Please raise an issue if you needed to enable this feature."; - } - ]; - FeatureFlag user_schema = 5 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "true"; @@ -142,13 +109,6 @@ message GetInstanceFeaturesResponse { } ]; - FeatureFlag web_key = 9 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "true"; - description: "Enable the webkey/v3alpha API. The first time this feature is enabled, web keys are generated and activated."; - } - ]; - FeatureFlag debug_oidc_parent_error = 10 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "true"; diff --git a/proto/zitadel/feature/v2beta/system.proto b/proto/zitadel/feature/v2beta/system.proto index 624e68ec79..ae500eb87b 100644 --- a/proto/zitadel/feature/v2beta/system.proto +++ b/proto/zitadel/feature/v2beta/system.proto @@ -11,8 +11,8 @@ import "zitadel/feature/v2beta/feature.proto"; option go_package = "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta;feature"; message SetSystemFeaturesRequest{ - reserved 6; - reserved "actions"; + reserved 2, 3, 6; + reserved "oidc_trigger_introspection_projections", "oidc_legacy_introspection", "actions"; optional bool login_default_org = 1 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "true"; @@ -20,20 +20,6 @@ message SetSystemFeaturesRequest{ } ]; - optional bool oidc_trigger_introspection_projections = 2 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "true"; - description: "Enable projection triggers during an introspection request. This can act as workaround if there are noticeable consistency issues in the introspection response but can have an impact on performance. We are planning to remove triggers for introspection requests in the future. Please raise an issue if you needed to enable this feature."; - } - ]; - - optional bool oidc_legacy_introspection = 3 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "true"; - description: "We have recently refactored the introspection endpoint for performance reasons. This feature can be used to rollback to the legacy implementation if unexpected bugs arise. Please raise an issue if you needed to enable this feature."; - } - ]; - optional bool user_schema = 4 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "true"; @@ -78,8 +64,8 @@ message ResetSystemFeaturesResponse { message GetSystemFeaturesRequest {} message GetSystemFeaturesResponse { - reserved 7; - reserved "actions"; + reserved 3, 4, 7; + reserved "oidc_trigger_introspection_projections", "oidc_legacy_introspection", "actions"; zitadel.object.v2beta.Details details = 1; FeatureFlag login_default_org = 2 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -88,20 +74,6 @@ message GetSystemFeaturesResponse { } ]; - FeatureFlag oidc_trigger_introspection_projections = 3 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "true"; - description: "Enable projection triggers during an introspection request. This can act as workaround if there are noticeable consistency issues in the introspection response but can have an impact on performance. We are planning to remove triggers for introspection requests in the future. Please raise an issue if you needed to enable this feature."; - } - ]; - - FeatureFlag oidc_legacy_introspection = 4 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - example: "true"; - description: "We have recently refactored the introspection endpoint for performance reasons. This feature can be used to rollback to the legacy implementation if unexpected bugs arise. Please raise an issue if you needed to enable this feature."; - } - ]; - FeatureFlag user_schema = 5 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "true"; diff --git a/proto/zitadel/filter/v2/filter.proto b/proto/zitadel/filter/v2/filter.proto new file mode 100644 index 0000000000..3817324d31 --- /dev/null +++ b/proto/zitadel/filter/v2/filter.proto @@ -0,0 +1,96 @@ +syntax = "proto3"; + +package zitadel.filter.v2; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/filter/v2;filter"; + +import "google/protobuf/timestamp.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; + +enum TextFilterMethod { + TEXT_FILTER_METHOD_EQUALS = 0; + TEXT_FILTER_METHOD_EQUALS_IGNORE_CASE = 1; + TEXT_FILTER_METHOD_STARTS_WITH = 2; + TEXT_FILTER_METHOD_STARTS_WITH_IGNORE_CASE = 3; + TEXT_FILTER_METHOD_CONTAINS = 4; + TEXT_FILTER_METHOD_CONTAINS_IGNORE_CASE = 5; + TEXT_FILTER_METHOD_ENDS_WITH = 6; + TEXT_FILTER_METHOD_ENDS_WITH_IGNORE_CASE = 7; +} + +enum ListFilterMethod { + LIST_FILTER_METHOD_IN = 0; +} + +enum TimestampFilterMethod { + TIMESTAMP_FILTER_METHOD_EQUALS = 0; + TIMESTAMP_FILTER_METHOD_AFTER = 1; + TIMESTAMP_FILTER_METHOD_AFTER_OR_EQUALS = 2; + TIMESTAMP_FILTER_METHOD_BEFORE = 3; + TIMESTAMP_FILTER_METHOD_BEFORE_OR_EQUALS = 4; +} + +message PaginationRequest { + // Starting point for retrieval, in combination of offset used to query a set list of objects. + uint64 offset = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "0"; + } + ]; + // limit is the maximum amount of objects returned. The default is set to 100 + // with a maximum of 1000 in the runtime configuration. + // If the limit exceeds the maximum configured ZITADEL will throw an error. + // If no limit is present the default is taken. + uint32 limit = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "10"; + } + ]; + // Asc is the sorting order. If true the list is sorted ascending, if false + // the list is sorted descending. The default is descending. + bool asc = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "false"; + } + ]; +} + +message PaginationResponse { + // Absolute number of objects matching the query, regardless of applied limit. + uint64 total_result = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "100"; + } + ]; + // Applied limit from query, defines maximum amount of objects per request, to compare if all objects are returned. + uint64 applied_limit = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "100"; + } + ]; +} + +message IDFilter { + // Only return resources that belong to this id. + string id = 1 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + max_length: 200; + example: "\"69629023906488337\""; + } + ]; +} + +message TimestampFilter { + // Filter resources by timestamp. + google.protobuf.Timestamp timestamp = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + // Defines the condition (e.g., equals, before, after) that the timestamp of the retrieved resources should match. + TimestampFilterMethod method = 2 [ + (validate.rules).enum.defined_only = true + ]; +} diff --git a/proto/zitadel/filter/v2beta/filter.proto b/proto/zitadel/filter/v2beta/filter.proto index 6aae583cde..098808fed0 100644 --- a/proto/zitadel/filter/v2beta/filter.proto +++ b/proto/zitadel/filter/v2beta/filter.proto @@ -6,6 +6,7 @@ option go_package = "github.com/zitadel/zitadel/pkg/grpc/filter/v2beta;filter"; import "google/protobuf/timestamp.proto"; import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; enum TextFilterMethod { TEXT_FILTER_METHOD_EQUALS = 0; @@ -56,4 +57,48 @@ message PaginationResponse { example: "\"100\""; } ]; -} \ No newline at end of file +} + +message IDFilter { + // Only return resources that belong to this id. + string id = 1 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + max_length: 200; + example: "\"69629023906488337\""; + } + ]; +} + +message TimestampFilter { + // Filter resources by timestamp. + google.protobuf.Timestamp timestamp = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + // Defines the condition (e.g., equals, before, after) that the timestamp of the retrieved resources should match. + TimestampFilterMethod method = 2 [ + (validate.rules).enum.defined_only = true + ]; +} + +message InIDsFilter { + // Defines the ids to query for. + repeated string ids = 1 [ + (validate.rules).repeated = { + unique: true + items: { + string: { + min_len: 1 + max_len: 200 + } + } + }, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "[\"69629023906488334\",\"69622366012355662\"]"; + } + ]; +} diff --git a/proto/zitadel/instance/v2beta/instance.proto b/proto/zitadel/instance/v2beta/instance.proto new file mode 100644 index 0000000000..21f6148490 --- /dev/null +++ b/proto/zitadel/instance/v2beta/instance.proto @@ -0,0 +1,192 @@ +syntax = "proto3"; + +import "protoc-gen-openapiv2/options/annotations.proto"; +import "zitadel/object/v2/object.proto"; +import "validate/validate.proto"; +import "google/protobuf/timestamp.proto"; + +package zitadel.instance.v2beta; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/instance/v2beta;instance"; + +message Instance { + string id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\"" + } + ]; + + // change_date is the timestamp when the object was changed + // + // on read: the timestamp of the last event reduced by the projection + // + // on manipulation: the timestamp of the event(s) added by the manipulation + google.protobuf.Timestamp change_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; + + google.protobuf.Timestamp creation_date = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; + State state = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "current state of the instance"; + } + ]; + string name = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"ZITADEL\""; + } + ]; + string version = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"1.0.0\""; + } + ]; + repeated Domain domains = 7; +} + +enum State { + STATE_UNSPECIFIED = 0; + STATE_CREATING = 1; + STATE_RUNNING = 2; + STATE_STOPPING = 3; + STATE_STOPPED = 4; +} + +message Domain { + string instance_id = 1; + + google.protobuf.Timestamp creation_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; + + string domain = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"zitadel.com\"" + } + ]; + bool primary = 4; + bool generated = 5; +} + +enum FieldName { + FIELD_NAME_UNSPECIFIED = 0; + FIELD_NAME_ID = 1; + FIELD_NAME_NAME = 2; + FIELD_NAME_CREATION_DATE = 3; +} + +message Query { + oneof query { + option (validate.required) = true; + + IdsQuery id_query = 1; + DomainsQuery domain_query = 2; + } +} + +message IdsQuery { + repeated string ids = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Instance ID"; + example: "[\"4820840938402429\",\"4820840938402422\"]" + } + ]; +} + +message DomainsQuery { + repeated string domains = 1 [ + (validate.rules).repeated = {max_items: 20, items: {string: {min_len: 1, max_len: 100}}}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + max_items: 20; + example: "[\"my-instace.zitadel.cloud\", \"auth.custom.com\"]"; + description: "Return the instances that have the requested domains"; + } + ]; +} +message DomainSearchQuery { + oneof query { + option (validate.required) = true; + + DomainQuery domain_query = 1; + DomainGeneratedQuery generated_query = 2; + DomainPrimaryQuery primary_query = 3; + } +} + +message DomainQuery { + string domain = 1 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + max_length: 200; + example: "\"zitadel.com\""; + } + ]; + zitadel.object.v2.TextQueryMethod method = 2 [ + (validate.rules).enum.defined_only = true, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Defines which text equality method is used"; + } + ]; +} + +message DomainGeneratedQuery { + bool generated = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Generated domains"; + } + ]; +} + +message DomainPrimaryQuery { + bool primary = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Primary domains"; + } + ]; +} + +enum DomainFieldName { + DOMAIN_FIELD_NAME_UNSPECIFIED = 0; + DOMAIN_FIELD_NAME_DOMAIN = 1; + DOMAIN_FIELD_NAME_PRIMARY = 2; + DOMAIN_FIELD_NAME_GENERATED = 3; + DOMAIN_FIELD_NAME_CREATION_DATE = 4; +} + +message TrustedDomain { + string instance_id = 1; + + google.protobuf.Timestamp creation_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; + + string domain = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"zitadel.com\"" + } + ]; +} + +message TrustedDomainSearchQuery { + oneof query { + option (validate.required) = true; + + DomainQuery domain_query = 1; + } +} + +enum TrustedDomainFieldName { + TRUSTED_DOMAIN_FIELD_NAME_UNSPECIFIED = 0; + TRUSTED_DOMAIN_FIELD_NAME_DOMAIN = 1; + TRUSTED_DOMAIN_FIELD_NAME_CREATION_DATE = 2; +} diff --git a/proto/zitadel/instance/v2beta/instance_service.proto b/proto/zitadel/instance/v2beta/instance_service.proto new file mode 100644 index 0000000000..0a5de00286 --- /dev/null +++ b/proto/zitadel/instance/v2beta/instance_service.proto @@ -0,0 +1,648 @@ +syntax = "proto3"; + +package zitadel.instance.v2beta; + +import "validate/validate.proto"; +import "zitadel/object/v2/object.proto"; +import "zitadel/instance/v2beta/instance.proto"; +import "zitadel/filter/v2beta/filter.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "google/protobuf/empty.proto"; +import "google/api/annotations.proto"; +import "google/api/field_behavior.proto"; +import "google/protobuf/timestamp.proto"; +import "zitadel/protoc_gen_zitadel/v2/options.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/instance/v2beta;instance"; + +option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { + info: { + title: "Instance Service"; + version: "2.0-beta"; + description: "This API is intended to manage instances in ZITADEL."; + contact:{ + name: "ZITADEL" + url: "https://zitadel.com" + email: "hi@zitadel.com" + } + license: { + name: "AGPL-3.0-only", + url: "https://github.com/zitadel/zitadel/blob/main/LICENSING.md"; + }; + }; + schemes: HTTPS; + schemes: HTTP; + + consumes: "application/json"; + consumes: "application/grpc"; + + produces: "application/json"; + produces: "application/grpc"; + + consumes: "application/grpc-web+proto"; + produces: "application/grpc-web+proto"; + + host: "$CUSTOM-DOMAIN"; + base_path: "/"; + + external_docs: { + description: "Detailed information about ZITADEL", + url: "https://zitadel.com/docs" + } + security_definitions: { + security: { + key: "OAuth2"; + value: { + type: TYPE_OAUTH2; + flow: FLOW_ACCESS_CODE; + authorization_url: "$CUSTOM-DOMAIN/oauth/v2/authorize"; + token_url: "$CUSTOM-DOMAIN/oauth/v2/token"; + scopes: { + scope: { + key: "openid"; + value: "openid"; + } + scope: { + key: "urn:zitadel:iam:org:project:id:zitadel:aud"; + value: "urn:zitadel:iam:org:project:id:zitadel:aud"; + } + } + } + } + } + security: { + security_requirement: { + key: "OAuth2"; + value: { + scope: "openid"; + scope: "urn:zitadel:iam:org:project:id:zitadel:aud"; + } + } + } + responses: { + key: "403"; + value: { + description: "Returned when the user does not have permission to access the resource."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + } + } + } + } + responses: { + key: "404"; + value: { + description: "Returned when the resource does not exist."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + } + } + } + } +}; + +// Service to manage instances and their domains. +// The service provides methods to create, update, delete and list instances and their domains. +service InstanceService { + + // Delete Instance + // + // Deletes an instance with the given ID. + // + // Required permissions: + // - `system.instance.delete` + rpc DeleteInstance(DeleteInstanceRequest) returns (DeleteInstanceResponse) { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "The deleted instance."; + } + }; + }; + + option (google.api.http) = { + delete: "/v2beta/instances/{instance_id}" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "system.instance.delete" + } + }; + } + + // Get Instance + // + // Returns the instance in the current context. + // + // The instace_id in the input message will be used in the future. + // + // Required permissions: + // - `iam.read` + rpc GetInstance(GetInstanceRequest) returns (GetInstanceResponse) { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "The instance of the context."; + } + }; + }; + + option (google.api.http) = { + get: "/v2beta/instances/{instance_id}" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "iam.read" + } + }; + } + + // Update Instance + // + // Updates instance in context with the given name. + // + // The instance_id in the input message will be used in the future. + // + // Required permissions: + // - `iam.write` + rpc UpdateInstance(UpdateInstanceRequest) returns (UpdateInstanceResponse) { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "The instance was successfully updated."; + } + }; + }; + + option (google.api.http) = { + put: "/v2beta/instances/{instance_id}" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "iam.write" + } + }; + } + + // List Instances + // + // Lists instances matching the given query. + // The query can be used to filter either by instance ID or domain. + // The request is paginated and returns 100 results by default. + // + // Required permissions: + // - `system.instance.read` + rpc ListInstances(ListInstancesRequest) returns (ListInstancesResponse) { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "The list of instances."; + } + }; + }; + + option (google.api.http) = { + post: "/v2beta/instances/search" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "system.instance.read" + } + }; + } + + // Add Custom Domain + // + // Adds a custom domain to the instance in context. + // + // The instance_id in the input message will be used in the future + // + // Required permissions: + // - `system.domain.write` + rpc AddCustomDomain(AddCustomDomainRequest) returns (AddCustomDomainResponse) { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "The added custom domain."; + } + }; + }; + + option (google.api.http) = { + post: "/v2beta/instances/{instance_id}/custom-domains" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "system.domain.write" + } + }; + } + + // Remove Custom Domain + // + // Removes a custom domain from the instance. + // + // The instance_id in the input message will be used in the future. + // + // Required permissions: + // - `system.domain.write` + rpc RemoveCustomDomain(RemoveCustomDomainRequest) returns (RemoveCustomDomainResponse) { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "The removed custom domain."; + } + }; + }; + + option (google.api.http) = { + delete: "/v2beta/instances/{instance_id}/custom-domains/{domain}" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "system.domain.write" + } + }; + } + + // List Custom Domains + // + // Lists custom domains of the instance. + // + // The instance_id in the input message will be used in the future. + // + // Required permissions: + // - `iam.read` + rpc ListCustomDomains(ListCustomDomainsRequest) returns (ListCustomDomainsResponse) { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "The list of custom domains."; + } + }; + }; + + option (google.api.http) = { + post: "/v2beta/instances/{instance_id}/custom-domains/search" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "iam.read" + } + }; + } + + // Add Trusted Domain + // + // Adds a trusted domain to the instance. + // + // The instance_id in the input message will be used in the future. + // + // Required permissions: + // - `iam.write` + rpc AddTrustedDomain(AddTrustedDomainRequest) returns (AddTrustedDomainResponse) { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "The added trusted domain."; + } + }; + }; + + option (google.api.http) = { + post: "/v2beta/instances/{instance_id}/trusted-domains" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "iam.write" + } + }; + } + + // Remove Trusted Domain + // + // Removes a trusted domain from the instance. + // + // The instance_id in the input message will be used in the future. + // + // Required permissions: + // - `iam.write` + rpc RemoveTrustedDomain(RemoveTrustedDomainRequest) returns (RemoveTrustedDomainResponse) { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "The removed trusted domain."; + } + }; + }; + + option (google.api.http) = { + delete: "/v2beta/instances/{instance_id}/trusted-domains/{domain}" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "iam.write" + } + }; + } + + + // List Trusted Domains + // + // Lists trusted domains of the instance. + // + // The instance_id in the input message will be used in the future. + // + // Required permissions: + // - `iam.read` + rpc ListTrustedDomains(ListTrustedDomainsRequest) returns (ListTrustedDomainsResponse) { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "The list of trusted domains."; + } + }; + }; + + option (google.api.http) = { + post: "/v2beta/instances/{instance_id}/trusted-domains/search" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "iam.read" + } + }; + } +} + +message DeleteInstanceRequest { + string instance_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"222430354126975533\""; + } + ]; +} + +message DeleteInstanceResponse { + google.protobuf.Timestamp deletion_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message GetInstanceRequest { + string instance_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"222430354126975533\""; + } + ]; +} + +message GetInstanceResponse { + zitadel.instance.v2beta.Instance instance = 1; +} + +message UpdateInstanceRequest { + // used only to identify the instance to change. + string instance_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"222430354126975533\""; + } + ]; + string instance_name = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + description: "\"name of the instance to update\""; + example: "\"my instance\""; + } + ]; +} + +message UpdateInstanceResponse { + google.protobuf.Timestamp change_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message ListInstancesRequest { + // Criterias the client is looking for. + repeated Query queries = 1; + + // Pagination and sorting. + zitadel.filter.v2beta.PaginationRequest pagination = 2; + + // The field the result is sorted by. + optional FieldName sorting_column = 3; +} + +message ListInstancesResponse { + // The list of instances. + repeated Instance instances = 1; + + // Contains the total number of instances matching the query and the applied limit. + zitadel.filter.v2beta.PaginationResponse pagination = 2; +} + +message AddCustomDomainRequest { + string instance_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"222430354126975533\""; + } + ]; + string domain = 2 [ + (validate.rules).string = {min_len: 1, max_len: 253}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 253; + } + ]; +} + +message AddCustomDomainResponse { + google.protobuf.Timestamp creation_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message RemoveCustomDomainRequest { + string instance_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"222430354126975533\""; + } + ]; + string domain = 2 [ + (validate.rules).string = {min_len: 1, max_len: 253}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 253; + } + ]; +} + +message RemoveCustomDomainResponse { + google.protobuf.Timestamp deletion_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message ListCustomDomainsRequest { + string instance_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"222430354126975533\""; + } + ]; + + // Pagination and sorting. + zitadel.filter.v2beta.PaginationRequest pagination = 2; + + // The field the result is sorted by. + DomainFieldName sorting_column = 3; + + // Criterias the client is looking for. + repeated DomainSearchQuery queries = 4; +} + +message ListCustomDomainsResponse { + repeated Domain domains = 1; + + // Contains the total number of domains matching the query and the applied limit. + zitadel.filter.v2beta.PaginationResponse pagination = 2; +} + +message AddTrustedDomainRequest { + string instance_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"222430354126975533\""; + } + ]; + string domain = 2 [ + (validate.rules).string = {min_len: 1, max_len: 253}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"login.example.com\""; + min_length: 1; + max_length: 253; + } + ]; +} + +message AddTrustedDomainResponse { + google.protobuf.Timestamp creation_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message RemoveTrustedDomainRequest { + string instance_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"222430354126975533\""; + } + ]; + string domain = 2 [ + (validate.rules).string = {min_len: 1, max_len: 253}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"login.example.com\""; + min_length: 1; + max_length: 253; + } + ]; +} + +message RemoveTrustedDomainResponse { + google.protobuf.Timestamp deletion_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message ListTrustedDomainsRequest { + string instance_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"222430354126975533\""; + } + ]; + + // Pagination and sorting. + zitadel.filter.v2beta.PaginationRequest pagination = 2; + + // The field the result is sorted by. + TrustedDomainFieldName sorting_column = 3; + + // Criterias the client is looking for. + repeated TrustedDomainSearchQuery queries = 4; +} + +message ListTrustedDomainsResponse { + repeated TrustedDomain trusted_domain = 1; + + // Contains the total number of domains matching the query and the applied limit. + zitadel.filter.v2beta.PaginationResponse pagination = 2; +} diff --git a/proto/zitadel/internal_permission/v2beta/internal_permission_service.proto b/proto/zitadel/internal_permission/v2beta/internal_permission_service.proto new file mode 100644 index 0000000000..3a27b89a4f --- /dev/null +++ b/proto/zitadel/internal_permission/v2beta/internal_permission_service.proto @@ -0,0 +1,384 @@ +syntax = "proto3"; + +package zitadel.internal_permission.v2beta; + +import "google/api/annotations.proto"; +import "google/api/field_behavior.proto"; +import "google/protobuf/duration.proto"; +import "google/protobuf/struct.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; + +import "zitadel/protoc_gen_zitadel/v2/options.proto"; + +import "google/protobuf/timestamp.proto"; +import "zitadel/filter/v2beta/filter.proto"; +import "zitadel/internal_permission/v2beta/query.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/internal_permission/v2beta;internal_permission"; + +option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { + info: { + title: "Internal Permission Service"; + version: "2.0-beta"; + description: "This API is intended to manage internal permissions in ZITADEL. This service is in beta state. It can AND will continue breaking until a stable version is released."; + contact:{ + name: "ZITADEL" + url: "https://zitadel.com" + email: "hi@zitadel.com" + } + license: { + name: "Apache 2.0", + url: "https://github.com/zitadel/zitadel/blob/main/LICENSING.md"; + }; + }; + schemes: HTTPS; + schemes: HTTP; + + consumes: "application/json"; + consumes: "application/grpc"; + + produces: "application/json"; + produces: "application/grpc"; + + consumes: "application/grpc-web+proto"; + produces: "application/grpc-web+proto"; + + host: "$CUSTOM-DOMAIN"; + base_path: "/"; + + external_docs: { + description: "Detailed information about ZITADEL", + url: "https://zitadel.com/docs" + } + security_definitions: { + security: { + key: "OAuth2"; + value: { + type: TYPE_OAUTH2; + flow: FLOW_ACCESS_CODE; + authorization_url: "$CUSTOM-DOMAIN/oauth/v2/authorize"; + token_url: "$CUSTOM-DOMAIN/oauth/v2/token"; + scopes: { + scope: { + key: "openid"; + value: "openid"; + } + scope: { + key: "urn:zitadel:iam:org:project:id:zitadel:aud"; + value: "urn:zitadel:iam:org:project:id:zitadel:aud"; + } + } + } + } + } + security: { + security_requirement: { + key: "OAuth2"; + value: { + scope: "openid"; + scope: "urn:zitadel:iam:org:project:id:zitadel:aud"; + } + } + } + responses: { + key: "403"; + value: { + description: "Returned when the user does not have permission to access the resource."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + } + } + } + } + responses: { + key: "404"; + value: { + description: "Returned when the resource does not exist."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + } + } + } + } +}; + + +// InternalPermissionService provides methods to manage permissions for resource +// and their management in ZITADEL itself. +// +// If you want to manage permissions and roles within your project or application, +// please use the AuthorizationsService. +service InternalPermissionService { + // ListAdministrators returns all administrators and its roles matching the request and necessary permissions. + // + // Required permissions depend on the resource type: + // - "iam.member.read" for instance administrators + // - "org.member.read" for organization administrators + // - "project.member.read" for project administrators + // - "project.grant.member.read" for project grant administrators + // - no permissions required for listing own administrator roles + rpc ListAdministrators(ListAdministratorsRequest) returns (ListAdministratorsResponse) { + option (google.api.http) = { + post: "/v2beta/administrators/search", + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "A list of all administrators matching the query"; + }; + }; + responses: { + key: "400"; + value: { + description: "invalid list query"; + }; + }; + }; + } + + // CreateAdministrator grants a administrator role to a user for a specific resource. + // + // Note that the roles are specific to the resource type. + // This means that if you want to grant a user the administrator role for an organization and a project, + // you need to create two administrator roles. + // + // Required permissions depend on the resource type: + // - "iam.member.write" for instance administrators + // - "org.member.write" for organization administrators + // - "project.member.write" for project administrators + // - "project.grant.member.write" for project grant administrators + rpc CreateAdministrator(CreateAdministratorRequest) returns (CreateAdministratorResponse) { + option (google.api.http) = { + post: "/v2beta/administrators" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "Administrator created successfully"; + }; + }; + responses: { + key: "409" + value: { + description: "The administrator to create already exists."; + } + }; + }; + } + + // UpdateAdministrator updates the specific administrator role. + // + // Note that any role previously granted to the user and not present in the request will be revoked. + // + // Required permissions depend on the resource type: + // - "iam.member.write" for instance administrators + // - "org.member.write" for organization administrators + // - "project.member.write" for project administrators + // - "project.grant.member.write" for project grant administrators + rpc UpdateAdministrator(UpdateAdministratorRequest) returns (UpdateAdministratorResponse) { + option (google.api.http) = { + post: "/v2beta/administrators/{user_id}" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "Administrator successfully updated or left unchanged"; + }; + }; + responses: { + key: "404" + value: { + description: "The administrator to update does not exist."; + } + }; + }; + } + + // DeleteAdministrator revokes a administrator role from a user. + // + // In case the administrator role is not found, the request will return a successful response as + // the desired state is already achieved. + // You can check the deletion date in the response to verify if the administrator role was deleted during the request. + // + // Required permissions depend on the resource type: + // - "iam.member.delete" for instance administrators + // - "org.member.delete" for organization administrators + // - "project.member.delete" for project administrators + // - "project.grant.member.delete" for project grant administrators + rpc DeleteAdministrator(DeleteAdministratorRequest) returns (DeleteAdministratorResponse) { + option (google.api.http) = { + delete: "/v2beta/administrators/{user_id}" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "Administrator deleted successfully"; + }; + }; + }; + } +} + +message ListAdministratorsRequest { + // List limitations and ordering. + optional zitadel.filter.v2beta.PaginationRequest pagination = 1; + // The field the result is sorted by. The default is the creation date. Beware that if you change this, your result pagination might be inconsistent. + optional AdministratorFieldName sorting_column = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"ADMINISTRATOR_FIELD_NAME_CREATION_DATE\"" + } + ]; + // Filter the administrator roles to be returned. + repeated AdministratorSearchFilter filters = 3; +} + +message ListAdministratorsResponse { + zitadel.filter.v2beta.PaginationResponse pagination = 1; + repeated Administrator administrators = 2; +} + +message GetAdministratorRequest { + // ID is the unique identifier of the administrator. + string id = 1 [(validate.rules).string = { + min_len: 1 + max_len: 200 + }]; +} + +message GetAdministratorResponse { + Administrator administrator = 1; +} + +message CreateAdministratorRequest { + // UserID is the ID of the user who should be granted the administrator role. + string user_id = 1 [(validate.rules).string = { + min_len: 1 + max_len: 200 + }]; + // Resource is the type of the resource the administrator roles should be granted for. + ResourceType resource = 2; + + // Roles are the roles that should be granted to the user for the specified resource. + // Note that roles are currently specific to the resource type. + // This means that if you want to grant a user the administrator role for an organization and a project, + // you need to create two administrator roles. + repeated string roles = 3 [(validate.rules).repeated = { + unique: true + items: { + string: { + min_len: 1 + max_len: 200 + } + } + }]; +} + +message ResourceType { + message ProjectGrant { + // ProjectID is required to grant administrator privileges for a specific project. + string project_id = 1; + // ProjectGrantID is required to grant administrator privileges for a specific project grant. + string project_grant_id = 2; + } + + // Resource is the type of the resource the administrator roles should be granted for. + oneof resource { + option (validate.required) = true; + + // Instance is the resource type for granting administrator privileges on the instance level. + bool instance = 1 [(validate.rules).bool = {const: true}]; + // OrganizationID is required to grant administrator privileges for a specific organization. + string organization_id = 2; + // ProjectID is required to grant administrator privileges for a specific project. + string project_id = 3; + // ProjectGrantID is required to grant administrator privileges for a specific project grant. + ProjectGrant project_grant = 4; + } +} + +message CreateAdministratorResponse { + // CreationDate is the timestamp when the administrator role was created. + google.protobuf.Timestamp creation_date = 1; +} + +message UpdateAdministratorRequest { + // UserID is the ID of the user who should have his administrator roles update. + string user_id = 1 [(validate.rules).string = { + min_len: 1 + max_len: 200 + }]; + // Resource is the type of the resource the administrator roles should be granted for. + ResourceType resource = 2; + + // Roles are the roles that the user should be granted. + // Note that any role previously granted to the user and not present in the list will be revoked. + repeated string roles = 3 [(validate.rules).repeated = { + unique: true + items: { + string: { + min_len: 1 + max_len: 200 + } + } + }]; +} + +message UpdateAdministratorResponse { + // ChangeDate is the timestamp when the administrator role was last updated. + google.protobuf.Timestamp change_date = 1; +} + +message DeleteAdministratorRequest { + // UserID is the ID of the user who should have his administrator roles removed. + string user_id = 1 [(validate.rules).string = { + min_len: 1 + max_len: 200 + }]; + // Resource is the type of the resource the administrator roles should be removed for. + ResourceType resource = 2; +} + +message DeleteAdministratorResponse { + // DeletionDate is the timestamp when the administrator role was deleted. + // Note that the deletion date is only guaranteed to be set if the deletion was successful during the request. + // In case the deletion occurred in a previous request, the deletion date might not be set. + google.protobuf.Timestamp deletion_date = 1; +} \ No newline at end of file diff --git a/proto/zitadel/internal_permission/v2beta/query.proto b/proto/zitadel/internal_permission/v2beta/query.proto new file mode 100644 index 0000000000..b23183cd50 --- /dev/null +++ b/proto/zitadel/internal_permission/v2beta/query.proto @@ -0,0 +1,166 @@ +syntax = "proto3"; + +import "google/protobuf/timestamp.proto"; +import "validate/validate.proto"; +import "zitadel/filter/v2beta/filter.proto"; + +package zitadel.internal_permission.v2beta; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/internal_permission/v2beta;internal_permission"; + +message Administrator { + // CreationDate is the timestamp when the administrator role was granted. + google.protobuf.Timestamp creation_date = 1; + // ChangeDate is the timestamp when the administrator role was last updated. + // In case the administrator role was not updated, this field is equal to the creation date. + google.protobuf.Timestamp change_date = 2; + // User is the user who was granted the administrator role. + User user = 3; + // Resource is the type of the resource the administrator roles were granted for. + oneof resource { + // Instance is returned if the administrator roles were granted on the instance level. + bool instance = 4; + // Organization provides information about the organization the administrator roles were granted for. + Organization organization = 5; + // Project provides information about the project the administrator roles were granted for. + Project project = 6; + // ProjectGrant provides information about the project grant the administrator roles were granted for. + ProjectGrant project_grant = 7; + } + // Roles are the roles that were granted to the user for the specified resource. + repeated string roles = 8; +} + +message User { + // ID is the unique identifier of the user. + string id = 1; + // PreferredLoginName is the preferred login name of the user. This value is unique across the whole instance. + string preferred_login_name = 2; + // DisplayName is the public display name of the user. + // By default it's the user's given name and family name, their username or their email address. + string display_name = 3; + // The organization the user belong to. + string organization_id = 4; +} + +message Organization { + // ID is the unique identifier of the organization the user was granted the administrator role for. + string id = 1; + // Name is the name of the organization the user was granted the administrator role for. + string name = 2; +} +message Project { + // ID is the unique identifier of the project the user was granted the administrator role for. + string id = 1; + // Name is the name of the project the user was granted the administrator role for. + string name = 2; + // OrganizationID is the ID of the organization the project belongs to. + string organization_id = 3; +} +message ProjectGrant { + // ID is the unique identifier of the project grant the user was granted the administrator role for. + string id = 1; + // ProjectID is the ID of the project the project grant belongs to. + string project_id = 2; + // ProjectName is the name of the project the project grant belongs to. + string project_name = 3; + // OrganizationID is the ID of the organization the project grant belongs to. + string organization_id = 4; + // OrganizationID is the ID of the organization the project grant belongs to. + string granted_organization_id = 5; +} + +message AdministratorSearchFilter{ + oneof filter { + option (validate.required) = true; + // Search for administrator roles by their creation date. + zitadel.filter.v2beta.TimestampFilter creation_date = 1; + // Search for administrator roles by their change date. + zitadel.filter.v2beta.TimestampFilter change_date = 2; + // Search for administrators roles by the IDs of the users who was granted the administrator role. + zitadel.filter.v2beta.InIDsFilter in_user_ids_filter = 3; + // Search for administrators roles by the ID of the organization the user is part of. + zitadel.filter.v2beta.IDFilter user_organization_id = 4; + // Search for administrators roles by the preferred login name of the user. + UserPreferredLoginNameFilter user_preferred_login_name = 5; + // Search for administrators roles by the display name of the user. + UserDisplayNameFilter user_display_name = 6; + // Search for administrators roles granted for a specific resource. + ResourceFilter resource = 7; + // Search for administrators roles granted with a specific role. + RoleFilter role = 8; + + // Combine multiple authorization queries with an AND operation. + AndFilter and = 9; + // Combine multiple authorization queries with an OR operation. + // For example, to search for authorizations of multiple OrganizationIDs. + OrFilter or = 10; + // Negate an authorization query. + NotFilter not = 11; + } +} + +message UserPreferredLoginNameFilter { + // Search for administrators by the preferred login name of the user. + string preferred_login_name = 1 [(validate.rules).string = { + min_len: 1 + max_len: 200 + }]; + // Specify the method to search for the preferred login name. Default is EQUAL. + // For example, to search for all administrator roles of a user with a preferred login name + // containing a specific string, use CONTAINS or CONTAINS_IGNORE_CASE. + zitadel.filter.v2beta.TextFilterMethod method = 2 [(validate.rules).enum.defined_only = true]; +} + +message UserDisplayNameFilter { + // Search for administrators by the display name of the user. + string display_name = 1 [(validate.rules).string = { + min_len: 1 + max_len: 200 + }]; + // Specify the method to search for the display name. Default is EQUAL. + // For example, to search for all administrator roles of a user with a display name + // containing a specific string, use CONTAINS or CONTAINS_IGNORE_CASE. + zitadel.filter.v2beta.TextFilterMethod method = 2 [(validate.rules).enum.defined_only = true]; +} + +message ResourceFilter { + // Search for administrators by the granted resource. + oneof resource { + // Search for administrators granted on the instance level. + bool instance = 1; + // Search for administrators granted on a specific organization. + string organization_id = 2; + // Search for administrators granted on a specific project. + string project_id = 3; + // Search for administrators granted on a specific project grant. + string project_grant_id = 4; + } +} + +message RoleFilter { + // Search for administrators by the granted role. + string role_key = 1 [(validate.rules).string = { + min_len: 1 + max_len: 200 + }]; +} + +message AndFilter { + repeated AdministratorSearchFilter queries = 1; +} + +message OrFilter { + repeated AdministratorSearchFilter queries = 1; +} + +message NotFilter { + AdministratorSearchFilter query = 1; +} + +enum AdministratorFieldName { + ADMINISTRATOR_FIELD_NAME_UNSPECIFIED = 0; + ADMINISTRATOR_FIELD_NAME_USER_ID = 1; + ADMINISTRATOR_FIELD_NAME_CREATION_DATE = 2; + ADMINISTRATOR_FIELD_NAME_CHANGE_DATE = 3; +} diff --git a/proto/zitadel/management.proto b/proto/zitadel/management.proto index 96d4adf2d4..8bbb0cc3a0 100644 --- a/proto/zitadel/management.proto +++ b/proto/zitadel/management.proto @@ -432,7 +432,11 @@ service ManagementService { }; } - // Deprecated: use ImportHumanUser + // Create User (Human) + // + // Deprecated: use [ImportHumanUser](apis/resources/mgmt/management-service-import-human-user.api.mdx) instead. + // + // Create a new user with the type human. The newly created user will get an initialization email if either the email address is not marked as verified or no password is set. If a password is set the user will not be requested to set a new one on the first login. rpc AddHumanUser(AddHumanUserRequest) returns (AddHumanUserResponse) { option (google.api.http) = { post: "/users/human" @@ -444,10 +448,8 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Deprecated: Create User (Human)"; - description: "Create a new user with the type human. The newly created user will get an initialization email if either the email address is not marked as verified or no password is set. If a password is set the user will not be requested to set a new one on the first login.\n\nDeprecated: use ImportHumanUser" - tags: "Users"; deprecated: true; + tags: "Users"; parameters: { headers: { name: "x-zitadel-orgid"; @@ -459,7 +461,11 @@ service ManagementService { }; } - // Deprecated: please use user service v2 AddHumanUser + // Create/Import User (Human) + // + // Deprecated: use [UpdateHumanUser](apis/resources/user_service_v2/user-service-update-human-user.api.mdx) instead. + // + // Create/import a new user with the type human. The newly created user will get an initialization email if either the email address is not marked as verified or no password is set. If a password is set the user will not be requested to set a new one on the first login. rpc ImportHumanUser(ImportHumanUserRequest) returns (ImportHumanUserResponse) { option (google.api.http) = { post: "/users/human/_import" @@ -471,11 +477,9 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Create/Import User (Human)"; - description: "Create/import a new user with the type human. The newly created user will get an initialization email if either the email address is not marked as verified or no password is set. If a password is set the user will not be requested to set a new one on the first login.\n\nDeprecated: please use user service v2 [AddHumanUser](apis/resources/user_service_v2/user-service-add-human-user.api.mdx)" + deprecated: true; tags: "Users"; tags: "User Human" - deprecated: true; parameters: { headers: { name: "x-zitadel-orgid"; @@ -487,6 +491,9 @@ service ManagementService { }; } + // Create User (Machine) + // + // Create a new user with the type machine for your API, service or device. These users are used for non-interactive authentication flows. rpc AddMachineUser(AddMachineUserRequest) returns (AddMachineUserResponse) { option (google.api.http) = { post: "/users/machine" @@ -498,8 +505,6 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Create User (Machine)"; - description: "Create a new user with the type machine for your API, service or device. These users are used for non-interactive authentication flows." tags: "Users"; tags: "User Machine"; responses: { @@ -683,7 +688,9 @@ service ManagementService { }; } - // Deprecated: please use user service v2 UpdateHumanUser + // Change user name + // + // Change the username of the user. Be aware that the user has to log in with the newly added username afterward rpc UpdateUserName(UpdateUserNameRequest) returns (UpdateUserNameResponse) { option (google.api.http) = { put: "/users/{user_id}/username" @@ -695,10 +702,7 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Change user name"; - description: "Change the username of the user. Be aware that the user has to log in with the newly added username afterward.\n\nDeprecated: please use user service v2 UpdateHumanUser" tags: "Users"; - deprecated: true; responses: { key: "200" value: { @@ -903,7 +907,11 @@ service ManagementService { }; } - // Deprecated: please use user service v2 UpdateHumanUser + // Update User Profile (Human) + // + // Deprecated: use [user service v2 UpdateHumanUser](apis/resources/user_service_v2/user-service-update-human-user.api.mdx) instead. + // + // Update the profile information from a user. The profile includes basic information like first_name and last_name. rpc UpdateHumanProfile(UpdateHumanProfileRequest) returns (UpdateHumanProfileResponse) { option (google.api.http) = { put: "/users/{user_id}/profile" @@ -915,11 +923,9 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Update User Profile (Human)"; - description: "Update the profile information from a user. The profile includes basic information like first_name and last_name.\n\nDeprecated: please use user service v2 UpdateHumanUser" + deprecated: true; tags: "Users"; tags: "User Human"; - deprecated: true; responses: { key: "200" value: { @@ -970,7 +976,11 @@ service ManagementService { }; } - // Deprecated: please use user service v2 SetEmail + // Update User Email (Human) + // + // Deprecated: use [user service v2 SetEmail](apis/resources/user_service_v2/user-service-set-email.api.mdx) instead. + // + // Change the email address of a user. If the state is set to not verified, the user will get a verification email. rpc UpdateHumanEmail(UpdateHumanEmailRequest) returns (UpdateHumanEmailResponse) { option (google.api.http) = { put: "/users/{user_id}/email" @@ -982,8 +992,6 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Update User Email (Human)"; - description: "Change the email address of a user. If the state is set to not verified, the user will get a verification email.\n\nDeprecated: please use user service v2 SetEmail" tags: "Users"; tags: "User Human"; deprecated: true; @@ -1039,7 +1047,11 @@ service ManagementService { }; } - // Deprecated: please use user service v2 ResendEmailCode + // Resend User Email Verification + // + // Deprecated: use [user service v2 ResendEmailCode](apis/resources/user_service_v2/user-service-resend-email-code.api.mdx) instead. + // + // Resend the email verification notification to the given email address of the user. rpc ResendHumanEmailVerification(ResendHumanEmailVerificationRequest) returns (ResendHumanEmailVerificationResponse) { option (google.api.http) = { post: "/users/{user_id}/email/_resend_verification" @@ -1051,8 +1063,6 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Resend User Email Verification"; - description: "Resend the email verification notification to the given email address of the user.\n\nDeprecated: please use user service v2 ResendEmailCode" tags: "Users"; tags: "User Human"; deprecated: true; @@ -1106,7 +1116,9 @@ service ManagementService { }; } - // Deprecated: please use user service v2 SetPhone + // Update User Phone (Human) + // + // Change the phone number of a user. If the state is set to not verified, the user will get an SMS to verify (if a notification provider is configured). The phone number is only for informational purposes and to send messages, not for Authentication (2FA). rpc UpdateHumanPhone(UpdateHumanPhoneRequest) returns (UpdateHumanPhoneResponse) { option (google.api.http) = { put: "/users/{user_id}/phone" @@ -1118,11 +1130,8 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Update User Phone (Human)"; - description: "Change the phone number of a user. If the state is set to not verified, the user will get an SMS to verify (if a notification provider is configured). The phone number is only for informational purposes and to send messages, not for Authentication (2FA).\n\nDeprecated: please use user service v2 SetPhone" tags: "Users"; tags: "User Human"; - deprecated: true; responses: { key: "200" value: { @@ -1140,7 +1149,11 @@ service ManagementService { }; } - // Deprecated: please use user service v2 SetPhone + // Remove User Phone (Human) + // + // Deprecated: use user service v2 [user service v2 SetPhone](apis/resources/user_service_v2/user-service-set-phone.api.mdx) instead. + // + // Remove the configured phone number of a user. rpc RemoveHumanPhone(RemoveHumanPhoneRequest) returns (RemoveHumanPhoneResponse) { option (google.api.http) = { delete: "/users/{user_id}/phone" @@ -1151,8 +1164,6 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Remove User Phone (Human)"; - description: "Remove the configured phone number of a user.\n\nDeprecated: please use user service v2 SetPhone" tags: "Users"; tags: "User Human"; deprecated: true; @@ -1173,7 +1184,11 @@ service ManagementService { }; } - // Deprecated: please use user service v2 ResendPhoneCode + // Resend User Phone Verification + // + // Deprecated: use user service v2 [user service v2 ResendPhoneCode](apis/resources/user_service_v2/user-service-resend-phone-code.api.mdx) instead. + // + // Resend the notification for the verification of the phone number, to the number stored on the user. rpc ResendHumanPhoneVerification(ResendHumanPhoneVerificationRequest) returns (ResendHumanPhoneVerificationResponse) { option (google.api.http) = { post: "/users/{user_id}/phone/_resend_verification" @@ -1185,8 +1200,6 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Resend User Phone Verification"; - description: "Resend the notification for the verification of the phone number, to the number stored on the user.\n\nDeprecated: please use user service v2 ResendPhoneCode" tags: "Users"; tags: "User Human"; deprecated: true; @@ -1238,7 +1251,9 @@ service ManagementService { }; } - // Deprecated: please use user service v2 SetPassword + // Set Human Initial Password + // + // Deprecated: use [user service v2 SetPassword](apis/resources/user_service_v2/user-service-set-password.api.mdx) instead. rpc SetHumanInitialPassword(SetHumanInitialPasswordRequest) returns (SetHumanInitialPasswordResponse) { option (google.api.http) = { post: "/users/{user_id}/password/_initialize" @@ -1252,7 +1267,6 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Users"; tags: "User Human"; - summary: "Set Human Initial Password\n\nDeprecated: please use user service v2 SetPassword"; deprecated: true; parameters: { headers: { @@ -1265,7 +1279,9 @@ service ManagementService { }; } - // Deprecated: please use user service v2 SetPassword + // Set User Password + // + // Deprecated: use [user service v2 SetPassword](apis/resources/user_service_v2/user-service-set-password.api.mdx) instead. rpc SetHumanPassword(SetHumanPasswordRequest) returns (SetHumanPasswordResponse) { option (google.api.http) = { post: "/users/{user_id}/password" @@ -1277,8 +1293,6 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Set User Password"; - description: "Set a new password for a user. Per default, the user has to change the password on the next login. You can set no_change_required to true, to avoid the change on the next login.\n\nDeprecated: please use user service v2 SetPassword" tags: "Users"; tags: "User Human"; deprecated: true; @@ -1299,7 +1313,11 @@ service ManagementService { }; } - // Deprecated: please use user service v2 PasswordReset + // Send Reset Password Notification + // + // Deprecated: use [user service v2 PasswordReset](apis/resources/user_service_v2/user-service-password-reset.api.mdx) instead. + // + // The user will receive an email with a link to change the password. rpc SendHumanResetPasswordNotification(SendHumanResetPasswordNotificationRequest) returns (SendHumanResetPasswordNotificationResponse) { option (google.api.http) = { post: "/users/{user_id}/password/_reset" @@ -1311,8 +1329,6 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Send Reset Password Notification"; - description: "The user will receive an email with a link to change the password.\n\nDeprecated: please use user service v2 PasswordReset" tags: "Users"; tags: "User Human"; deprecated: true; @@ -1629,6 +1645,9 @@ service ManagementService { }; } + // Update Machine User + // + // Change a service account/machine user. It is used for accounts with non-interactive authentication possibilities. rpc UpdateMachine(UpdateMachineRequest) returns (UpdateMachineResponse) { option (google.api.http) = { put: "/users/{user_id}/machine" @@ -1640,8 +1659,6 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Update Machine User"; - description: "Change a service account/machine user. It is used for accounts with non-interactive authentication possibilities." tags: "Users"; tags: "User Machine"; responses: { @@ -1661,6 +1678,9 @@ service ManagementService { }; } + // Create Secret for Machine User + // + // Create a new secret for a machine user/service account. It is used to authenticate the user (client credential grant). rpc GenerateMachineSecret(GenerateMachineSecretRequest) returns (GenerateMachineSecretResponse) { option (google.api.http) = { put: "/users/{user_id}/secret" @@ -1672,8 +1692,6 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Create Secret for Machine User"; - description: "Create a new secret for a machine user/service account. It is used to authenticate the user (client credential grant)." tags: "Users"; tags: "User Machine"; responses: { @@ -1693,6 +1711,9 @@ service ManagementService { }; } + // Delete Secret of Machine User + // + // Delete a secret of a machine user/service account. The user will not be able to authenticate with the secret afterward. rpc RemoveMachineSecret(RemoveMachineSecretRequest) returns (RemoveMachineSecretResponse) { option (google.api.http) = { delete: "/users/{user_id}/secret" @@ -1703,8 +1724,6 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Delete Secret of Machine User"; - description: "Delete a secret of a machine user/service account. The user will not be able to authenticate with the secret afterward." tags: "Users"; tags: "User Machine"; responses: { @@ -1724,6 +1743,9 @@ service ManagementService { }; } + // Get Machine user Key By ID + // + // Get a specific Key of a machine user by its id. Machine keys are used to authenticate with jwt profile authentication. rpc GetMachineKeyByIDs(GetMachineKeyByIDsRequest) returns (GetMachineKeyByIDsResponse) { option (google.api.http) = { get: "/users/{user_id}/keys/{key_id}" @@ -1734,8 +1756,6 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Get Machine user Key By ID"; - description: "Get a specific Key of a machine user by its id. Machine keys are used to authenticate with jwt profile authentication." tags: "Users"; tags: "User Machine"; responses: { @@ -1755,6 +1775,9 @@ service ManagementService { }; } + // List Machine Keys + // + // Get the list of keys of a machine user. Machine keys are used to authenticate with jwt profile authentication. rpc ListMachineKeys(ListMachineKeysRequest) returns (ListMachineKeysResponse) { option (google.api.http) = { post: "/users/{user_id}/keys/_search" @@ -1766,8 +1789,6 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Get Machine user Key By ID"; - description: "Get the list of keys of a machine user. Machine keys are used to authenticate with jwt profile authentication." tags: "Users"; tags: "User Machine"; responses: { @@ -1787,6 +1808,12 @@ service ManagementService { }; } + // Create Key for machine user + // + // If a public key is not supplied, a new key is generated and will be returned in the response. + // Make sure to store the returned key. + // If an RSA public key is supplied, the private key is omitted from the response. + // Machine keys are used to authenticate with jwt profile. rpc AddMachineKey(AddMachineKeyRequest) returns (AddMachineKeyResponse) { option (google.api.http) = { post: "/users/{user_id}/keys" @@ -1798,8 +1825,6 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Create Key for machine user"; - description: "If a public key is not supplied, a new key is generated and will be returned in the response. Make sure to store the returned key. If an RSA public key is supplied, the private key is omitted from the response. Machine keys are used to authenticate with jwt profile." tags: "Users"; tags: "User Machine"; responses: { @@ -1819,6 +1844,10 @@ service ManagementService { }; } + // Delete Key for machine user + // + // Delete a specific key from a user. + // The user will not be able to authenticate with that key afterward. rpc RemoveMachineKey(RemoveMachineKeyRequest) returns (RemoveMachineKeyResponse) { option (google.api.http) = { delete: "/users/{user_id}/keys/{key_id}" @@ -1829,8 +1858,6 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Delete Key for machine user"; - description: "Delete a specific key from a user. The user will not be able to authenticate with that key afterward." tags: "Users"; tags: "User Machine"; responses: { @@ -1850,6 +1877,9 @@ service ManagementService { }; } + // Get Personal-Access-Token (PAT) by ID + // + // Returns the PAT for a user, currently only available for machine users/service accounts. PATs are ready-to-use tokens and can be sent directly in the authentication header. rpc GetPersonalAccessTokenByIDs(GetPersonalAccessTokenByIDsRequest) returns (GetPersonalAccessTokenByIDsResponse) { option (google.api.http) = { get: "/users/{user_id}/pats/{token_id}" @@ -1860,8 +1890,6 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Get a Personal-Access-Token (PAT) by ID"; - description: "Returns the PAT for a user, currently only available for machine users/service accounts. PATs are ready-to-use tokens and can be sent directly in the authentication header." tags: "Users"; tags: "User Machine"; responses: { @@ -1881,6 +1909,9 @@ service ManagementService { }; } + // List Personal-Access-Tokens (PATs) + // + // Returns a list of PATs for a user, currently only available for machine users/service accounts. PATs are ready-to-use tokens and can be sent directly in the authentication header. rpc ListPersonalAccessTokens(ListPersonalAccessTokensRequest) returns (ListPersonalAccessTokensResponse) { option (google.api.http) = { post: "/users/{user_id}/pats/_search" @@ -1892,8 +1923,6 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Get a Personal-Access-Token (PAT) by ID"; - description: "Returns a list of PATs for a user, currently only available for machine users/service accounts. PATs are ready-to-use tokens and can be sent directly in the authentication header." tags: "Users"; tags: "User Machine"; responses: { @@ -1913,6 +1942,11 @@ service ManagementService { }; } + // Create a Personal-Access-Token (PAT) + // + // Generates a new PAT for the user. Currently only available for machine users. + // The token will be returned in the response, make sure to store it. + // PATs are ready-to-use tokens and can be sent directly in the authentication header. rpc AddPersonalAccessToken(AddPersonalAccessTokenRequest) returns (AddPersonalAccessTokenResponse) { option (google.api.http) = { post: "/users/{user_id}/pats" @@ -1924,8 +1958,6 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Create a Personal-Access-Token (PAT)"; - description: "Generates a new PAT for the user. Currently only available for machine users. The token will be returned in the response, make sure to store it. PATs are ready-to-use tokens and can be sent directly in the authentication header." tags: "Users"; tags: "User Machine"; responses: { @@ -1945,6 +1977,9 @@ service ManagementService { }; } + // Remove a Personal-Access-Token (PAT) by ID + // + // Delete a PAT from a user. Afterward, the user will not be able to authenticate with that token anymore. rpc RemovePersonalAccessToken(RemovePersonalAccessTokenRequest) returns (RemovePersonalAccessTokenResponse) { option (google.api.http) = { delete: "/users/{user_id}/pats/{token_id}" @@ -1955,8 +1990,6 @@ service ManagementService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Get a Personal-Access-Token (PAT) by ID"; - description: "Delete a PAT from a user. Afterward, the user will not be able to authenticate with that token anymore." tags: "Users"; tags: "User Machine"; responses: { @@ -2003,7 +2036,7 @@ service ManagementService { }; } - // Deprecated: please use user service v2 RemoveLinkedIDP + // Deprecated: please use [user service v2 RemoveIDPLink](apis/resources/user_service_v2/user-service-remove-idp-link.api.mdx) rpc RemoveHumanLinkedIDP(RemoveHumanLinkedIDPRequest) returns (RemoveHumanLinkedIDPResponse) { option (google.api.http) = { delete: "/users/{user_id}/idps/{idp_id}/{linked_user_id}" @@ -2119,6 +2152,7 @@ service ManagementService { }; } + // Deprecated: use CreateOrganization [apis/resources/org_service_v2beta/organization-service-create-organization.api.mdx] API instead rpc AddOrg(AddOrgRequest) returns (AddOrgResponse) { option (google.api.http) = { post: "/orgs" @@ -2133,6 +2167,7 @@ service ManagementService { tags: "Organizations"; summary: "Create Organization"; description: "Create a new organization. Based on the given name a domain will be generated to be able to identify users within an organization." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -2144,6 +2179,7 @@ service ManagementService { }; } + // Deprecated: use UpdateOrganization [apis/resources/org_service_v2beta/organization-service-update-organization.api.mdx] API instead rpc UpdateOrg(UpdateOrgRequest) returns (UpdateOrgResponse) { option (google.api.http) = { put: "/orgs/me" @@ -2158,6 +2194,7 @@ service ManagementService { tags: "Organizations"; summary: "Update Organization"; description: "Change the name of the organization." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -2169,6 +2206,7 @@ service ManagementService { }; } + // Deprecated: use DeactivateOrganization [apis/resources/org_service_v2beta/organization-service-deactivate-organization.api.mdx] API instead rpc DeactivateOrg(DeactivateOrgRequest) returns (DeactivateOrgResponse) { option (google.api.http) = { post: "/orgs/me/_deactivate" @@ -2183,6 +2221,7 @@ service ManagementService { tags: "Organizations"; summary: "Deactivate Organization"; description: "Sets the state of my organization to deactivated. Users of this organization will not be able to log in." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -2194,6 +2233,7 @@ service ManagementService { }; } + // Deprecated: use ActivateOrganization [apis/resources/org_service_v2beta/organization-service-activate-organization.api.mdx] API instead rpc ReactivateOrg(ReactivateOrgRequest) returns (ReactivateOrgResponse) { option (google.api.http) = { post: "/orgs/me/_reactivate" @@ -2208,6 +2248,7 @@ service ManagementService { tags: "Organizations"; summary: "Reactivate Organization"; description: "Set the state of my organization to active. The state of the organization has to be deactivated to perform the request. Users of this organization will be able to log in again." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -2219,6 +2260,7 @@ service ManagementService { }; } + // Deprecated: use DeleteOrganization [apis/resources/org_service_v2beta/organization-service-delete-organization.api.mdx] API instead rpc RemoveOrg(RemoveOrgRequest) returns (RemoveOrgResponse) { option (google.api.http) = { delete: "/orgs/me" @@ -2232,6 +2274,7 @@ service ManagementService { tags: "Organizations"; summary: "Delete Organization"; description: "Deletes my organization and all its resources (Users, Projects, Grants to and from the org). Users of this organization will not be able to log in." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -2243,6 +2286,7 @@ service ManagementService { }; } + // Deprecated: use SetOrganizationMetadata [apis/resources/org_service_v2beta/organization-service-set-organization-metadata.api.mdx] API instead rpc SetOrgMetadata(SetOrgMetadataRequest) returns (SetOrgMetadataResponse) { option (google.api.http) = { post: "/metadata/{key}" @@ -2258,6 +2302,7 @@ service ManagementService { tags: "Organization Metadata"; summary: "Set Organization Metadata"; description: "This endpoint either adds or updates a metadata value for the requested key. Make sure the value is base64 encoded." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -2269,6 +2314,7 @@ service ManagementService { }; } + // Deprecated: use SetOrganizationMetadata [apis/resources/org_service_v2beta/organization-service-set-organization-metadata.api.mdx] API instead rpc BulkSetOrgMetadata(BulkSetOrgMetadataRequest) returns (BulkSetOrgMetadataResponse) { option (google.api.http) = { post: "/metadata/_bulk" @@ -2284,6 +2330,7 @@ service ManagementService { tags: "Organization Metadata"; summary: "Bulk Set Organization Metadata"; description: "This endpoint sets a list of metadata to the organization. Make sure the values are base64 encoded." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -2295,6 +2342,7 @@ service ManagementService { }; } + // Deprecated: use ListOrganizationMetadata [apis/resources/org_service_v2beta/organization-service-list-organization-metadata.api.mdx] API instead rpc ListOrgMetadata(ListOrgMetadataRequest) returns (ListOrgMetadataResponse) { option (google.api.http) = { post: "/metadata/_search" @@ -2310,6 +2358,7 @@ service ManagementService { tags: "Organization Metadata"; summary: "Search Organization Metadata"; description: "Get the metadata of an organization filtered by your query." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -2321,6 +2370,7 @@ service ManagementService { }; } + // Deprecated: use ListOrganizationMetadata [apis/resources/org_service_v2beta/organization-service-list-organization-metadata.api.mdx] API instead rpc GetOrgMetadata(GetOrgMetadataRequest) returns (GetOrgMetadataResponse) { option (google.api.http) = { get: "/metadata/{key}" @@ -2335,6 +2385,7 @@ service ManagementService { tags: "Organization Metadata"; summary: "Get Organization Metadata By Key"; description: "Get a metadata object from an organization by a specific key." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -2346,6 +2397,7 @@ service ManagementService { }; } + // Deprecated: use DeleteOrganizationMetadata [apis/resources/org_service_v2beta/organization-service-delete-organization-metadata.api.mdx] API instead rpc RemoveOrgMetadata(RemoveOrgMetadataRequest) returns (RemoveOrgMetadataResponse) { option (google.api.http) = { delete: "/metadata/{key}" @@ -2360,6 +2412,7 @@ service ManagementService { tags: "Organization Metadata"; summary: "Delete Organization Metadata By Key"; description: "Remove a metadata object from an organization with a specific key." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -2371,6 +2424,7 @@ service ManagementService { }; } + // Deprecated: use DeleteOrganizationMetadata [apis/resources/org_service_v2beta/organization-service-delete-organization-metadata.api.mdx] API instead rpc BulkRemoveOrgMetadata(BulkRemoveOrgMetadataRequest) returns (BulkRemoveOrgMetadataResponse) { option (google.api.http) = { delete: "/metadata/_bulk" @@ -2384,6 +2438,7 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "Organizations"; tags: "Organization Metadata"; + deprecated: true summary: "Bulk Delete Metadata"; description: "Remove a list of metadata objects from an organization with a list of keys." parameters: { @@ -2397,31 +2452,7 @@ service ManagementService { }; } - rpc ListOrgDomains(ListOrgDomainsRequest) returns (ListOrgDomainsResponse) { - option (google.api.http) = { - post: "/orgs/me/domains/_search" - body: "*" - }; - - option (zitadel.v1.auth_option) = { - permission: "org.read" - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - tags: "Organizations"; - summary: "Search Domains"; - description: "Returns the list of registered domains of an organization. The domains are used to identify to which organization a user belongs." - parameters: { - headers: { - name: "x-zitadel-orgid"; - description: "The default is always the organization of the requesting user. If you like to get/set a result of another organization include the header. Make sure the user has permission to access the requested data."; - type: STRING, - required: false; - }; - }; - }; - } - + // Deprecated: use AddOrganizationDomain [apis/resources/org_service_v2beta/organization-service-add-organization-domain.api.mdx] API instead rpc AddOrgDomain(AddOrgDomainRequest) returns (AddOrgDomainResponse) { option (google.api.http) = { post: "/orgs/me/domains" @@ -2436,6 +2467,7 @@ service ManagementService { tags: "Organizations"; summary: "Add Domain"; description: "Add a new domain to an organization. The domains are used to identify to which organization a user belongs." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -2447,6 +2479,34 @@ service ManagementService { }; } + // Deprecated: use ListOrganizationDomains [apis/resources/org_service_v2beta/organization-service-list-organization-domains.api.mdx] API instead + rpc ListOrgDomains(ListOrgDomainsRequest) returns (ListOrgDomainsResponse) { + option (google.api.http) = { + post: "/orgs/me/domains/_search" + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "org.read" + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: "Organizations"; + summary: "Search Domains"; + description: "Returns the list of registered domains of an organization. The domains are used to identify to which organization a user belongs." + deprecated: true + parameters: { + headers: { + name: "x-zitadel-orgid"; + description: "The default is always the organization of the requesting user. If you like to get/set a result of another organization include the header. Make sure the user has permission to access the requested data."; + type: STRING, + required: false; + }; + }; + }; + } + + // Deprecated: use DeleteOrganizationDomain [apis/resources/org_service_v2beta/organization-service-delete-organization-domain.api.mdx] API instead rpc RemoveOrgDomain(RemoveOrgDomainRequest) returns (RemoveOrgDomainResponse) { option (google.api.http) = { delete: "/orgs/me/domains/{domain}" @@ -2460,6 +2520,7 @@ service ManagementService { tags: "Organizations"; summary: "Remove Domain"; description: "Delete a new domain from an organization. The domains are used to identify to which organization a user belongs. If the uses use the domain for login, this will not be possible afterwards. They have to use another domain instead." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -2471,6 +2532,7 @@ service ManagementService { }; } + // Deprecated: use GenerateOrganizationDomainValidation [apis/resources/org_service_v2beta/organization-service-generate-organization-domain-validation.api.mdx] API instead rpc GenerateOrgDomainValidation(GenerateOrgDomainValidationRequest) returns (GenerateOrgDomainValidationResponse) { option (google.api.http) = { post: "/orgs/me/domains/{domain}/validation/_generate" @@ -2485,6 +2547,7 @@ service ManagementService { tags: "Organizations"; summary: "Generate Domain Verification"; description: "Generate a new file to be able to verify your domain with DNS or HTTP challenge." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -2496,6 +2559,7 @@ service ManagementService { }; } + // Deprecated: use VerifyOrganizationDomain [apis/resources/org_service_v2beta/organization-service-verify-organization-domain.api.mdx] API instead rpc ValidateOrgDomain(ValidateOrgDomainRequest) returns (ValidateOrgDomainResponse) { option (google.api.http) = { post: "/orgs/me/domains/{domain}/validation/_validate" @@ -2510,6 +2574,7 @@ service ManagementService { tags: "Organizations"; summary: "Verify Domain"; description: "Make sure you have added the required verification to your domain, depending on the method you have chosen (HTTP or DNS challenge). ZITADEL will check it and set the domain as verified if it was successful. A verify domain has to be unique." + deprecated: true parameters: { headers: { name: "x-zitadel-orgid"; @@ -3036,7 +3101,7 @@ service ManagementService { rpc UpdateProjectRole(UpdateProjectRoleRequest) returns (UpdateProjectRoleResponse) { option (google.api.http) = { - put: "/projects/{project_id}/roles/{role_key}" + put: "/projects/{project_id}/roles/{role_key=**}" body: "*" }; @@ -3062,7 +3127,7 @@ service ManagementService { rpc RemoveProjectRole(RemoveProjectRoleRequest) returns (RemoveProjectRoleResponse) { option (google.api.http) = { - delete: "/projects/{project_id}/roles/{role_key}" + delete: "/projects/{project_id}/roles/{role_key=**}" }; option (zitadel.v1.auth_option) = { @@ -3222,6 +3287,7 @@ service ManagementService { }; } + // Deprecated: Use [GetApplication](/apis/resources/application_service_v2/application-service-get-application.api.mdx) instead to fetch an app rpc GetAppByID(GetAppByIDRequest) returns (GetAppByIDResponse) { option (google.api.http) = { get: "/projects/{project_id}/apps/{app_id}" @@ -3244,9 +3310,11 @@ service ManagementService { required: false; }; }; + deprecated: true; }; } + // Deprecated: Use [ListApplications](/apis/resources/application_service_v2/application-service-list-applications.api.mdx) instead to list applications rpc ListApps(ListAppsRequest) returns (ListAppsResponse) { option (google.api.http) = { post: "/projects/{project_id}/apps/_search" @@ -3270,6 +3338,7 @@ service ManagementService { required: false; }; }; + deprecated: true; }; } @@ -3298,6 +3367,7 @@ service ManagementService { }; } + // Deprecated: Use [CreateApplication](/apis/resources/application_service_v2/application-service-create-application.api.mdx) instead to create an OIDC application rpc AddOIDCApp(AddOIDCAppRequest) returns (AddOIDCAppResponse) { option (google.api.http) = { post: "/projects/{project_id}/apps/oidc" @@ -3321,62 +3391,74 @@ service ManagementService { required: false; }; }; + deprecated: true; }; } - rpc AddSAMLApp(AddSAMLAppRequest) returns (AddSAMLAppResponse) { - option (google.api.http) = { - post: "/projects/{project_id}/apps/saml" - body: "*" - }; - - option (zitadel.v1.auth_option) = { - permission: "project.app.write" - check_field_name: "ProjectId" - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - tags: "Applications"; - summary: "Create Application (SAML)"; - description: "Create a new SAML client. Returns an entity ID" - parameters: { - headers: { - name: "x-zitadel-orgid"; - description: "The default is always the organization of the requesting user. If you like to change/get objects of another organization include the header. Make sure the requesting user has permission to access the requested data."; - type: STRING, - required: false; - }; - }; - }; - } - - rpc AddAPIApp(AddAPIAppRequest) returns (AddAPIAppResponse) { - option (google.api.http) = { - post: "/projects/{project_id}/apps/api" - body: "*" - }; - - option (zitadel.v1.auth_option) = { - permission: "project.app.write" - check_field_name: "ProjectId" + // Deprecated: Use [CreateApplication](/apis/resources/application_service_v2/application-service-create-application.api.mdx) instead to create a SAML application + rpc AddSAMLApp(AddSAMLAppRequest) returns (AddSAMLAppResponse) { + option (google.api.http) = { + post: "/projects/{project_id}/apps/saml" + body: "*" }; - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - tags: "Applications"; - summary: "Create Application (API)"; - description: "Create a new API client. The client id will be generated and returned in the response. Depending on the chosen configuration also a secret will be generated and returned." - parameters: { - headers: { - name: "x-zitadel-orgid"; - description: "The default is always the organization of the requesting user. If you like to change/get objects of another organization include the header. Make sure the requesting user has permission to access the requested data."; - type: STRING, - required: false; - }; - }; - }; + option (zitadel.v1.auth_option) = { + permission: "project.app.write" + check_field_name: "ProjectId" + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: "Applications"; + summary: "Create Application (SAML)"; + description: "Create a new SAML client. Returns an entity ID" + parameters: { + headers: { + name: "x-zitadel-orgid"; + description: "The default is always the organization of the requesting user. If you like to change/get objects of another organization include the header. Make sure the requesting user has permission to access the requested data."; + type: STRING, + required: false; + }; + }; + deprecated: true; + }; + } + + // Create Application (API) + // + // Create a new API client. The client id will be generated and returned in the response. + // Depending on the chosen configuration also a secret will be generated and returned. + // + // Deprecated: Use [CreateApplication](/apis/resources/application_service_v2/application-service-create-application.api.mdx) instead to create an API application + rpc AddAPIApp(AddAPIAppRequest) returns (AddAPIAppResponse) { + option (google.api.http) = { + post: "/projects/{project_id}/apps/api" + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "project.app.write" + check_field_name: "ProjectId" + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: "Applications"; + summary: "Create Application (API)"; + description: "Create a new API client. The client id will be generated and returned in the response. Depending on the chosen configuration also a secret will be generated and returned." + parameters: { + headers: { + name: "x-zitadel-orgid"; + description: "The default is always the organization of the requesting user. If you like to change/get objects of another organization include the header. Make sure the requesting user has permission to access the requested data."; + type: STRING, + required: false; + }; + }; + deprecated: true; + }; } // Changes application + // + // Deprecated: Use [PatchApplication](/apis/resources/application_service_v2/application-service-patch-application.api.mdx) instead to update the generic params of an app rpc UpdateApp(UpdateAppRequest) returns (UpdateAppResponse) { option (google.api.http) = { put: "/projects/{project_id}/apps/{app_id}" @@ -3400,9 +3482,11 @@ service ManagementService { required: false; }; }; + deprecated: true; }; } + // Deprecated: Use [PatchApplication](/apis/resources/application_service_v2/application-service-patch-application.api.mdx) instead to update the config of an OIDC app rpc UpdateOIDCAppConfig(UpdateOIDCAppConfigRequest) returns (UpdateOIDCAppConfigResponse) { option (google.api.http) = { put: "/projects/{project_id}/apps/{app_id}/oidc_config" @@ -3426,61 +3510,67 @@ service ManagementService { required: false; }; }; + deprecated: true }; } - rpc UpdateSAMLAppConfig(UpdateSAMLAppConfigRequest) returns (UpdateSAMLAppConfigResponse) { - option (google.api.http) = { - put: "/projects/{project_id}/apps/{app_id}/saml_config" - body: "*" - }; - - option (zitadel.v1.auth_option) = { - permission: "project.app.write" - check_field_name: "ProjectId" - }; - - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - tags: "Applications"; - summary: "Update SAML Application Config"; - description: "Update the SAML specific configuration of an application." - parameters: { - headers: { - name: "x-zitadel-orgid"; - description: "The default is always the organization of the requesting user. If you like to change/get objects of another organization include the header. Make sure the requesting user has permission to access the requested data."; - type: STRING, - required: false; - }; - }; - }; - } - - rpc UpdateAPIAppConfig(UpdateAPIAppConfigRequest) returns (UpdateAPIAppConfigResponse) { - option (google.api.http) = { - put: "/projects/{project_id}/apps/{app_id}/api_config" - body: "*" - }; + // Deprecated: Use [PatchApplication](/apis/resources/application_service_v2/application-service-patch-application.api.mdx) instead to update the config of a SAML app + rpc UpdateSAMLAppConfig(UpdateSAMLAppConfigRequest) returns (UpdateSAMLAppConfigResponse) { + option (google.api.http) = { + put: "/projects/{project_id}/apps/{app_id}/saml_config" + body: "*" + }; option (zitadel.v1.auth_option) = { - permission: "project.app.write" - check_field_name: "ProjectId" + permission: "project.app.write" + check_field_name: "ProjectId" }; - option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - tags: "Applications"; - summary: "Update API Application Config"; - description: "Update the OIDC-specific configuration of an application." - parameters: { - headers: { - name: "x-zitadel-orgid"; - description: "The default is always the organization of the requesting user. If you like to change/get objects of another organization include the header. Make sure the requesting user has permission to access the requested data."; - type: STRING, - required: false; - }; - }; - }; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: "Applications"; + summary: "Update SAML Application Config"; + description: "Update the SAML specific configuration of an application." + parameters: { + headers: { + name: "x-zitadel-orgid"; + description: "The default is always the organization of the requesting user. If you like to change/get objects of another organization include the header. Make sure the requesting user has permission to access the requested data."; + type: STRING, + required: false; + }; + }; + deprecated: true; + }; } + // Deprecated: Use [PatchApplication](/apis/resources/application_service_v2/application-service-patch-application.api.mdx) instead to update the config of an API app + rpc UpdateAPIAppConfig(UpdateAPIAppConfigRequest) returns (UpdateAPIAppConfigResponse) { + option (google.api.http) = { + put: "/projects/{project_id}/apps/{app_id}/api_config" + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "project.app.write" + check_field_name: "ProjectId" + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: "Applications"; + summary: "Update API Application Config"; + description: "Update the OIDC-specific configuration of an application." + parameters: { + headers: { + name: "x-zitadel-orgid"; + description: "The default is always the organization of the requesting user. If you like to change/get objects of another organization include the header. Make sure the requesting user has permission to access the requested data."; + type: STRING, + required: false; + }; + }; + deprecated: true; + }; + } + + // Deprecated: Use [DeactivateApplication](/apis/resources/application_service_v2/application-service-deactivate-application.api.mdx) instead to deactivate an app rpc DeactivateApp(DeactivateAppRequest) returns (DeactivateAppResponse) { option (google.api.http) = { post: "/projects/{project_id}/apps/{app_id}/_deactivate" @@ -3504,9 +3594,11 @@ service ManagementService { required: false; }; }; + deprecated: true; }; } + // Deprecated: Use [ReactivateApplication](/apis/resources/application_service_v2/application-service-reactivate-application.api.mdx) instead to reactivate an app rpc ReactivateApp(ReactivateAppRequest) returns (ReactivateAppResponse) { option (google.api.http) = { post: "/projects/{project_id}/apps/{app_id}/_reactivate" @@ -3530,9 +3622,11 @@ service ManagementService { required: false; }; }; + deprecated: true; }; } + // Deprecated: Use [DeleteApplication](/apis/resources/application_service_v2/application-service-delete-application.api.mdx) instead to delete an app rpc RemoveApp(RemoveAppRequest) returns (RemoveAppResponse) { option (google.api.http) = { delete: "/projects/{project_id}/apps/{app_id}" @@ -3555,9 +3649,11 @@ service ManagementService { required: false; }; }; + deprecated: true; }; } + // Deprecated: Use [RegenerateClientSecret](/apis/resources/application_service_v2/application-service-regenerate-client-secret.api.mdx) instead to regenerate an OIDC app client secret rpc RegenerateOIDCClientSecret(RegenerateOIDCClientSecretRequest) returns (RegenerateOIDCClientSecretResponse) { option (google.api.http) = { post: "/projects/{project_id}/apps/{app_id}/oidc_config/_generate_client_secret" @@ -3581,9 +3677,11 @@ service ManagementService { required: false; }; }; + deprecated: true; }; } + // Deprecated: Use [RegenerateClientSecret](/apis/resources/application_service_v2/application-service-regenerate-client-secret.api.mdx) instead to regenerate an API app client secret rpc RegenerateAPIClientSecret(RegenerateAPIClientSecretRequest) returns (RegenerateAPIClientSecretResponse) { option (google.api.http) = { post: "/projects/{project_id}/apps/{app_id}/api_config/_generate_client_secret" @@ -3607,9 +3705,11 @@ service ManagementService { required: false; }; }; + deprecated: true; }; } + // Deprecated: Use [GetApplicationKey](/apis/resources/application_service_v2/application-service-get-application-key.api.mdx) instead to get an application key rpc GetAppKey(GetAppKeyRequest) returns (GetAppKeyResponse) { option (google.api.http) = { get: "/projects/{project_id}/apps/{app_id}/keys/{key_id}" @@ -3632,9 +3732,11 @@ service ManagementService { required: false; }; }; + deprecated: true; }; } + // Deprecated: Use [ListApplicationKeys](/apis/resources/application_service_v2/application-service-list-application-keys.api.mdx) instead to list application keys rpc ListAppKeys(ListAppKeysRequest) returns (ListAppKeysResponse) { option (google.api.http) = { post: "/projects/{project_id}/apps/{app_id}/keys/_search" @@ -3661,6 +3763,8 @@ service ManagementService { }; } + // Deprecated: Use [CreateApplicationKey](/apis/resources/application_service_v2/application-service-create-application-key.api.mdx) instead to + // create an application key rpc AddAppKey(AddAppKeyRequest) returns (AddAppKeyResponse){ option (google.api.http) = { post: "/projects/{project_id}/apps/{app_id}/keys" @@ -3684,9 +3788,12 @@ service ManagementService { required: false; }; }; + deprecated: true; }; } + // Deprecated: Use [DeleteApplicationKey](/apis/resources/application_service_v2/application-service-delete-application-key.api.mdx) instead to + // delete an application key rpc RemoveAppKey(RemoveAppKeyRequest) returns (RemoveAppKeyResponse) { option (google.api.http) = { delete: "/projects/{project_id}/apps/{app_id}/keys/{key_id}" @@ -3709,6 +3816,7 @@ service ManagementService { required: false; }; }; + deprecated: true; }; } @@ -4069,6 +4177,11 @@ service ManagementService { }; } + // Get User Grant By ID + // + // Deprecated: [List authorizations](apis/resources/authorization_service_v2/zitadel-authorization-v-2-beta-authorization-service-list-authorizations.api.mdx) and filter by its ID. + // + // Returns a user grant per ID. A user grant is a role a user has for a specific project and organization. rpc GetUserGrantByID(GetUserGrantByIDRequest) returns (GetUserGrantByIDResponse) { option (google.api.http) = { get: "/users/{user_id}/grants/{grant_id}" @@ -4080,8 +4193,7 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "User Grants"; - summary: "User Grant By ID"; - description: "Returns a user grant per ID. A user grant is a role a user has for a specific project and organization." + deprecated: true; parameters: { headers: { name: "x-zitadel-orgid"; @@ -4093,6 +4205,11 @@ service ManagementService { }; } + // Search User Grants + // + // Deprecated: [List authorizations](apis/resources/authorization_service_v2/zitadel-authorization-v-2-beta-authorization-service-list-authorizations.api.mdx) and pass the user ID filter to search for a users grants on owned or granted projects. + // + // Returns a list of user grants that match the search queries. User grants are the roles users have for a specific project and organization. rpc ListUserGrants(ListUserGrantRequest) returns (ListUserGrantResponse) { option (google.api.http) = { post: "/users/grants/_search" @@ -4105,8 +4222,7 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "User Grants"; - summary: "Search User Grants"; - description: "Returns a list of user grants that match the search queries. User grants are the roles users have for a specific project and organization." + deprecated: true; parameters: { headers: { name: "x-zitadel-orgid"; @@ -4118,6 +4234,12 @@ service ManagementService { }; } + + // Add User Grant + // + // Deprecated: [Add an authorization](apis/resources/authorization_service_v2/zitadel-authorization-v-2-beta-authorization-service-create-authorization.api.mdx) to grant a user access to an owned or granted project. + // + // Add a user grant for a specific user. User grants are the roles users have for a specific project and organization. rpc AddUserGrant(AddUserGrantRequest) returns (AddUserGrantResponse) { option (google.api.http) = { post: "/users/{user_id}/grants" @@ -4130,8 +4252,7 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "User Grants"; - summary: "Add User Grant"; - description: "Add a user grant for a specific user. User grants are the roles users have for a specific project and organization." + deprecated: true; parameters: { headers: { name: "x-zitadel-orgid"; @@ -4143,6 +4264,11 @@ service ManagementService { }; } + // Update User Grant + // + // Deprecated: [Update an authorization](apis/resources/authorization_service_v2/zitadel-authorization-v-2-beta-authorization-service-update-authorization.api.mdx) to update a user's roles on an owned or granted project. + // + // Update the roles of a user grant. User grants are the roles users have for a specific project and organization. rpc UpdateUserGrant(UpdateUserGrantRequest) returns (UpdateUserGrantResponse) { option (google.api.http) = { put: "/users/{user_id}/grants/{grant_id}" @@ -4155,8 +4281,7 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "User Grants"; - summary: "Update User Grants"; - description: "Update the roles of a user grant. User grants are the roles users have for a specific project and organization." + deprecated: true; parameters: { headers: { name: "x-zitadel-orgid"; @@ -4168,6 +4293,11 @@ service ManagementService { }; } + // Deactivate User Grant + // + // Deprecated: [Deactivate an authorization](apis/resources/authorization_service_v2/zitadel-authorization-v-2-beta-authorization-service-deactivate-authorization.api.mdx) to disable a user's access to an owned or granted project. + // + // Deactivate the user grant. The user will not be able to use the granted project anymore. Also, the roles will not be included in the tokens when requested. An error will be returned if the user grant is already deactivated. rpc DeactivateUserGrant(DeactivateUserGrantRequest) returns (DeactivateUserGrantResponse) { option (google.api.http) = { post: "/users/{user_id}/grants/{grant_id}/_deactivate" @@ -4180,8 +4310,7 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "User Grants"; - summary: "Deactivate User Grant"; - description: "Deactivate the user grant. The user will not be able to use the granted project anymore. Also, the roles will not be included in the tokens when requested. An error will be returned if the user grant is already deactivated." + deprecated: true; parameters: { headers: { name: "x-zitadel-orgid"; @@ -4193,6 +4322,11 @@ service ManagementService { }; } + // Reactivate User Grant + // + // Deprecated: [Activate an authorization](apis/resources/authorization_service_v2/zitadel-authorization-v-2-beta-authorization-service-activate-authorization.api.mdx) to enable a user's access to an owned or granted project. + // + // Reactivate a deactivated user grant. The user will be able to use the granted project again. An error will be returned if the user grant is not deactivated. rpc ReactivateUserGrant(ReactivateUserGrantRequest) returns (ReactivateUserGrantResponse) { option (google.api.http) = { post: "/users/{user_id}/grants/{grant_id}/_reactivate" @@ -4205,8 +4339,7 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "User Grants"; - summary: "Reactivate User Grant"; - description: "Reactivate a deactivated user grant. The user will be able to use the granted project again. An error will be returned if the user grant is not deactivated." + deprecated: true; parameters: { headers: { name: "x-zitadel-orgid"; @@ -4218,6 +4351,11 @@ service ManagementService { }; } + // Remove User Grant + // + // Deprecated: [Delete an authorization](apis/resources/authorization_service_v2/zitadel-authorization-v-2-beta-authorization-service-delete-authorization.api.mdx) to remove a users access to an owned or granted project. + // + // Removes the user grant from the user. The user will not be able to use the granted project anymore. Also, the roles will not be included in the tokens when requested. rpc RemoveUserGrant(RemoveUserGrantRequest) returns (RemoveUserGrantResponse) { option (google.api.http) = { delete: "/users/{user_id}/grants/{grant_id}" @@ -4229,8 +4367,7 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "User Grants"; - summary: "Remove User Grant"; - description: "Removes the user grant from the user. The user will not be able to use the granted project anymore. Also, the roles will not be included in the tokens when requested." + deprecated: true; parameters: { headers: { name: "x-zitadel-orgid"; @@ -4242,6 +4379,11 @@ service ManagementService { }; } + // Bulk Remove User Grants + // + // Deprecated: [Delete authorizations one after the other](apis/resources/authorization_service_v2/zitadel-authorization-v-2-beta-authorization-service-delete-authorization.api.mdx) to remove access for multiple users on multiple owned or granted projects. + // + // Remove a list of user grants. The users will not be able to use the granted project anymore. Also, the roles will not be included in the tokens when requested. rpc BulkRemoveUserGrant(BulkRemoveUserGrantRequest) returns (BulkRemoveUserGrantResponse) { option (google.api.http) = { delete: "/user_grants/_bulk" @@ -4254,8 +4396,7 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags: "User Grants"; - summary: "Bulk Remove User Grants"; - description: "Remove a list of user grants. The users will not be able to use the granted project anymore. Also, the roles will not be included in the tokens when requested." + deprecated: true; parameters: { headers: { name: "x-zitadel-orgid"; @@ -9209,7 +9350,7 @@ message AddOrgMemberRequest { repeated string roles = 2 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "[\"ORG_OWNER\"]"; - description: "If no roles are provided the user won't have any rights" + description: "If no roles are provided the user won't have any rights, so the member definition will be regarded as invalid." } ]; } @@ -9222,7 +9363,7 @@ message UpdateOrgMemberRequest { repeated string roles = 2 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "[\"IAM_OWNER\"]"; - description: "If no roles are provided the user won't have any rights" + description: "If no roles are provided the user won't have any rights, so the member definition will be regarded as invalid." } ]; } @@ -9643,7 +9784,7 @@ message AddProjectMemberRequest { repeated string roles = 3 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "[\"PROJECT_OWNER\"]"; - description: "If no roles are provided the user won't have any rights" + description: "If no roles are provided the user won't have any rights, so the member definition will be regarded as invalid." } ]; } @@ -9658,7 +9799,7 @@ message UpdateProjectMemberRequest { repeated string roles = 3 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "[\"PROJECT_OWNER\"]"; - description: "If no roles are provided the user won't have any rights" + description: "If no roles are provided the user won't have any rights, so the member definition will be regarded as invalid." } ]; } @@ -10313,7 +10454,7 @@ message AddProjectGrantMemberRequest { repeated string roles = 4 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "[\"PROJECT_GRANT_OWNER\"]"; - description: "If no roles are provided the user won't have any rights" + description: "If no roles are provided the user won't have any rights, so the member definition will be regarded as invalid." } ]; } @@ -10337,7 +10478,7 @@ message UpdateProjectGrantMemberRequest { repeated string roles = 4 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "[\"PROJECT_GRANT_OWNER\"]"; - description: "If no roles are provided the user won't have any rights" + description: "If no roles are provided the user won't have any rights, so the member definition will be regarded as invalid." } ]; } diff --git a/proto/zitadel/metadata/v2/metadata.proto b/proto/zitadel/metadata/v2/metadata.proto new file mode 100644 index 0000000000..c04548ba4e --- /dev/null +++ b/proto/zitadel/metadata/v2/metadata.proto @@ -0,0 +1,57 @@ +syntax = "proto3"; + +import "zitadel/filter/v2/filter.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; +import "google/protobuf/timestamp.proto"; + +package zitadel.metadata.v2; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/metadata/v2"; + +message Metadata { + google.protobuf.Timestamp creation_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; + google.protobuf.Timestamp change_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; + string key = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "metadata key", + example: "\"key1\""; + } + ]; + bytes value = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "metadata value is base64 encoded, make sure to decode to get the value", + example: "\"VGhpcyBpcyBteSBmaXJzdCB2YWx1ZQ==\""; + } + ]; +} + +message MetadataSearchFilter { + oneof filter { + option (validate.required) = true; + MetadataKeyFilter key_filter = 1; + } +} + +message MetadataKeyFilter { + string key = 1 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"key\"" + } + ]; + zitadel.filter.v2.TextFilterMethod method = 2 [ + (validate.rules).enum.defined_only = true, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "defines which text equality method is used"; + } + ]; +} \ No newline at end of file diff --git a/proto/zitadel/metadata/v2beta/metadata.proto b/proto/zitadel/metadata/v2beta/metadata.proto new file mode 100644 index 0000000000..87fcc51869 --- /dev/null +++ b/proto/zitadel/metadata/v2beta/metadata.proto @@ -0,0 +1,57 @@ +syntax = "proto3"; + +import "zitadel/object/v2beta/object.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; +import "google/protobuf/timestamp.proto"; + +package zitadel.metadata.v2beta; + +option go_package ="github.com/zitadel/zitadel/pkg/grpc/metadata/v2beta"; + +message Metadata { + google.protobuf.Timestamp creation_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; + google.protobuf.Timestamp change_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; + string key = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "metadata key", + example: "\"key1\""; + } + ]; + bytes value = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "metadata value is base64 encoded, make sure to decode to get the value", + example: "\"VGhpcyBpcyBteSBmaXJzdCB2YWx1ZQ==\""; + } + ]; +} + +message MetadataQuery { + oneof query { + option (validate.required) = true; + MetadataKeyQuery key_query = 1; + } +} + +message MetadataKeyQuery { + string key = 1 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"key\"" + } + ]; + zitadel.object.v2beta.TextQueryMethod method = 2 [ + (validate.rules).enum.defined_only = true, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "defines which text equality method is used"; + } + ]; +} diff --git a/proto/zitadel/org/v2/org_service.proto b/proto/zitadel/org/v2/org_service.proto index 94ced55146..729350e1f9 100644 --- a/proto/zitadel/org/v2/org_service.proto +++ b/proto/zitadel/org/v2/org_service.proto @@ -197,6 +197,14 @@ message AddOrganizationRequest{ } ]; repeated Admin admins = 2; + // optionally set your own id unique for the organization. + optional string org_id = 3 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + max_length: 200; + example: "\"d654e6ba-70a3-48ef-a95d-37c8d8a7901a\""; + } + ]; } message AddOrganizationResponse{ diff --git a/proto/zitadel/org/v2beta/org.proto b/proto/zitadel/org/v2beta/org.proto new file mode 100644 index 0000000000..08cf47e820 --- /dev/null +++ b/proto/zitadel/org/v2beta/org.proto @@ -0,0 +1,169 @@ +syntax = "proto3"; + +package zitadel.org.v2beta; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/org/v2beta;org"; + +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; +import "zitadel/object/v2beta/object.proto"; +import "google/protobuf/timestamp.proto"; + +message Organization { + // Unique identifier of the organization. + string id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\"" + } + ]; + + // The timestamp of the organization was created. + google.protobuf.Timestamp creation_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + + // The timestamp of the verification of the organization domain. + google.protobuf.Timestamp changed_date = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; + + // Current state of the organization, for example active, inactive and deleted. + OrgState state = 4; + + // Name of the organization. + string name = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"ZITADEL\""; + } + ]; + // Primary domain used in the organization. + string primary_domain = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"zitadel.cloud\""; + } + ]; +} + +enum OrgState { + ORG_STATE_UNSPECIFIED = 0; + ORG_STATE_ACTIVE = 1; + ORG_STATE_INACTIVE = 2; + ORG_STATE_REMOVED = 3; +} + +enum OrgFieldName { + ORG_FIELD_NAME_UNSPECIFIED = 0; + ORG_FIELD_NAME_NAME = 1; + ORG_FIELD_NAME_CREATION_DATE = 2; +} + +message OrganizationSearchFilter{ + oneof filter { + option (validate.required) = true; + + OrgNameFilter name_filter = 1; + OrgDomainFilter domain_filter = 2; + OrgStateFilter state_filter = 3; + OrgIDFilter id_filter = 4; + } +} +message OrgNameFilter { + // Organization name. + string name = 1 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"ZITADEL\""; + } + ]; + // Defines which text equality method is used. + zitadel.object.v2beta.TextQueryMethod method = 2 [ + (validate.rules).enum.defined_only = true + ]; +} + +message OrgDomainFilter { + // The domain. + string domain = 1 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"zitadel.cloud\""; + } + ]; + // Defines which text equality method is used. + zitadel.object.v2beta.TextQueryMethod method = 2 [ + (validate.rules).enum.defined_only = true + ]; +} + +message OrgStateFilter { + // Current state of the organization. + OrgState state = 1 [ + (validate.rules).enum.defined_only = true + ]; +} + +message OrgIDFilter { + // The Organization id. + string id = 1 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\"" + } + ]; +} + +// from proto/zitadel/org.proto +message DomainSearchFilter { + oneof filter { + option (validate.required) = true; + DomainNameFilter domain_name_filter = 1; + } +} + +// from proto/zitadel/org.proto +message DomainNameFilter { + // The domain. + string name = 1 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"zitadel.cloud\""; + } + ]; + // Defines which text equality method is used. + zitadel.object.v2beta.TextQueryMethod method = 2 [ + (validate.rules).enum.defined_only = true + ]; +} + +// from proto/zitadel/org.proto +message Domain { + // The Organization id. + string organization_id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\"" + } + ]; + // The domain name. + string domain_name = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"zitadel.com\""; + } + ]; + // Defines if the domain is verified. + bool is_verified = 3; + // Defines if the domain is the primary domain. + bool is_primary = 4; + // Defines the protocol the domain was validated with. + DomainValidationType validation_type = 5; +} + +// from proto/zitadel/org.proto +enum DomainValidationType { + DOMAIN_VALIDATION_TYPE_UNSPECIFIED = 0; + DOMAIN_VALIDATION_TYPE_HTTP = 1; + DOMAIN_VALIDATION_TYPE_DNS = 2; +} diff --git a/proto/zitadel/org/v2beta/org_service.proto b/proto/zitadel/org/v2beta/org_service.proto index 90c29ca354..43307214f5 100644 --- a/proto/zitadel/org/v2beta/org_service.proto +++ b/proto/zitadel/org/v2beta/org_service.proto @@ -6,24 +6,22 @@ package zitadel.org.v2beta; import "zitadel/object/v2beta/object.proto"; import "zitadel/protoc_gen_zitadel/v2/options.proto"; import "zitadel/user/v2beta/auth.proto"; -import "zitadel/user/v2beta/email.proto"; -import "zitadel/user/v2beta/phone.proto"; -import "zitadel/user/v2beta/idp.proto"; -import "zitadel/user/v2beta/password.proto"; -import "zitadel/user/v2beta/user.proto"; +import "zitadel/org/v2beta/org.proto"; +import "zitadel/metadata/v2beta/metadata.proto"; import "zitadel/user/v2beta/user_service.proto"; import "google/api/annotations.proto"; import "google/api/field_behavior.proto"; -import "google/protobuf/duration.proto"; import "google/protobuf/struct.proto"; import "protoc-gen-openapiv2/options/annotations.proto"; import "validate/validate.proto"; +import "google/protobuf/timestamp.proto"; +import "zitadel/filter/v2beta/filter.proto"; option go_package = "github.com/zitadel/zitadel/pkg/grpc/org/v2beta;org"; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { info: { - title: "User Service"; + title: "Organization Service (Beta)"; version: "2.0-beta"; description: "This API is intended to manage organizations in a ZITADEL instance. This project is in beta state. It can AND will continue breaking until the services provide the same functionality as the current login."; contact:{ @@ -111,8 +109,15 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { service OrganizationService { - // Create a new organization and grant the user(s) permission to manage it - rpc AddOrganization(AddOrganizationRequest) returns (AddOrganizationResponse) { + // Create Organization + // + // Create a new organization with an administrative user. If no specific roles are sent for the users, they will be granted the role ORG_OWNER. + // + // Required permission: + // - `org.create` + // + // Deprecated: Use [AddOrganization](/apis/resources/org_service_v2/organization-service-add-organization.api.mdx) instead to create an organization. + rpc CreateOrganization(CreateOrganizationRequest) returns (CreateOrganizationResponse) { option (google.api.http) = { post: "/v2beta/organizations" body: "*" @@ -122,34 +127,415 @@ service OrganizationService { auth_option: { permission: "org.create" } - http_response: { - success_code: 201 + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "Organization created successfully"; + }; + }; + responses: { + key: "409" + value: { + description: "The organization to create already exists."; + } + }; + deprecated: true; + }; + } + + // Update Organization + // + // Change the name of the organization. + // + // Required permission: + // - `org.write` + rpc UpdateOrganization(UpdateOrganizationRequest) returns (UpdateOrganizationResponse) { + option (google.api.http) = { + post: "/v2beta/organizations/{id}" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "org.write" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "Organization created successfully"; + }; + }; + responses: { + key: "404" + value: { + description: "Organisation's not found"; + } + }; + responses: { + key: "409" + value: { + description: "Organisation's name already taken"; + } + }; + }; + + } + + // List Organizations + // + // Returns a list of organizations that match the requesting filters. All filters are applied with an AND condition. + // + // Required permission: + // - `iam.read` + // + // Deprecated: Use [ListOrganizations](/apis/resources/org_service_v2/organization-service-list-organizations.api.mdx) instead to list organizations. + rpc ListOrganizations(ListOrganizationsRequest) returns (ListOrganizationsResponse) { + option (google.api.http) = { + post: "/v2beta/organizations/search"; + body: "*"; + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "iam.read"; } }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Create an Organization"; - description: "Create a new organization with an administrative user. If no specific roles are sent for the users, they will be granted the role ORG_OWNER." responses: { - key: "200" + key: "200"; + }; + deprecated: true; + }; + } + + // Delete Organization + // + // Deletes the organization and all its resources (Users, Projects, Grants to and from the org). Users of this organization will not be able to log in. + // + // Required permission: + // - `org.delete` + rpc DeleteOrganization(DeleteOrganizationRequest) returns (DeleteOrganizationResponse) { + option (google.api.http) = { + delete: "/v2beta/organizations/{id}" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "org.delete"; + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; value: { - description: "OK"; + description: "Organization created successfully"; + }; + }; + responses: { + key: "404" + value: { + description: "Organisation's not found"; } }; }; } + + // Set Organization Metadata + // + // Adds or updates a metadata value for the requested key. Make sure the value is base64 encoded. + // + // Required permission: + // - `org.write` + rpc SetOrganizationMetadata(SetOrganizationMetadataRequest) returns (SetOrganizationMetadataResponse) { + option (google.api.http) = { + post: "/v2beta/organizations/{organization_id}/metadata" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "org.write" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + }; + responses: { + // TODO This needs to chagne to 404 + key: "400" + value: { + description: "Organisation's not found"; + } + }; + }; + } + + // List Organization Metadata + // + // List metadata of an organization filtered by query. + // + // Required permission: + // - `org.read` + rpc ListOrganizationMetadata(ListOrganizationMetadataRequest) returns (ListOrganizationMetadataResponse ) { + option (google.api.http) = { + post: "/v2beta/organizations/{organization_id}/metadata/search" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { auth_option: { + permission: "org.read" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + }; + }; + + } + + // Delete Organization Metadata + // + // Delete metadata objects from an organization with a specific key. + // + // Required permission: + // - `org.write` + rpc DeleteOrganizationMetadata(DeleteOrganizationMetadataRequest) returns (DeleteOrganizationMetadataResponse) { + option (google.api.http) = { + delete: "/v2beta/organizations/{organization_id}/metadata" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "org.write" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + }; + }; + + } + + // Add Organization Domain + // + // Add a new domain to an organization. The domains are used to identify to which organization a user belongs. + // + // Required permission: + // - `org.write` + rpc AddOrganizationDomain(AddOrganizationDomainRequest) returns (AddOrganizationDomainResponse) { + option (google.api.http) = { + post: "/v2beta/organizations/{organization_id}/domains" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "org.write" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + }; + responses: { + key: "409" + value: { + description: "Domain already exists"; + } + }; + }; + + } + + // List Organization Domains + // + // Returns the list of registered domains of an organization. The domains are used to identify to which organization a user belongs. + // + // Required permission: + // - `org.read` + rpc ListOrganizationDomains(ListOrganizationDomainsRequest) returns (ListOrganizationDomainsResponse) { + option (google.api.http) = { + post: "/v2beta/organizations/{organization_id}/domains/search" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "org.read" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + }; + }; + + } + + // Delete Organization Domain + // + // Delete a new domain from an organization. The domains are used to identify to which organization a user belongs. If the uses use the domain for login, this will not be possible afterwards. They have to use another domain instead. + // + // Required permission: + // - `org.write` + rpc DeleteOrganizationDomain(DeleteOrganizationDomainRequest) returns (DeleteOrganizationDomainResponse) { + option (google.api.http) = { + delete: "/v2beta/organizations/{organization_id}/domains" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "org.write" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + }; + }; + + } + + // Generate Organization Domain Validation + // + // Generate a new file to be able to verify your domain with DNS or HTTP challenge. + // + // Required permission: + // - `org.write` + rpc GenerateOrganizationDomainValidation(GenerateOrganizationDomainValidationRequest) returns (GenerateOrganizationDomainValidationResponse) { + option (google.api.http) = { + post: "/v2beta/organizations/{organization_id}/domains/validation/generate" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "org.write" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + }; + responses: { + key: "404" + value: { + description: "Domain doesn't exist on organization"; + } + }; + }; + } + + // Verify Organization Domain + // + // Make sure you have added the required verification to your domain, depending on the method you have chosen (HTTP or DNS challenge). ZITADEL will check it and set the domain as verified if it was successful. A verify domain has to be unique. + // + // Required permission: + // - `org.write` + rpc VerifyOrganizationDomain(VerifyOrganizationDomainRequest) returns (VerifyOrganizationDomainResponse) { + option (google.api.http) = { + post: "/v2beta/organizations/{organization_id}/domains/validation/verify" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "org.write" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + }; + }; + } + + // Deactivate Organization + // + // Sets the state of my organization to deactivated. Users of this organization will not be able to log in. + // + // Required permission: + // - `org.write` + rpc DeactivateOrganization(DeactivateOrganizationRequest) returns (DeactivateOrganizationResponse) { + option (google.api.http) = { + post: "/v2beta/organizations/{id}/deactivate" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "org.write" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + }; + }; + + } + + // Activate Organization + // + // Set the state of my organization to active. The state of the organization has to be deactivated to perform the request. Users of this organization will be able to log in again. + // + // Required permission: + // - `org.write` + rpc ActivateOrganization(ActivateOrganizationRequest) returns (ActivateOrganizationResponse) { + option (google.api.http) = { + post: "/v2beta/organizations/{id}/activate" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "org.write" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + }; + }; + + } + + } -message AddOrganizationRequest{ +message CreateOrganizationRequest{ + // The Admin for the newly created Organization. message Admin { oneof user_type{ string user_id = 1; zitadel.user.v2beta.AddHumanUserRequest human = 2; } - // specify Org Member Roles for the provided user (default is ORG_OWNER if roles are empty) + // specify Organization Member Roles for the provided user (default is ORG_OWNER if roles are empty) repeated string roles = 3; } + // name of the Organization to be created. string name = 1 [ (validate.rules).string = {min_len: 1, max_len: 200}, (google.api.field_behavior) = REQUIRED, @@ -159,16 +545,417 @@ message AddOrganizationRequest{ example: "\"ZITADEL\""; } ]; - repeated Admin admins = 2; + // Optionally set your own id unique for the organization. + optional string id = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200 }, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629012906488334\""; + } + ]; + // Additional Admins for the Organization. + repeated Admin admins = 3; } -message AddOrganizationResponse{ - message CreatedAdmin { - string user_id = 1; - optional string email_code = 2; - optional string phone_code = 3; - } - zitadel.object.v2beta.Details details = 1; - string organization_id = 2; - repeated CreatedAdmin created_admins = 3; +message CreatedAdmin { + string user_id = 1; + optional string email_code = 2; + optional string phone_code = 3; } + +message AssignedAdmin { + string user_id = 1; +} + +message OrganizationAdmin { + // The admins created/assigned for the Organization. + oneof OrganizationAdmin { + CreatedAdmin created_admin = 1; + AssignedAdmin assigned_admin = 2; + } +} + +message CreateOrganizationResponse{ + // The timestamp of the organization was created. + google.protobuf.Timestamp creation_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + + // Organization ID of the newly created organization. + string id = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629012906488334\""; + } + ]; + + // The admins created/assigned for the Organization + repeated OrganizationAdmin organization_admins = 3; +} + +message UpdateOrganizationRequest { + // Organization Id for the Organization to be updated + string id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629012906488334\""; + } + ]; + + // New Name for the Organization to be updated + string name = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"Customer 1\""; + } + ]; +} + +message UpdateOrganizationResponse { + // The timestamp of the update to the organization. + google.protobuf.Timestamp change_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message ListOrganizationsRequest { + // List limitations and ordering. + optional zitadel.filter.v2beta.PaginationRequest pagination = 1; + // the field the result is sorted + zitadel.org.v2beta.OrgFieldName sorting_column = 2; + // Define the criteria to query for. + // repeated ProjectRoleQuery filters = 4; + repeated zitadel.org.v2beta.OrganizationSearchFilter filter = 3; +} + +message ListOrganizationsResponse { + // Pagination of the Organizations results + zitadel.filter.v2beta.PaginationResponse pagination = 1; + // The Organizations requested + repeated zitadel.org.v2beta.Organization organizations = 2; +} + +message DeleteOrganizationRequest { + + // Organization Id for the Organization to be deleted + string id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\""; + min_length: 1; + max_length: 200; + } + ]; +} + +message DeleteOrganizationResponse { + // The timestamp of the deletion of the organization. + google.protobuf.Timestamp deletion_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message DeactivateOrganizationRequest { + // Organization Id for the Organization to be deactivated + string id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\""; + min_length: 1; + max_length: 200; + } + ]; +} + +message DeactivateOrganizationResponse { + // The timestamp of the deactivation of the organization. + google.protobuf.Timestamp change_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message ActivateOrganizationRequest { + // Organization Id for the Organization to be activated + string id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\""; + min_length: 1; + max_length: 200; + } + ]; +} + +message ActivateOrganizationResponse { + // The timestamp of the activation of the organization. + google.protobuf.Timestamp change_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message AddOrganizationDomainRequest { + // Organization Id for the Organization for which the domain is to be added to. + string organization_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629012906488334\""; + } + ]; + // The domain you want to add to the organization. + string domain = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"testdomain.com\""; + } + ]; +} + +message AddOrganizationDomainResponse { + // The timestamp of the organization was created. + google.protobuf.Timestamp creation_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; +} + +message ListOrganizationDomainsRequest { + // Organization Id for the Organization which domains are to be listed. + string organization_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629012906488334\""; + } + ]; + + // List limitations and ordering. + optional zitadel.filter.v2beta.PaginationRequest pagination = 2; + // Define the criteria to query for. + repeated DomainSearchFilter filters = 3; +} + +message ListOrganizationDomainsResponse { + // Pagination of the Organizations domain results. + zitadel.filter.v2beta.PaginationResponse pagination = 1; + // The domains requested. + repeated Domain domains = 2; +} + +message DeleteOrganizationDomainRequest { + // Organization Id for the Organization which domain is to be deleted. + string organization_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629012906488334\""; + } + ]; + string domain = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"testdomain.com\""; + } + ]; +} + +message DeleteOrganizationDomainResponse { + // The timestamp of the deletion of the organization domain. + google.protobuf.Timestamp deletion_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message GenerateOrganizationDomainValidationRequest { + // Organization Id for the Organization which doman to be validated. + string organization_id = 1 [ + + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629012906488334\""; + } + ]; + // The domain which to be deleted. + string domain = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"testdomain.com\""; + } + ]; + DomainValidationType type = 3 [(validate.rules).enum = {defined_only: true, not_in: [0]}]; +} + +message GenerateOrganizationDomainValidationResponse { + // The token verify domain. + string token = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"ofSBHsSAVHAoTIE4Iv2gwhaYhTjcY5QX\""; + } + ]; + // URL used to verify the domain. + string url = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"https://testdomain.com/.well-known/zitadel-challenge/ofSBHsSAVHAoTIE4Iv2gwhaYhTjcY5QX\""; + } + ]; +} + +message VerifyOrganizationDomainRequest { + // Organization Id for the Organization doman to be verified. + string organization_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629012906488334\""; + } + ]; + // Organization Id for the Organization doman to be verified. + string domain = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"testdomain.com\""; + } + ]; +} + +message VerifyOrganizationDomainResponse { + // The timestamp of the verification of the organization domain. + google.protobuf.Timestamp change_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message Metadata { + // Key in the metadata key/value pair. + string key = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + // Value in the metadata key/value pair. + bytes value = 2 [(validate.rules).bytes = {min_len: 1, max_len: 500000}]; +} +message SetOrganizationMetadataRequest{ + // Organization Id for the Organization doman to be verified. + string organization_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629012906488334\""; + } + ]; + // Metadata to set. + repeated Metadata metadata = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + title: "Medata (Key/Value)" + description: "The values have to be base64 encoded."; + example: "[{\"key\": \"test1\", \"value\": \"VGhpcyBpcyBteSBmaXJzdCB2YWx1ZQ==\"}, {\"key\": \"test2\", \"value\": \"VGhpcyBpcyBteSBzZWNvbmQgdmFsdWU=\"}]" + } + ]; +} + +message SetOrganizationMetadataResponse{ + // The timestamp of the update of the organization metadata. + google.protobuf.Timestamp set_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message ListOrganizationMetadataRequest { + // Organization ID of Orgalization which metadata is to be listed. + string organization_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629012906488334\""; + } + ]; + + // List limitations and ordering. + optional zitadel.filter.v2beta.PaginationRequest pagination = 2; + // Define the criteria to query for. + repeated zitadel.metadata.v2beta.MetadataQuery filter = 3; +} + +message ListOrganizationMetadataResponse { + // Pagination of the Organizations metadata results. + zitadel.filter.v2beta.PaginationResponse pagination = 1; + // The Organization metadata requested. + repeated zitadel.metadata.v2beta.Metadata metadata = 2; +} + +message DeleteOrganizationMetadataRequest { + // Organization ID of Orgalization which metadata is to be deleted is stored on. + string organization_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629012906488334\""; + } + ]; + // The keys for the Organization metadata to be deleted. + repeated string keys = 2 [(validate.rules).repeated.items.string = {min_len: 1, max_len: 200}]; +} + +message DeleteOrganizationMetadataResponse{ + // The timestamp of the deletiion of the organization metadata. + google.protobuf.Timestamp deletion_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + + diff --git a/proto/zitadel/project/v2beta/project_service.proto b/proto/zitadel/project/v2beta/project_service.proto new file mode 100644 index 0000000000..66f221b911 --- /dev/null +++ b/proto/zitadel/project/v2beta/project_service.proto @@ -0,0 +1,1237 @@ +syntax = "proto3"; + +package zitadel.project.v2beta; + +import "google/api/annotations.proto"; +import "google/api/field_behavior.proto"; +import "google/protobuf/duration.proto"; +import "google/protobuf/struct.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; + +import "zitadel/protoc_gen_zitadel/v2/options.proto"; + +import "zitadel/project/v2beta/query.proto"; +import "google/protobuf/timestamp.proto"; +import "zitadel/filter/v2beta/filter.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/project/v2beta;project"; + +option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { + info: { + title: "Project Service"; + version: "2.0-beta"; + description: "This API is intended to manage Projects in a ZITADEL Organization. This service is in beta state. It can AND will continue breaking until a stable version is released."; + contact:{ + name: "ZITADEL" + url: "https://zitadel.com" + email: "hi@zitadel.com" + } + license: { + name: "Apache 2.0", + url: "https://github.com/zitadel/zitadel/blob/main/LICENSING.md"; + }; + }; + schemes: HTTPS; + schemes: HTTP; + + consumes: "application/json"; + consumes: "application/grpc"; + + produces: "application/json"; + produces: "application/grpc"; + + consumes: "application/grpc-web+proto"; + produces: "application/grpc-web+proto"; + + host: "$CUSTOM-DOMAIN"; + base_path: "/"; + + external_docs: { + description: "Detailed information about ZITADEL", + url: "https://zitadel.com/docs" + } + security_definitions: { + security: { + key: "OAuth2"; + value: { + type: TYPE_OAUTH2; + flow: FLOW_ACCESS_CODE; + authorization_url: "$CUSTOM-DOMAIN/oauth/v2/authorize"; + token_url: "$CUSTOM-DOMAIN/oauth/v2/token"; + scopes: { + scope: { + key: "openid"; + value: "openid"; + } + scope: { + key: "urn:zitadel:iam:org:project:id:zitadel:aud"; + value: "urn:zitadel:iam:org:project:id:zitadel:aud"; + } + } + } + } + } + security: { + security_requirement: { + key: "OAuth2"; + value: { + scope: "openid"; + scope: "urn:zitadel:iam:org:project:id:zitadel:aud"; + } + } + } + responses: { + key: "403"; + value: { + description: "Returned when the user does not have permission to access the resource."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + } + } + } + } + responses: { + key: "404"; + value: { + description: "Returned when the resource does not exist."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + } + } + } + } +}; + +// Service to manage projects. +service ProjectService { + + // Create Project + // + // Create a new Project. + // + // Required permission: + // - `project.create` + rpc CreateProject (CreateProjectRequest) returns (CreateProjectResponse) { + option (google.api.http) = { + post: "/v2beta/projects" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "Project created successfully"; + }; + }; + responses: { + key: "409" + value: { + description: "The project to create already exists."; + } + }; + }; + } + + // Update Project + // + // Update an existing project. + // + // Required permission: + // - `project.write` + rpc UpdateProject (UpdateProjectRequest) returns (UpdateProjectResponse) { + option (google.api.http) = { + post: "/v2beta/projects/{id}" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "Project successfully updated or left unchanged"; + }; + }; + responses: { + key: "404" + value: { + description: "The project to update does not exist."; + } + }; + }; + } + + // Delete Project + // + // Delete an existing project. + // In case the project is not found, the request will return a successful response as + // the desired state is already achieved. + // + // Required permission: + // - `project.delete` + rpc DeleteProject (DeleteProjectRequest) returns (DeleteProjectResponse) { + option (google.api.http) = { + delete: "/v2beta/projects/{id}" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "Project deleted successfully"; + }; + }; + }; + } + + // Get Project + // + // Returns the project identified by the requested ID. + // + // Required permission: + // - `project.read` + rpc GetProject (GetProjectRequest) returns (GetProjectResponse) { + option (google.api.http) = { + get: "/v2beta/projects/{id}" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "project.read" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200" + value: { + description: "Project retrieved successfully"; + } + }; + responses: { + key: "404" + value: { + description: "The project to get does not exist."; + } + }; + }; + } + + // List Projects + // + // List all matching projects. By default all projects of the instance that the caller has permission to read are returned. + // Make sure to include a limit and sorting for pagination. + // + // Required permission: + // - `project.read` + rpc ListProjects (ListProjectsRequest) returns (ListProjectsResponse) { + option (google.api.http) = { + post: "/v2beta/projects/search", + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "project.read" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "A list of all projects matching the query"; + }; + }; + responses: { + key: "400"; + value: { + description: "invalid list query"; + }; + }; + }; + } + + // Deactivate Project + // + // Set the state of a project to deactivated. Request returns no error if the project is already deactivated. + // Applications under deactivated projects are not able to login anymore. + // + // Required permission: + // - `project.write` + rpc DeactivateProject (DeactivateProjectRequest) returns (DeactivateProjectResponse) { + option (google.api.http) = { + post: "/v2beta/projects/{id}/deactivate" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "Project deactivated successfully"; + }; + }; + responses: { + key: "404" + value: { + description: "The project to deactivate does not exist."; + } + }; + }; + } + + // Activate Project + // + // Set the state of a project to active. Request returns no error if the project is already activated. + // + // Required permission: + // - `project.write` + rpc ActivateProject (ActivateProjectRequest) returns (ActivateProjectResponse) { + option (google.api.http) = { + post: "/v2beta/projects/{id}/activate" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "Project activated successfully"; + }; + }; + responses: { + key: "404" + value: { + description: "The project to activate does not exist."; + } + }; + }; + } + + // Add Project Role + // + // Add a new project role to a project. The key must be unique within the project. + // + // Required permission: + // - `project.role.write` + rpc AddProjectRole (AddProjectRoleRequest) returns (AddProjectRoleResponse) { + option (google.api.http) = { + post: "/v2beta/projects/{project_id}/roles" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "Project role added successfully"; + }; + }; + responses: { + key: "404" + value: { + description: "The project to add the project role does not exist."; + } + }; + }; + } + + // Update Project Role + // + // Change a project role. The key is not editable. If a key should change, remove the role and create a new one. + // + // Required permission: + // - `project.role.write` + rpc UpdateProjectRole (UpdateProjectRoleRequest) returns (UpdateProjectRoleResponse) { + option (google.api.http) = { + post: "/v2beta/projects/{project_id}/roles/{role_key}" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "Project role updated successfully"; + }; + }; + responses: { + key: "404" + value: { + description: "The project role to update does not exist."; + } + }; + }; + } + + // Remove Project Role + // + // Removes the role from the project and on every resource it has a dependency. This includes project grants and user grants. + // + // Required permission: + // - `project.role.write` + rpc RemoveProjectRole (RemoveProjectRoleRequest) returns (RemoveProjectRoleResponse) { + option (google.api.http) = { + delete: "/v2beta/projects/{project_id}/roles/{role_key}" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "Project role removed successfully"; + }; + }; + responses: { + key: "404" + value: { + description: "The project role to remove does not exist."; + } + }; + }; + } + + // List Project Roles + // + // Returns all roles of a project matching the search query. + // + // Required permission: + // - `project.role.read` + rpc ListProjectRoles (ListProjectRolesRequest) returns (ListProjectRolesResponse) { + option (google.api.http) = { + post: "/v2beta/projects/{project_id}/roles/search" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "project.role.read" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "A list of all project roles matching the query"; + }; + }; + responses: { + key: "400"; + value: { + description: "invalid list query"; + }; + }; + }; + } + + // Create Project Grant + // + // Grant a project to another organization. + // The project grant will allow the granted organization to access the project and manage the authorizations for its users. + // + // Required permission: + // - `project.grant.create` + rpc CreateProjectGrant (CreateProjectGrantRequest) returns (CreateProjectGrantResponse) { + option (google.api.http) = { + post: "/v2beta/projects/{project_id}/grants" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "Project grant created successfully"; + }; + }; + responses: { + key: "409" + value: { + description: "The project grant to create already exists."; + } + }; + }; + } + + // Update Project Grant + // + // Change the roles of the project that is granted to another organization. + // The project grant will allow the granted organization to access the project and manage the authorizations for its users. + // + // Required permission: + // - `project.grant.write` + rpc UpdateProjectGrant (UpdateProjectGrantRequest) returns (UpdateProjectGrantResponse) { + option (google.api.http) = { + post: "/v2beta/projects/{project_id}/grants/{granted_organization_id}" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "Project grant successfully updated or left unchanged"; + }; + }; + responses: { + key: "404" + value: { + description: "The project grant to update does not exist."; + } + }; + }; + } + + // Delete Project Grant + // + // Delete a project grant. All user grants for this project grant will also be removed. + // A user will not have access to the project afterward (if permissions are checked). + // In case the project grant is not found, the request will return a successful response as + // the desired state is already achieved. + // + // Required permission: + // - `project.grant.delete` + rpc DeleteProjectGrant (DeleteProjectGrantRequest) returns (DeleteProjectGrantResponse) { + option (google.api.http) = { + delete: "/v2beta/projects/{project_id}/grants/{granted_organization_id}" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "Project grant deleted successfully"; + }; + }; + }; + } + + // Deactivate Project Grant + // + // Set the state of the project grant to deactivated. + // Applications under deactivated projects grants are not able to login anymore. + // + // Required permission: + // - `project.grant.write` + rpc DeactivateProjectGrant(DeactivateProjectGrantRequest) returns (DeactivateProjectGrantResponse) { + option (google.api.http) = { + post: "/v2beta/projects/{project_id}/grants/{granted_organization_id}/deactivate" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "Project grant deactivated successfully"; + }; + }; + }; + } + + // Activate Project Grant + // + // Set the state of the project grant to activated. + // + // Required permission: + // - `project.grant.write` + rpc ActivateProjectGrant(ActivateProjectGrantRequest) returns (ActivateProjectGrantResponse) { + option (google.api.http) = { + post: "/v2beta/projects/{project_id}/grants/{granted_organization_id}/activate" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "Project grant activated successfully"; + }; + }; + }; + } + + // List Project Grants + // + // Returns a list of project grants. A project grant is when the organization grants its project to another organization. + // + // Required permission: + // - `project.grant.write` + rpc ListProjectGrants(ListProjectGrantsRequest) returns (ListProjectGrantsResponse) { + option (google.api.http) = { + post: "/v2beta/projects/grants/search" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "project.grant.read" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "A list of all project grants matching the query"; + }; + }; + responses: { + key: "400"; + value: { + description: "invalid list query"; + }; + }; + }; + } +} + +message CreateProjectRequest { + // The unique identifier of the organization the project belongs to. + string organization_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"69629026806489455\""; + } + ]; + // The unique identifier of the project. + optional string id = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"69629026806489455\""; + } + ]; + // Name of the project. + string name = 3 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"MyProject\""; + } + ]; + // Enable this setting to have role information included in the user info endpoint. It is also dependent on your application settings to include it in tokens and other types. + bool project_role_assertion = 4; + // When enabled ZITADEL will check if a user has an authorization to use this project assigned when login into an application of this project. + bool authorization_required = 5; + // When enabled ZITADEL will check if the organization of the user, that is trying to log in, has access to this project (either owns the project or is granted). + bool project_access_required = 6; + // Define which private labeling/branding should trigger when getting to a login of this project. + PrivateLabelingSetting private_labeling_setting = 7 [ + (validate.rules).enum = {defined_only: true} + ]; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + example: "{\"organizationId\":\"69629026806489455\",\"name\":\"MyProject\",\"projectRoleAssertion\":true,\"projectRoleCheck\":true,\"hasProjectCheck\":true,\"privateLabelingSetting\":\"PRIVATE_LABELING_SETTING_UNSPECIFIED\"}"; + }; +} + +message CreateProjectResponse { + // The unique identifier of the newly created project. + string id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629012906488334\""; + } + ]; + // The timestamp of the project creation. + google.protobuf.Timestamp creation_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; +} + +message UpdateProjectRequest { + string id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"69629026806489455\""; + } + ]; + // Name of the project. + optional string name = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"MyProject-Updated\""; + } + ]; + // Enable this setting to have role information included in the user info endpoint. It is also dependent on your application settings to include it in tokens and other types. + optional bool project_role_assertion = 3; + // When enabled ZITADEL will check if a user has a role of this project assigned when login into an application of this project. + optional bool project_role_check = 4; + // When enabled ZITADEL will check if the organization of the user, that is trying to log in, has a grant to this project. + optional bool has_project_check = 5; + // Define which private labeling/branding should trigger when getting to a login of this project. + optional PrivateLabelingSetting private_labeling_setting = 6 [ + (validate.rules).enum = {defined_only: true} + ]; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + example: "{\"name\":\"MyProject-Updated\",\"projectRoleAssertion\":true,\"projectRoleCheck\":true,\"hasProjectCheck\":true,\"privateLabelingSetting\":\"PRIVATE_LABELING_SETTING_UNSPECIFIED\"}"; + }; +} + +message UpdateProjectResponse { + // The timestamp of the change of the project. + google.protobuf.Timestamp change_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message DeleteProjectRequest { + // The unique identifier of the project. + string id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"69629026806489455\""; + } + ]; +} + +message DeleteProjectResponse { + // The timestamp of the deletion of the project. + // Note that the deletion date is only guaranteed to be set if the deletion was successful during the request. + // In case the deletion occurred in a previous request, the deletion date might be empty. + google.protobuf.Timestamp deletion_date = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message GetProjectRequest { + // The unique identifier of the project. + string id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"69629026806489455\""; + } + ]; +} + +message GetProjectResponse { + Project project = 1; +} + +message ListProjectsRequest { + // List limitations and ordering. + optional zitadel.filter.v2beta.PaginationRequest pagination = 1; + // The field the result is sorted by. The default is the creation date. Beware that if you change this, your result pagination might be inconsistent. + optional ProjectFieldName sorting_column = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"PROJECT_FIELD_NAME_CREATION_DATE\"" + } + ]; + // Define the criteria to query for. + repeated ProjectSearchFilter filters = 3; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + example: "{\"pagination\":{\"offset\":0,\"limit\":0,\"asc\":true},\"sortingColumn\":\"PROJECT_FIELD_NAME_CREATION_DATE\",\"filters\":[{\"projectNameFilter\":{\"projectName\":\"MyProject\",\"method\":\"TEXT_FILTER_METHOD_EQUALS\"}},{\"inProjectIdsFilter\":{\"projectIds\":[\"69629023906488334\",\"69622366012355662\"]}}]}"; + }; +} + +message ListProjectsResponse { + zitadel.filter.v2beta.PaginationResponse pagination = 1; + repeated Project projects = 2; +} + +message DeactivateProjectRequest { + // The unique identifier of the project. + string id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"69629026806489455\""; + } + ]; +} + +message DeactivateProjectResponse { + // The timestamp of the change of the project. + google.protobuf.Timestamp change_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message ActivateProjectRequest { + // The unique identifier of the project. + string id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"69629026806489455\""; + } + ]; +} + +message ActivateProjectResponse { + // The timestamp of the change of the project. + google.protobuf.Timestamp change_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message AddProjectRoleRequest { + // ID of the project. + string project_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629026806489455\""; + } + ]; + // The key is the only relevant attribute for ZITADEL regarding the authorization checks. + string role_key = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"ADMIN\""; + } + ]; + // Name displayed for the role. + string display_name = 3 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"Administrator\""; + } + ]; + // The group is only used for display purposes. That you have better handling, like giving all the roles from a group to a user. + optional string group = 4 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + max_length: 200; + example: "\"Admins\""; + } + ]; +} + +message AddProjectRoleResponse { + // The timestamp of the project role creation. + google.protobuf.Timestamp creation_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; +} + +message UpdateProjectRoleRequest { + // ID of the project. + string project_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629026806489455\""; + } + ]; + // The key is the only relevant attribute for ZITADEL regarding the authorization checks. + string role_key = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"ADMIN\""; + } + ]; + // Name displayed for the role. + optional string display_name = 3 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"Administrator\""; + } + ]; + // The group is only used for display purposes. That you have better handling, like giving all the roles from a group to a user. + optional string group = 4 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + max_length: 200; + example: "\"Admins\""; + } + ]; +} + +message UpdateProjectRoleResponse { + // The timestamp of the change of the project role. + google.protobuf.Timestamp change_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message RemoveProjectRoleRequest { + // ID of the project. + string project_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629026806489455\""; + } + ]; + // The key is the only relevant attribute for ZITADEL regarding the authorization checks. + string role_key = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"ADMIN\""; + } + ]; +} + +message RemoveProjectRoleResponse { + // The timestamp of the removal of the project role. + // Note that the removal date is only guaranteed to be set if the removal was successful during the request. + // In case the removal occurred in a previous request, the removal date might be empty. + google.protobuf.Timestamp removal_date = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message ListProjectRolesRequest { + // ID of the project. + string project_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629026806489455\""; + } + ]; + // List limitations and ordering. + optional zitadel.filter.v2beta.PaginationRequest pagination = 2; + // The field the result is sorted by. The default is the creation date. Beware that if you change this, your result pagination might be inconsistent. + optional ProjectRoleFieldName sorting_column = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"PROJECT_ROLE_FIELD_NAME_CREATION_DATE\"" + } + ]; + // Define the criteria to query for. + repeated ProjectRoleSearchFilter filters = 4; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + example: "{\"pagination\":{\"offset\":0,\"limit\":0,\"asc\":true},\"sortingColumn\":\"PROJECT_ROLE_FIELD_NAME_CREATION_DATE\",\"filters\":[{\"keyFilter\":{\"key\":\"role.super.man\",\"method\":\"TEXT_FILTER_METHOD_EQUALS\"}},{\"displayNameFilter\":{\"displayName\":\"SUPER\"}}]}"; + }; +} + +message ListProjectRolesResponse { + zitadel.filter.v2beta.PaginationResponse pagination = 1; + repeated ProjectRole project_roles = 2; +} + + +message CreateProjectGrantRequest { + // ID of the project. + string project_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629026806489455\""; + } + ]; + // Organization the project is granted to. + string granted_organization_id = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"28746028909593987\"" + } + ]; + // Keys of the role available for the project grant. + repeated string role_keys = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[\"RoleKey1\", \"RoleKey2\"]"; + } + ]; +} + +message CreateProjectGrantResponse { + // The timestamp of the project grant creation. + google.protobuf.Timestamp creation_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; +} + +message UpdateProjectGrantRequest { + // ID of the project. + string project_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629026806489455\""; + } + ]; + // Organization the project is granted to. + string granted_organization_id = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"28746028909593987\"" + } + ]; + // Keys of the role available for the project grant. + repeated string role_keys = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[\"RoleKey1\", \"RoleKey2\"]"; + } + ]; +} + +message UpdateProjectGrantResponse { + // The timestamp of the change of the project grant. + google.protobuf.Timestamp change_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message DeleteProjectGrantRequest { + // ID of the project. + string project_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629026806489455\""; + } + ]; + // Organization the project is granted to. + string granted_organization_id = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"28746028909593987\"" + } + ]; +} + +message DeleteProjectGrantResponse { + // The timestamp of the deletion of the project grant. + // Note that the deletion date is only guaranteed to be set if the deletion was successful during the request. + // In case the deletion occurred in a previous request, the deletion date might be empty. + google.protobuf.Timestamp deletion_date = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message DeactivateProjectGrantRequest { + // ID of the project. + string project_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629026806489455\""; + } + ]; + // Organization the project is granted to. + string granted_organization_id = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"28746028909593987\"" + } + ]; +} + +message DeactivateProjectGrantResponse { + // The timestamp of the change of the project grant. + google.protobuf.Timestamp change_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message ActivateProjectGrantRequest { + // ID of the project. + string project_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629026806489455\""; + } + ]; + // Organization the project is granted to. + string granted_organization_id = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"28746028909593987\"" + } + ]; +} + +message ActivateProjectGrantResponse { + // The timestamp of the change of the project grant. + google.protobuf.Timestamp change_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message ListProjectGrantsRequest { + // List limitations and ordering. + optional zitadel.filter.v2beta.PaginationRequest pagination = 1; + // The field the result is sorted by. The default is the creation date. Beware that if you change this, your result pagination might be inconsistent. + optional ProjectGrantFieldName sorting_column = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"PROJECT_GRANT_FIELD_NAME_CREATION_DATE\"" + } + ]; + // Define the criteria to query for. + repeated ProjectGrantSearchFilter filters = 3; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + example: "{\"pagination\":{\"offset\":0,\"limit\":0,\"asc\":true},\"filters\":[{\"projectNameFilter\":{\"projectName\":\"MyProject\",\"method\":\"TEXT_FILTER_METHOD_EQUALS\"}},{\"inProjectIdsFilter\":{\"projectIds\":[\"69629023906488334\",\"69622366012355662\"]}}]}"; + }; +} + +message ListProjectGrantsResponse { + zitadel.filter.v2beta.PaginationResponse pagination = 1; + repeated ProjectGrant project_grants = 2; +} \ No newline at end of file diff --git a/proto/zitadel/project/v2beta/query.proto b/proto/zitadel/project/v2beta/query.proto new file mode 100644 index 0000000000..9bfde662a3 --- /dev/null +++ b/proto/zitadel/project/v2beta/query.proto @@ -0,0 +1,297 @@ +syntax = "proto3"; + +package zitadel.project.v2beta; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/project/v2beta;project"; + +import "google/api/field_behavior.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; +import "google/protobuf/timestamp.proto"; + +import "zitadel/filter/v2beta/filter.proto"; + +message ProjectGrant { + // The unique identifier of the organization which granted the project to the granted_organization_id. + string organization_id = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629012906488334\""; + } + ]; + // The timestamp of the granted project creation. + google.protobuf.Timestamp creation_date = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + // The timestamp of the last change to the granted project (e.g. creation, activation, deactivation). + google.protobuf.Timestamp change_date = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; + // The ID of the organization the project is granted to. + string granted_organization_id = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\"" + } + ]; + // The name of the organization the project is granted to. + string granted_organization_name = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"Some Organization\"" + } + ]; + // The roles of the granted project. + repeated string granted_role_keys = 7 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[\"role.super.man\"]" + } + ]; + // The ID of the granted project. + string project_id = 8 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\"" + } + ]; + // The name of the granted project. + string project_name = 9 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"ZITADEL\"" + } + ]; + // Describes the current state of the granted project. + ProjectGrantState state = 10 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "current state of the project"; + } + ]; +} + +enum ProjectGrantState { + PROJECT_GRANT_STATE_UNSPECIFIED = 0; + PROJECT_GRANT_STATE_ACTIVE = 1; + PROJECT_GRANT_STATE_INACTIVE = 2; +} + +message Project { + // The unique identifier of the project. + string id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629012906488334\""; + } + ]; + // The unique identifier of the organization the project belongs to. + string organization_id = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629012906488334\""; + } + ]; + // The timestamp of the project creation. + google.protobuf.Timestamp creation_date = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + // The timestamp of the last change to the project (e.g. creation, activation, deactivation). + google.protobuf.Timestamp change_date = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; + // The name of the project. + string name = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"ip_allow_list\""; + } + ]; + // Describes the current state of the project. + ProjectState state = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "current state of the project"; + } + ]; + // Describes if the roles of the user should be added to the token. + bool project_role_assertion = 7; + // When enabled ZITADEL will check if a user has an authorization to use this project assigned when login into an application of this project. + bool authorization_required = 8; + // When enabled ZITADEL will check if the organization of the user, that is trying to log in, has access to this project (either owns the project or is granted). + bool project_access_required = 9; + // Defines from where the private labeling should be triggered. + PrivateLabelingSetting private_labeling_setting = 10; + + // The ID of the organization the project is granted to. + optional string granted_organization_id = 12 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\"" + } + ]; + // The name of the organization the project is granted to. + optional string granted_organization_name = 13 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"Some Organization\"" + } + ]; + // Describes the current state of the granted project. + GrantedProjectState granted_state = 14 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "current state of the granted project"; + } + ]; +} + +enum ProjectState { + PROJECT_STATE_UNSPECIFIED = 0; + PROJECT_STATE_ACTIVE = 1; + PROJECT_STATE_INACTIVE = 2; +} + +enum GrantedProjectState { + GRANTED_PROJECT_STATE_UNSPECIFIED = 0; + GRANTED_PROJECT_STATE_ACTIVE = 1; + GRANTED_PROJECT_STATE_INACTIVE = 2; +} + +enum PrivateLabelingSetting { + PRIVATE_LABELING_SETTING_UNSPECIFIED = 0; + PRIVATE_LABELING_SETTING_ENFORCE_PROJECT_RESOURCE_OWNER_POLICY = 1; + PRIVATE_LABELING_SETTING_ALLOW_LOGIN_USER_RESOURCE_OWNER_POLICY = 2; +} + +enum ProjectFieldName { + PROJECT_FIELD_NAME_UNSPECIFIED = 0; + PROJECT_FIELD_NAME_ID = 1; + PROJECT_FIELD_NAME_CREATION_DATE = 2; + PROJECT_FIELD_NAME_CHANGE_DATE = 3; + PROJECT_FIELD_NAME_NAME = 4; +} + +enum ProjectGrantFieldName { + PROJECT_GRANT_FIELD_NAME_UNSPECIFIED = 0; + PROJECT_GRANT_FIELD_NAME_PROJECT_ID = 1; + PROJECT_GRANT_FIELD_NAME_CREATION_DATE = 2; + PROJECT_GRANT_FIELD_NAME_CHANGE_DATE = 3; +} + +enum ProjectRoleFieldName { + PROJECT_ROLE_FIELD_NAME_UNSPECIFIED = 0; + PROJECT_ROLE_FIELD_NAME_KEY = 1; + PROJECT_ROLE_FIELD_NAME_CREATION_DATE = 2; + PROJECT_ROLE_FIELD_NAME_CHANGE_DATE = 3; +} + +message ProjectSearchFilter { + oneof filter { + option (validate.required) = true; + + ProjectNameFilter project_name_filter = 1; + zitadel.filter.v2beta.InIDsFilter in_project_ids_filter = 2; + zitadel.filter.v2beta.IDFilter project_resource_owner_filter = 3; + zitadel.filter.v2beta.IDFilter project_grant_resource_owner_filter = 4; + zitadel.filter.v2beta.IDFilter project_organization_id_filter = 5; + } +} + +message ProjectNameFilter { + // Defines the name of the project to query for. + string project_name = 1 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + max_length: 200; + example: "\"ip_allow_list\""; + } + ]; + // Defines which text comparison method used for the name query. + zitadel.filter.v2beta.TextFilterMethod method = 2 [ + (validate.rules).enum.defined_only = true, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "defines which text equality method is used"; + } + ]; +} + +message ProjectGrantSearchFilter { + oneof filter { + option (validate.required) = true; + + ProjectNameFilter project_name_filter = 1; + ProjectRoleKeyFilter role_key_filter = 2; + zitadel.filter.v2beta.InIDsFilter in_project_ids_filter = 3; + zitadel.filter.v2beta.IDFilter project_resource_owner_filter = 4; + zitadel.filter.v2beta.IDFilter project_grant_resource_owner_filter = 5; + } +} + +message ProjectRole { + // ID of the project. + string project_id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629026806489455\""; + } + ]; + // Key of the project role. + string key = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"role.super.man\"" + } + ]; + // The timestamp of the project role creation. + google.protobuf.Timestamp creation_date = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + // The timestamp of the last change to the project role. + google.protobuf.Timestamp change_date = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; + // Display name of the project role. + string display_name = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"Super man\"" + } + ]; + // Group of the project role. + string group = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"people\"" + } + ]; +} + +message ProjectRoleSearchFilter { + oneof filter { + option (validate.required) = true; + + ProjectRoleKeyFilter role_key_filter = 1; + ProjectRoleDisplayNameFilter display_name_filter = 2; + } +} + +message ProjectRoleKeyFilter { + string key = 1 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"role.super.man\"" + } + ]; + // Defines which text comparison method used for the name query. + zitadel.filter.v2beta.TextFilterMethod method = 2 [ + (validate.rules).enum.defined_only = true + ]; +} + +message ProjectRoleDisplayNameFilter { + string display_name = 1 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"SUPER\"" + } + ]; + // Defines which text comparison method used for the name query. + zitadel.filter.v2beta.TextFilterMethod method = 2 [ + (validate.rules).enum.defined_only = true + ]; +} diff --git a/proto/zitadel/settings/v2/settings.proto b/proto/zitadel/settings/v2/settings.proto index b3ca5b5ca5..c797d27965 100644 --- a/proto/zitadel/settings/v2/settings.proto +++ b/proto/zitadel/settings/v2/settings.proto @@ -10,4 +10,4 @@ enum ResourceOwnerType { RESOURCE_OWNER_TYPE_UNSPECIFIED = 0; RESOURCE_OWNER_TYPE_INSTANCE = 1; RESOURCE_OWNER_TYPE_ORG = 2; -} +} \ No newline at end of file diff --git a/proto/zitadel/settings/v2/settings_service.proto b/proto/zitadel/settings/v2/settings_service.proto index 7f71e08da4..0a1f13e7e7 100644 --- a/proto/zitadel/settings/v2/settings_service.proto +++ b/proto/zitadel/settings/v2/settings_service.proto @@ -15,6 +15,8 @@ import "google/api/annotations.proto"; import "google/api/field_behavior.proto"; import "protoc-gen-openapiv2/options/annotations.proto"; import "validate/validate.proto"; +import "google/protobuf/struct.proto"; +import "zitadel/settings/v2/settings.proto"; option go_package = "github.com/zitadel/zitadel/pkg/grpc/settings/v2;settings"; @@ -362,6 +364,69 @@ service SettingsService { description: "Set the security settings of the ZITADEL instance." }; } + + // Get Hosted Login Translation + // + // Returns the translations in the requested locale for the hosted login. + // The translations returned are based on the input level specified (system, instance or organization). + // + // If the requested level doesn't contain all translations, and ignore_inheritance is set to false, + // a merging process fallbacks onto the higher levels ensuring all keys in the file have a translation, + // which could be in the default language if the one of the locale is missing on all levels. + // + // The etag returned in the response represents the hash of the translations as they are stored on DB + // and its reliable only if ignore_inheritance = true. + // + // Required permissions: + // - `iam.policy.read` + rpc GetHostedLoginTranslation(GetHostedLoginTranslationRequest) returns (GetHostedLoginTranslationResponse) { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "The localized translations."; + } + }; + }; + + option (google.api.http) = { + get: "/v2/settings/hosted_login_translation" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "iam.policy.read" + } + }; + } + + // Set Hosted Login Translation + // + // Sets the input translations at the specified level (instance or organization) for the input language. + // + // Required permissions: + // - `iam.policy.write` + rpc SetHostedLoginTranslation(SetHostedLoginTranslationRequest) returns (SetHostedLoginTranslationResponse) { + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "The translations was successfully set."; + } + }; + }; + + option (google.api.http) = { + put: "/v2/settings/hosted_login_translation"; + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "iam.policy.write" + } + }; + } } message GetLoginSettingsRequest { @@ -480,4 +545,76 @@ message SetSecuritySettingsRequest{ message SetSecuritySettingsResponse{ zitadel.object.v2.Details details = 1; +} + +message GetHostedLoginTranslationRequest { + oneof level { + bool system = 1 [(validate.rules).bool = {const: true}]; + bool instance = 2 [(validate.rules).bool = {const: true}]; + string organization_id = 3; + } + + string locale = 4 [ + (validate.rules).string = {min_len: 2}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 2; + example: "\"fr-FR\""; + } + ]; + + // if set to true, higher levels are ignored, if false higher levels are merged into the file + bool ignore_inheritance = 5; +} + +message GetHostedLoginTranslationResponse { + // hash of the payload + string etag = 1 [ + (validate.rules).string = {min_len: 32, max_len: 32}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 32; + max_length: 32; + example: "\"42a1ba123e6ea6f0c93e286ed97c7018\""; + } + ]; + + google.protobuf.Struct translations = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "{\"common\":{\"back\":\"Indietro\"},\"accounts\":{\"title\":\"Account\",\"description\":\"Seleziona l'account che desideri utilizzare.\",\"addAnother\":\"Aggiungi un altro account\",\"noResults\":\"Nessun account trovato\"}}"; + description: "Translations contains the translations in the request language."; + } + ]; +} + +message SetHostedLoginTranslationRequest { + oneof level { + bool instance = 1 [(validate.rules).bool = {const: true}]; + string organization_id = 2; + } + + string locale = 3 [ + (validate.rules).string = {min_len: 2}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 2; + example: "\"fr-FR\""; + } + ]; + + google.protobuf.Struct translations = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "{\"common\":{\"back\":\"Indietro\"},\"accounts\":{\"title\":\"Account\",\"description\":\"Seleziona l'account che desideri utilizzare.\",\"addAnother\":\"Aggiungi un altro account\",\"noResults\":\"Nessun account trovato\"}}"; + description: "Translations should contain the translations in the specified locale."; + } + ]; +} + +message SetHostedLoginTranslationResponse { + // hash of the saved translation. Valid only when ignore_inheritance = true + string etag = 1 [ + (validate.rules).string = {min_len: 32, max_len: 32}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 32; + max_length: 32; + example: "\"42a1ba123e6ea6f0c93e286ed97c7018\""; + } + ]; } \ No newline at end of file diff --git a/proto/zitadel/system.proto b/proto/zitadel/system.proto index f124c37a79..9b65fec600 100644 --- a/proto/zitadel/system.proto +++ b/proto/zitadel/system.proto @@ -117,6 +117,8 @@ service SystemService { } // Returns a list of ZITADEL instances + // + // Deprecated: Use [ListInstances](apis/resources/instance_service_v2/zitadel-instance-v-2-beta-instance-service-list-instances.api.mdx) instead to list instances rpc ListInstances(ListInstancesRequest) returns (ListInstancesResponse) { option (google.api.http) = { post: "/instances/_search" @@ -126,9 +128,15 @@ service SystemService { option (zitadel.v1.auth_option) = { permission: "system.instance.read"; }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; + }; } // Returns the detail of an instance + // + // Deprecated: Use [GetInstance](apis/resources/instance_service_v2/zitadel-instance-v-2-beta-instance-service-get-instance.api.mdx) instead to get the details of the instance in context rpc GetInstance(GetInstanceRequest) returns (GetInstanceResponse) { option (google.api.http) = { get: "/instances/{instance_id}"; @@ -137,6 +145,10 @@ service SystemService { option (zitadel.v1.auth_option) = { permission: "system.instance.read"; }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; + }; } // Deprecated: Use CreateInstance instead @@ -151,9 +163,15 @@ service SystemService { option (zitadel.v1.auth_option) = { permission: "system.instance.write"; }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; + }; } // Updates name of an existing instance + // + // Deprecated: Use [UpdateInstance](apis/resources/instance_service_v2/zitadel-instance-v-2-beta-instance-service-update-instance.api.mdx) instead to update the name of the instance in context rpc UpdateInstance(UpdateInstanceRequest) returns (UpdateInstanceResponse) { option (google.api.http) = { put: "/instances/{instance_id}" @@ -163,6 +181,10 @@ service SystemService { option (zitadel.v1.auth_option) = { permission: "system.instance.write"; }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; + }; } // Creates a new instance with all needed setup data @@ -180,6 +202,8 @@ service SystemService { // Removes an instance // This might take some time + // + // Deprecated: Use [DeleteInstance](apis/resources/instance_service_v2/zitadel-instance-v-2-beta-instance-service-delete-instance.api.mdx) instead to delete an instance rpc RemoveInstance(RemoveInstanceRequest) returns (RemoveInstanceResponse) { option (google.api.http) = { delete: "/instances/{instance_id}" @@ -188,6 +212,10 @@ service SystemService { option (zitadel.v1.auth_option) = { permission: "system.instance.delete"; }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; + }; } //Returns all instance members matching the request @@ -204,7 +232,9 @@ service SystemService { }; } - //Checks if a domain exists + // Checks if a domain exists + // + // Deprecated: Use [ListCustomDomains](apis/resources/instance_service_v2/zitadel-instance-v-2-beta-instance-service-list-custom-domains.api.mdx) instead to check existence of an instance rpc ExistsDomain(ExistsDomainRequest) returns (ExistsDomainResponse) { option (google.api.http) = { post: "/domains/{domain}/_exists"; @@ -214,10 +244,14 @@ service SystemService { option (zitadel.v1.auth_option) = { permission: "system.domain.read"; }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; + }; } // Returns the custom domains of an instance - //Checks if a domain exists + // Checks if a domain exists // Deprecated: Use the Admin APIs ListInstanceDomains on the admin API instead rpc ListDomains(ListDomainsRequest) returns (ListDomainsResponse) { option (google.api.http) = { @@ -228,9 +262,15 @@ service SystemService { option (zitadel.v1.auth_option) = { permission: "system.domain.read"; }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; + }; } // Adds a domain to an instance + // + // Deprecated: Use [AddCustomDomain](apis/resources/instance_service_v2/zitadel-instance-v-2-beta-instance-service-add-custom-domain.api.mdx) instead to add a custom domain to the instance in context rpc AddDomain(AddDomainRequest) returns (AddDomainResponse) { option (google.api.http) = { post: "/instances/{instance_id}/domains"; @@ -240,9 +280,15 @@ service SystemService { option (zitadel.v1.auth_option) = { permission: "system.domain.write"; }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; + }; } // Removes the domain of an instance + // + // Deprecated: Use [RemoveDomain](apis/resources/instance_service_v2/zitadel-instance-v-2-beta-instance-service-remove-custom-domain.api.mdx) instead to remove a custom domain from the instance in context rpc RemoveDomain(RemoveDomainRequest) returns (RemoveDomainResponse) { option (google.api.http) = { delete: "/instances/{instance_id}/domains/{domain}"; @@ -251,6 +297,10 @@ service SystemService { option (zitadel.v1.auth_option) = { permission: "system.domain.delete"; }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; + }; } // Sets the primary domain of an instance diff --git a/proto/zitadel/user/v2/email.proto b/proto/zitadel/user/v2/email.proto index e962707fcf..eb807206a7 100644 --- a/proto/zitadel/user/v2/email.proto +++ b/proto/zitadel/user/v2/email.proto @@ -19,7 +19,7 @@ message SetHumanEmail { example: "\"mini@mouse.com\""; } ]; - // if no verification is specified, an email is sent with the default url + // If no verification is specified, an email is sent with the default url oneof verification { SendEmailVerificationCode send_code = 2; ReturnEmailVerificationCode return_code = 3; diff --git a/proto/zitadel/user/v2/idp.proto b/proto/zitadel/user/v2/idp.proto index 73e633fb67..828a035c29 100644 --- a/proto/zitadel/user/v2/idp.proto +++ b/proto/zitadel/user/v2/idp.proto @@ -162,3 +162,21 @@ message IDPLink { } ]; } + +message FormData { + // The URL to which the form should be submitted using the POST method. + string url = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"https://idp.com/saml/v2/acs\""; + } + ]; + // The form fields to be submitted. + // Each field is represented as a key-value pair, where the key is the field / input name + // and the value is the field / input value. + // All fields need to be submitted as is and as input type "text". + map fields = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "{\"relayState\":\"state\",\"SAMLRequest\":\"asjfkj3ir2fj248=\"}"; + } + ]; +} \ No newline at end of file diff --git a/proto/zitadel/user/v2/key.proto b/proto/zitadel/user/v2/key.proto new file mode 100644 index 0000000000..ffa83c714e --- /dev/null +++ b/proto/zitadel/user/v2/key.proto @@ -0,0 +1,69 @@ +syntax = "proto3"; + +package zitadel.user.v2; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/user/v2;user"; + +import "protoc-gen-openapiv2/options/annotations.proto"; +import "google/protobuf/timestamp.proto"; +import "validate/validate.proto"; +import "zitadel/filter/v2/filter.proto"; + +message Key { + // The timestamp of the key creation. + google.protobuf.Timestamp creation_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + // The timestamp of the last change of the key. + google.protobuf.Timestamp change_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + // The unique identifier of the key. + string id = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629012906488334\""; + } + ]; + // The unique identifier of the user the key belongs to. + string user_id = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629012906488334\""; + } + ]; + // The unique identifier of the organization the key belongs to. + string organization_id = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629012906488334\""; + } + ]; + // The keys expiration date. + google.protobuf.Timestamp expiration_date = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; +} + +message KeysSearchFilter { + oneof filter { + option (validate.required) = true; + zitadel.filter.v2.IDFilter key_id_filter = 1; + zitadel.filter.v2.IDFilter user_id_filter = 2; + zitadel.filter.v2.IDFilter organization_id_filter = 3; + zitadel.filter.v2.TimestampFilter created_date_filter = 4; + zitadel.filter.v2.TimestampFilter expiration_date_filter = 5; + } +} + +enum KeyFieldName { + KEY_FIELD_NAME_UNSPECIFIED = 0; + KEY_FIELD_NAME_CREATED_DATE = 1; + KEY_FIELD_NAME_ID = 2; + KEY_FIELD_NAME_USER_ID = 3; + KEY_FIELD_NAME_ORGANIZATION_ID = 4; + KEY_FIELD_NAME_KEY_EXPIRATION_DATE = 5; +} diff --git a/proto/zitadel/user/v2/pat.proto b/proto/zitadel/user/v2/pat.proto new file mode 100644 index 0000000000..1d24c4c496 --- /dev/null +++ b/proto/zitadel/user/v2/pat.proto @@ -0,0 +1,70 @@ +syntax = "proto3"; + +package zitadel.user.v2; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/user/v2;user"; + +import "protoc-gen-openapiv2/options/annotations.proto"; +import "google/protobuf/timestamp.proto"; +import "validate/validate.proto"; +import "zitadel/filter/v2/filter.proto"; + +message PersonalAccessToken { + // The timestamp of the personal access token creation. + google.protobuf.Timestamp creation_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + // The timestamp of the last change of the personal access token. + google.protobuf.Timestamp change_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + // The unique identifier of the personal access token. + string id = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629012906488334\""; + } + ]; + // The unique identifier of the user the personal access token belongs to. + string user_id = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629012906488334\""; + } + ]; + // The unique identifier of the organization the personal access token belongs to. + string organization_id = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629012906488334\""; + } + ]; + // The personal access tokens expiration date. + google.protobuf.Timestamp expiration_date = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; +} + +message PersonalAccessTokensSearchFilter { + oneof filter { + option (validate.required) = true; + zitadel.filter.v2.IDFilter token_id_filter = 1; + zitadel.filter.v2.IDFilter user_id_filter = 2; + zitadel.filter.v2.IDFilter organization_id_filter = 3; + zitadel.filter.v2.TimestampFilter created_date_filter = 4; + zitadel.filter.v2.TimestampFilter expiration_date_filter = 5; + } +} + +enum PersonalAccessTokenFieldName { + PERSONAL_ACCESS_TOKEN_FIELD_NAME_UNSPECIFIED = 0; + PERSONAL_ACCESS_TOKEN_FIELD_NAME_CREATED_DATE = 1; + PERSONAL_ACCESS_TOKEN_FIELD_NAME_ID = 2; + PERSONAL_ACCESS_TOKEN_FIELD_NAME_USER_ID = 3; + PERSONAL_ACCESS_TOKEN_FIELD_NAME_ORGANIZATION_ID = 4; + PERSONAL_ACCESS_TOKEN_FIELD_NAME_EXPIRATION_DATE = 5; +} + diff --git a/proto/zitadel/user/v2/user_service.proto b/proto/zitadel/user/v2/user_service.proto index 6c842c0908..7ed12f0143 100644 --- a/proto/zitadel/user/v2/user_service.proto +++ b/proto/zitadel/user/v2/user_service.proto @@ -2,6 +2,14 @@ syntax = "proto3"; package zitadel.user.v2; +import "google/protobuf/timestamp.proto"; +import "google/api/annotations.proto"; +import "google/api/field_behavior.proto"; +import "google/protobuf/duration.proto"; +import "google/protobuf/struct.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; + import "zitadel/object/v2/object.proto"; import "zitadel/protoc_gen_zitadel/v2/options.proto"; import "zitadel/user/v2/auth.proto"; @@ -10,13 +18,11 @@ import "zitadel/user/v2/phone.proto"; import "zitadel/user/v2/idp.proto"; import "zitadel/user/v2/password.proto"; import "zitadel/user/v2/user.proto"; +import "zitadel/user/v2/key.proto"; +import "zitadel/user/v2/pat.proto"; import "zitadel/user/v2/query.proto"; -import "google/api/annotations.proto"; -import "google/api/field_behavior.proto"; -import "google/protobuf/duration.proto"; -import "google/protobuf/struct.proto"; -import "protoc-gen-openapiv2/options/annotations.proto"; -import "validate/validate.proto"; +import "zitadel/filter/v2/filter.proto"; +import "zitadel/metadata/v2/metadata.proto"; option go_package = "github.com/zitadel/zitadel/pkg/grpc/user/v2;user"; @@ -84,6 +90,28 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { } } } + responses: { + key: "400"; + value: { + description: "The request is malformed."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + } + } + } + } + responses: { + key: "401"; + value: { + description: "Returned when the user is not authenticated."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + } + } + } + } responses: { key: "403"; value: { @@ -95,21 +123,44 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { } } } - responses: { - key: "404"; - value: { - description: "Returned when the resource does not exist."; - schema: { - json_schema: { - ref: "#/definitions/rpcStatus"; - } - } - } - } }; service UserService { + // Create a User + // + // Create a new human or machine user in the specified organization. + // + // Required permission: + // - user.write + rpc CreateUser (CreateUserRequest) returns (CreateUserResponse) { + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200" + value: { + description: "OK"; + } + }; + responses: { + key: "409" + value: { + description: "The user already exists."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + }; + }; + } + }; + }; + } + // Create a new human user // // Create/import a new user with the type human. The newly created user will get a verification email if either the email address is not marked as verified and you did not request the verification to be returned. @@ -125,7 +176,7 @@ service UserService { org_field: "organization" } http_response: { - success_code: 201 + success_code: 200 } }; @@ -163,6 +214,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -224,12 +281,16 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } // Resend code to verify user email - // - // Resend code to verify user email. rpc ResendEmailCode (ResendEmailCodeRequest) returns (ResendEmailCodeResponse) { option (google.api.http) = { post: "/v2/users/{user_id}/email/resend" @@ -249,12 +310,16 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } // Send code to verify user email - // - // Send code to verify user email. rpc SendEmailCode (SendEmailCodeRequest) returns (SendEmailCodeResponse) { option (google.api.http) = { post: "/v2/users/{user_id}/email/send" @@ -274,6 +339,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -299,6 +370,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -324,12 +401,18 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } - // Remove the user phone + // Delete the user phone // - // Remove the user phone + // Delete the phone number of a user. rpc RemovePhone(RemovePhoneRequest) returns (RemovePhoneResponse) { option (google.api.http) = { delete: "/v2/users/{user_id}/phone" @@ -343,20 +426,22 @@ service UserService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { - summary: "Delete the user phone"; - description: "Delete the phone number of a user." responses: { key: "200" value: { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } // Resend code to verify user phone - // - // Resend code to verify user phone. rpc ResendPhoneCode (ResendPhoneCodeRequest) returns (ResendPhoneCodeResponse) { option (google.api.http) = { post: "/v2/users/{user_id}/phone/resend" @@ -376,6 +461,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -401,10 +492,64 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } - // Update User + // Update a User + // + // Partially update an existing user. + // If you change the users email or phone, you can specify how the ownership should be verified. + // If you change the users password, you can specify if the password should be changed again on the users next login. + // + // Required permission: + // - user.write + rpc UpdateUser(UpdateUserRequest) returns (UpdateUserResponse) { + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200" + value: { + description: "OK"; + } + }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + }; + }; + } + } + responses: { + key: "409" + value: { + description: "The user already exists."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + }; + }; + } + }; + }; + } + + // Update Human User // // Update all information from a user.. rpc UpdateHumanUser(UpdateHumanUserRequest) returns (UpdateHumanUserResponse) { @@ -426,6 +571,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -451,6 +602,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -476,6 +633,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -501,12 +664,18 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } // Unlock user // - // The state of the user will be changed to 'locked'. The user will not be able to log in anymore. The endpoint returns an error if the user is already in the state 'locked'. Use this endpoint if the user should not be able to log in temporarily because of an event that happened (wrong password, etc.).. + // The state of the user will be changed to 'active'. The user will be able to log in again. The endpoint returns an error if the user is not in the state 'locked'. rpc UnlockUser(UnlockUserRequest) returns (UnlockUserResponse) { option (google.api.http) = { post: "/v2/users/{user_id}/unlock" @@ -526,6 +695,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -539,7 +714,7 @@ service UserService { option (zitadel.protoc_gen_zitadel.v2.options) = { auth_option: { - permission: "user.delete" + permission: "authenticated" } }; @@ -550,6 +725,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -574,6 +755,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -598,6 +785,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -622,6 +815,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -670,6 +869,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -694,6 +899,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -718,6 +929,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -743,6 +960,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -767,6 +990,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -791,6 +1020,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -814,6 +1049,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -838,6 +1079,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -861,6 +1108,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -885,6 +1138,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -908,6 +1167,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -958,6 +1223,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "Intent ID does not exist."; + } + } }; } @@ -983,6 +1254,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -1033,6 +1310,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -1058,6 +1341,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -1083,6 +1372,251 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } + }; + } + + + // Add a Users Secret + // + // Generates a client secret for the user. + // The client id is the users username. + // If the user already has a secret, it is overwritten. + // Only users of type machine can have a secret. + // + // Required permission: + // - user.write + rpc AddSecret(AddSecretRequest) returns (AddSecretResponse) { + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200" + value: { + description: "The secret was successfully generated."; + } + }; + responses: { + key: "404" + value: { + description: "The user ID does not exist."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + }; + }; + } + }; + }; + } + + // Remove a Users Secret + // + // Remove the current client ID and client secret from a machine user. + // + // Required permission: + // - user.write + rpc RemoveSecret(RemoveSecretRequest) returns (RemoveSecretResponse) { + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200" + value: { + description: "The secret was either successfully removed or it didn't exist in the first place."; + } + }; + }; + } + + // Add a Key + // + // Add a keys that can be used to securely authenticate at the Zitadel APIs using JWT profile authentication using short-lived tokens. + // Make sure you store the returned key safely, as you won't be able to read it from the Zitadel API anymore. + // Only users of type machine can have keys. + // + // Required permission: + // - user.write + rpc AddKey(AddKeyRequest) returns (AddKeyResponse) { + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200" + value: { + description: "The key was successfully created."; + } + }; + responses: { + key: "404" + value: { + description: "The user ID does not exist."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + }; + }; + } + }; + }; + } + + // Remove a Key + // + // Remove a machine users key by the given key ID and an optionally given user ID. + // + // Required permission: + // - user.write + rpc RemoveKey(RemoveKeyRequest) returns (RemoveKeyResponse) { + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200" + value: { + description: "The key was either successfully removed or it not found in the first place."; + } + }; + }; + } + + // Search Keys + // + // List all matching keys. By default all keys of the instance on which the caller has permission to read the owning users are returned. + // Make sure to include a limit and sorting for pagination. + // + // Required permission: + // - user.read + rpc ListKeys(ListKeysRequest) returns (ListKeysResponse) { + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "A list of all machine user keys matching the query"; + }; + }; + responses: { + key: "400"; + value: { + description: "invalid list query"; + }; + }; + }; + } + + // Add a Personal Access Token + // + // Personal access tokens (PAT) are the easiest way to authenticate to the Zitadel APIs. + // Make sure you store the returned PAT safely, as you won't be able to read it from the Zitadel API anymore. + // Only users of type machine can have personal access tokens. + // + // Required permission: + // - user.write + rpc AddPersonalAccessToken(AddPersonalAccessTokenRequest) returns (AddPersonalAccessTokenResponse) { + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200" + value: { + description: "The personal access token was successfully created."; + } + }; + responses: { + key: "404" + value: { + description: "The user ID does not exist."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + }; + }; + } + }; + }; + } + + // Remove a Personal Access Token + // + // Removes a machine users personal access token by the given token ID and an optionally given user ID. + // + // Required permission: + // - user.write + rpc RemovePersonalAccessToken(RemovePersonalAccessTokenRequest) returns (RemovePersonalAccessTokenResponse) { + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200" + value: { + description: "The personal access token was either successfully removed or it was not found in the first place."; + } + }; + }; + } + + // Search Personal Access Tokens + // + // List all personal access tokens. By default all personal access tokens of the instance on which the caller has permission to read the owning users are returned. + // Make sure to include a limit and sorting for pagination. + // + // Required permission: + // - user.read + rpc ListPersonalAccessTokens(ListPersonalAccessTokensRequest) returns (ListPersonalAccessTokensResponse) { + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + value: { + description: "A list of all personal access tokens matching the query"; + }; + }; + responses: { + key: "400"; + value: { + description: "invalid list query"; + }; + }; }; } @@ -1155,6 +1689,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -1183,6 +1723,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -1209,6 +1755,12 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } }; } @@ -1234,9 +1786,92 @@ service UserService { description: "OK"; } }; + responses: { + key: "404"; + value: { + description: "User ID does not exist."; + } + } + }; + } + // Set User Metadata + // + // Sets a list of key value pairs. Existing metadata entries with matching keys are overwritten. Existing metadata entries without matching keys are untouched. To remove metadata entries, use [DeleteUserMetadata](apis/resources/user_service_v2/user-service-delete-user-metadata.api.mdx). For HTTP requests, make sure the bytes array value is base64 encoded. + // + // Required permission: + // - `user.write` + rpc SetUserMetadata(SetUserMetadataRequest) returns (SetUserMetadataResponse) { + option (google.api.http) = { + post: "/v2/users/{user_id}/metadata" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + }; + responses: { + key: "400" + value: { + description: "User not found"; + } + }; }; } + // List User Metadata + // + // List metadata of an user filtered by query. + // + // Required permission: + // - `user.read` + rpc ListUserMetadata(ListUserMetadataRequest) returns (ListUserMetadataResponse) { + option (google.api.http) = { + post: "/v2/users/{user_id}/metadata/search" + body: "*" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = {auth_option: { + permission: "user.read" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + }; + }; + } + + // Delete User Metadata + // + // Delete metadata objects from an user with a specific key. + // + // Required permission: + // - `user.write` + rpc DeleteUserMetadata(DeleteUserMetadataRequest) returns (DeleteUserMetadataResponse) { + option (google.api.http) = { + delete: "/v2/users/{user_id}/metadata" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200"; + }; + }; + } } message AddHumanUserRequest{ @@ -1296,6 +1931,156 @@ message AddHumanUserResponse { optional string phone_code = 4; } + +message CreateUserRequest{ + message Human { + // Set the users profile information. + SetHumanProfile profile = 1 [ + (validate.rules).message.required = true, + (google.api.field_behavior) = REQUIRED + ]; + // Set the users email address and optionally send a verification email. + SetHumanEmail email = 2 [ + (validate.rules).message.required = true, + (google.api.field_behavior) = REQUIRED + ]; + // Set the users phone number and optionally send a verification SMS. + optional SetHumanPhone phone = 3; + // Set the users initial password and optionally require the user to set a new password. + oneof password_type { + Password password = 4; + HashedPassword hashed_password = 5; + } + // Create the user with a list of links to identity providers. + // This can be useful in migration-scenarios. + // For example, if a user already has an account in an external identity provider or another Zitadel instance, an IDP link allows the user to authenticate as usual. + // Sessions, second factors, hardware keys registered externally are still available for authentication. + // Use the following endpoints to manage identity provider links: + // - [AddIDPLink](apis/resources/user_service_v2/user-service-add-idp-link.api.mdx) + // - [RemoveIDPLink](apis/resources/user_service_v2/user-service-remove-idp-link.api.mdx) + repeated IDPLink idp_links = 7; + // An Implementation of RFC 6238 is used, with HMAC-SHA-1 and time-step of 30 seconds. + // Currently no other options are supported, and if anything different is used the validation will fail. + optional string totp_secret = 8 [ + (validate.rules).string = {min_len: 1 max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"TJOPWSDYILLHXFV4MLKNNJOWFG7VSDCK\""; + } + ]; + + // Metadata to bet set. The values have to be base64 encoded. + repeated Metadata metadata = 9 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[{\"key\": \"test1\", \"value\": \"VGhpcyBpcyBteSBmaXJzdCB2YWx1ZQ==\"}, {\"key\": \"test2\", \"value\": \"VGhpcyBpcyBteSBzZWNvbmQgdmFsdWU=\"}]" + } + ]; + } + message Machine { + // The machine users name is a human readable field that helps identifying the user. + string name = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"Acceptance Test User\""; + } + ]; + // The description is a field that helps to remember the purpose of the user. + optional string description = 2 [ + (validate.rules).string = {min_len: 1, max_len: 500}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 500, + example: "\"The user calls the session API in the continuous integration pipeline for acceptance tests.\""; + } + ]; + } + // The unique identifier of the organization the user belongs to. + string organization_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"69629026806489455\""; + } + ]; + // The ID is a unique identifier for the user in the instance. + // If not specified, it will be generated. + // You can set your own user id that is unique within the instance. + // This is useful in migration scenarios, for example if the user already has an ID in another Zitadel system. + // If not specified, it will be generated. + // It can't be changed after creation. + optional string user_id = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"163840776835432345\""; + } + ]; + + // The username is a unique identifier for the user in the organization. + // If not specified, Zitadel sets the username to the email for users of type human and to the user_id for users of type machine. + // It is used to identify the user in the organization and can be used for login. + optional string username = 3 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"minnie-mouse\""; + } + ]; + + // The type of the user. + oneof user_type { + option (validate.required) = true; + // Users of type human are users that are meant to be used by a person. + // They can log in interactively using a login UI. + // By default, new users will receive a verification email and, if a phone is configured, a verification SMS. + // To make sure these messages are sent, configure and activate valid SMTP and Twilio configurations. + // Read more about your options for controlling this behaviour in the email and phone field documentations. + Human human = 4; + // Users of type machine are users that are meant to be used by a machine. + // In order to authenticate, [add a secret](apis/resources/user_service_v2/user-service-add-secret.api.mdx), [a key](apis/resources/user_service_v2/user-service-add-key.api.mdx) or [a personal access token](apis/resources/user_service_v2/user-service-add-personal-access-token.api.mdx) to the user. + // Tokens generated for new users of type machine will be of an opaque Bearer type. + // You can change the users token type to JWT by using the [management v1 service method UpdateMachine](apis/resources/mgmt/management-service-update-machine.api.mdx). + Machine machine = 5; + } + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + example: "{\"organizationId\":\"69629026806489455\",\"userId\":\"163840776835432345\",\"username\":\"minnie-mouse\",\"human\":{\"profile\":{\"givenName\":\"Minnie\",\"familyName\":\"Mouse\",\"nickName\":\"Mini\",\"displayName\":\"Minnie Mouse\",\"preferredLanguage\":\"en\",\"gender\":\"GENDER_FEMALE\"},\"email\":{\"email\":\"mini@mouse.com\",\"sendCode\":{\"urlTemplate\":\"https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}\"}},\"phone\":{\"phone\":\"+41791234567\",\"isVerified\":true},\"password\":{\"password\":\"Secr3tP4ssw0rd!\",\"changeRequired\":true},\"idpLinks\":[{\"idpId\":\"d654e6ba-70a3-48ef-a95d-37c8d8a7901a\",\"userId\":\"6516849804890468048461403518\",\"userName\":\"user@external.com\"}],\"totpSecret\":\"TJOPWSDYILLHXFV4MLKNNJOWFG7VSDCK\"}}"; + }; +} + +message CreateUserResponse { + // The unique identifier of the newly created user. + string id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629012906488334\""; + } + ]; + // The timestamp of the user creation. + google.protobuf.Timestamp creation_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + + // The email verification code if it was requested by setting the email verification to return_code. + optional string email_code = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"XTA6BC\""; + } + ]; + // The phone verification code if it was requested by setting the phone verification to return_code. + optional string phone_code = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"XTA6BC\""; + } + ]; +} + message GetUserByIDRequest { reserved 2; reserved "organization"; @@ -1550,6 +2335,142 @@ message DeleteUserResponse { zitadel.object.v2.Details details = 1; } + +message UpdateUserRequest{ + message Human { + message Profile { + // The given name is the first name of the user. + // For example, it can be used to personalize notifications and login UIs. + optional string given_name = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"Minnie\""; + } + ]; + // The family name is the last name of the user. + // For example, it can be used to personalize user interfaces and notifications. + optional string family_name = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"Mouse\""; + } + ]; + // The nick name is the users short name. + // For example, it can be used to personalize user interfaces and notifications. + optional string nick_name = 3 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"Mini\""; + } + ]; + // The display name is how a user should primarily be displayed in lists. + // It can also for example be used to personalize user interfaces and notifications. + optional string display_name = 4 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"Minnie Mouse\""; + } + ]; + // The users preferred language is the language that systems should use to interact with the user. + // It has the format of a [BCP-47 language tag](https://datatracker.ietf.org/doc/html/rfc3066). + // It is used by Zitadel where no higher prioritized preferred language can be used. + // For example, browser settings can overwrite a users preferred_language. + // Notification messages and standard login UIs use the users preferred language if it is supported and allowed on the instance. + // Else, the default language of the instance is used. + optional string preferred_language = 5 [ + (validate.rules).string = {min_len: 1, max_len: 10}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 10; + example: "\"en-US\""; + } + ]; + // The users gender can for example be used to personalize user interfaces and notifications. + optional Gender gender = 6 [ + (validate.rules).enum = {defined_only: true}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"GENDER_FEMALE\""; + } + ]; + } + // Change the users profile information + optional Profile profile = 1; + // Change the users email address and/or trigger a verification email + optional SetHumanEmail email = 2; + // Change the users phone number and/or trigger a verification SMS + // To delete the users phone number, leave the phone field empty and omit the verification field. + optional SetHumanPhone phone = 3; + // Change the users password. + // You can optionally require the current password or the verification code to be correct. + optional SetPassword password = 4; + } + message Machine { + // The machine users name is a human readable field that helps identifying the user. + optional string name = 1 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"Acceptance Test User\""; + } + ]; + // The description is a field that helps to remember the purpose of the user. + optional string description = 2 [ + (validate.rules).string = {max_len: 500}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"The user calls the session API in the continuous integration pipeline for acceptance tests.\""; + } + ]; + } + // The user id is the users unique identifier in the instance. + // It can't be changed. + string user_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"163840776835432345\""; + } + ]; + // Set a new username that is unique within the instance. + // Beware that active tokens and sessions are invalidated when the username is changed. + optional string username = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"minnie-mouse\""; + } + ]; + // Change type specific properties of the user. + oneof user_type { + Human human = 3; + Machine machine = 4; + } + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + example: "{\"username\":\"minnie-mouse\",\"human\":{\"profile\":{\"givenName\":\"Minnie\",\"familyName\":\"Mouse\",\"displayName\":\"Minnie Mouse\"},\"email\":{\"email\":\"mini@mouse.com\",\"returnCode\":{}},\"phone\":{\"phone\":\"+41791234567\",\"isVerified\":true},\"password\":{\"password\":{\"password\":\"Secr3tP4ssw0rd!\",\"changeRequired\":true},\"verificationCode\":\"SKJd342k\"},\"totpSecret\":\"TJOPWSDYILLHXFV4MLKNNJOWFG7VSDCK\"}}"; + }; +} + +message UpdateUserResponse { + // The timestamp of the change of the user. + google.protobuf.Timestamp change_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; + // In case the email verification was set to return_code, the code will be returned + optional string email_code = 2; + // In case the phone verification was set to return_code, the code will be returned + optional string phone_code = 3; +} + message UpdateHumanUserRequest{ string user_id = 1 [ (validate.rules).string = {min_len: 1, max_len: 200}, @@ -1595,7 +2516,6 @@ message DeactivateUserResponse { zitadel.object.v2.Details details = 1; } - message ReactivateUserRequest { string user_id = 1 [ (validate.rules).string = {min_len: 1, max_len: 200}, @@ -2061,11 +2981,15 @@ message StartIdentityProviderIntentResponse{ description: "IDP Intent information" } ]; + // POST call information + // Deprecated: Use form_data instead bytes post_form = 4 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { description: "POST call information" } ]; + // Data for a form POST call + FormData form_data = 5; } } @@ -2384,3 +3308,313 @@ message HumanMFAInitSkippedRequest { message HumanMFAInitSkippedResponse { zitadel.object.v2.Details details = 1; } + +message AddSecretRequest { + // The users resource ID. + string user_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"163840776835432345\""; + } + ]; +} + +message AddSecretResponse { + // The timestamp of the secret creation. + google.protobuf.Timestamp creation_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + // The client secret. + // Store this secret in a secure place. + // It is not possible to retrieve it again. + string client_secret = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"WoYLHB23HAZaCSxeMJGEzbu8urHICVdFp2IegVr6Q5U4lZHKAtRvmaalNDWfCuHV\""; + } + ]; +} + +message RemoveSecretRequest { + // The users resource ID. + string user_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"163840776835432345\""; + } + ]; +} + +message RemoveSecretResponse { + // The timestamp of the secret deletion. + google.protobuf.Timestamp deletion_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; +} + +message AddKeyRequest { + // The users resource ID. + string user_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"163840776835432345\""; + } + ]; + // The date the key will expire and no logins will be possible anymore. + google.protobuf.Timestamp expiration_date = 2 [ + (validate.rules).timestamp.required = true, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2519-04-01T08:45:00.000000Z\""; + } + ]; + // Optionally provide a public key of your own generated RSA private key. + bytes public_key = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1...\""; + } + ]; +} + +message AddKeyResponse { + // The timestamp of the key creation. + google.protobuf.Timestamp creation_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + // The keys ID. + string key_id = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"163840776835432345\""; + } + ]; + // The key which is usable to authenticate against the API. + bytes key_content = 3; +} + + +message RemoveKeyRequest { + // The users resource ID. + string user_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"163840776835432345\""; + } + ]; + // The keys ID. + string key_id = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"163840776835432345\""; + } + ]; +} + +message RemoveKeyResponse { + // The timestamp of the key deletion. + google.protobuf.Timestamp deletion_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; +} + +message ListKeysRequest { + // List limitations and ordering. + optional zitadel.filter.v2.PaginationRequest pagination = 1; + // The field the result is sorted by. The default is the creation date. Beware that if you change this, your result pagination might be inconsistent. + optional KeyFieldName sorting_column = 2 [ + (validate.rules).enum = {defined_only: true}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"KEY_FIELD_NAME_CREATED_DATE\"" + } + ]; + // Define the criteria to query for. + repeated KeysSearchFilter filters = 3; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + example: "{\"pagination\":{\"offset\":0,\"limit\":10,\"asc\":false},\"sortingColumn\":\"KEY_FIELD_NAME_CREATED_DATE\",\"filters\":[{\"andFilter\":{\"filters\":[{\"organizationIdFilter\":{\"organizationId\":\"163840776835432345\",\"method\":\"TEXT_FILTER_METHOD_EQUALS\"}},{\"notFilter\":{\"filter\":{\"userIdFilter\":{\"userId\":\"163840776835432345\",\"method\":\"TEXT_FILTER_METHOD_EQUALS\"}}}},{\"orFilter\":{\"filters\":[{\"keyIdFilter\":{\"keyId\":\"163840776835432345\",\"method\":\"TEXT_FILTER_METHOD_EQUALS\"}},{\"keyIdFilter\":{\"keyId\":\"163840776835432345\",\"method\":\"TEXT_FILTER_METHOD_EQUALS\"}}]}}]}}]}"; + }; +} + +message ListKeysResponse { + zitadel.filter.v2.PaginationResponse pagination = 1; + repeated Key result = 2; +} + +message AddPersonalAccessTokenRequest { + // The users resource ID. + string user_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"163840776835432345\""; + } + ]; + // The timestamp when the token will expire. + google.protobuf.Timestamp expiration_date = 2 [ + (validate.rules).timestamp.required = true, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2519-04-01T08:45:00.000000Z\""; + } + ]; +} + +message AddPersonalAccessTokenResponse { + // The timestamp of the personal access token creation. + google.protobuf.Timestamp creation_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + // The tokens ID. + string token_id = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"163840776835432345\""; + } + ]; + // The personal access token that can be used to authenticate against the API + string token = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c\""; + } + ]; +} + +message RemovePersonalAccessTokenRequest { + // The users resource ID. + string user_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"163840776835432345\""; + } + ]; + // The tokens ID. + string token_id = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"163840776835432345\""; + } + ]; +} + +message RemovePersonalAccessTokenResponse { + // The timestamp of the personal access token deletion. + google.protobuf.Timestamp deletion_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; +} + + +message ListPersonalAccessTokensRequest { + // List limitations and ordering. + optional zitadel.filter.v2.PaginationRequest pagination = 1; + // The field the result is sorted by. The default is the creation date. Beware that if you change this, your result pagination might be inconsistent. + optional PersonalAccessTokenFieldName sorting_column = 2 [ + (validate.rules).enum = {defined_only: true}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "\"PERSONAL_ACCESS_TOKEN_FIELD_NAME_CREATED_DATE\"" + } + ]; + // Define the criteria to query for. + repeated PersonalAccessTokensSearchFilter filters = 3; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + example: "{\"pagination\":{\"offset\":0,\"limit\":10,\"asc\":false},\"sortingColumn\":\"PERSONAL_ACCESS_TOKEN_FIELD_NAME_CREATED_DATE\",\"filters\":[{\"andFilter\":{\"filters\":[{\"organizationIdFilter\":{\"organizationId\":\"163840776835432345\",\"method\":\"TEXT_FILTER_METHOD_EQUALS\"}},{\"notFilter\":{\"filter\":{\"userIdFilter\":{\"userId\":\"163840776835432345\",\"method\":\"TEXT_FILTER_METHOD_EQUALS\"}}}},{\"orFilter\":{\"filters\":[{\"personalAccessTokenIdFilter\":{\"personalAccessTokenId\":\"163840776835432345\",\"method\":\"TEXT_FILTER_METHOD_EQUALS\"}},{\"personalAccessTokenIdFilter\":{\"personalAccessTokenId\":\"163840776835432345\",\"method\":\"TEXT_FILTER_METHOD_EQUALS\"}}]}}]}}]}"; + }; +} + +message ListPersonalAccessTokensResponse { + zitadel.filter.v2.PaginationResponse pagination = 1; + repeated PersonalAccessToken result = 2; +} + +message Metadata { + // Key in the metadata key/value pair. + string key = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + // Value in the metadata key/value pair. + bytes value = 2 [(validate.rules).bytes = {min_len: 1, max_len: 500000}]; +} + +message SetUserMetadataRequest{ + // ID of the user under which the metadata gets set. + string user_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629012906488334\""; + } + ]; + // Metadata to bet set. The values have to be base64 encoded. + repeated Metadata metadata = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[{\"key\": \"test1\", \"value\": \"VGhpcyBpcyBteSBmaXJzdCB2YWx1ZQ==\"}, {\"key\": \"test2\", \"value\": \"VGhpcyBpcyBteSBzZWNvbmQgdmFsdWU=\"}]" + } + ]; +} + +message SetUserMetadataResponse{ + // The timestamp of the update of the user metadata. + google.protobuf.Timestamp set_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message ListUserMetadataRequest { + // ID of the user under which the metadata is to be listed. + string user_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629012906488334\""; + } + ]; + + // List limitations and ordering. + optional zitadel.filter.v2.PaginationRequest pagination = 2; + // Define the criteria to query for. + repeated zitadel.metadata.v2.MetadataSearchFilter filters = 3; +} + +message ListUserMetadataResponse { + // Pagination of the users metadata results. + zitadel.filter.v2.PaginationResponse pagination = 1; + // The user metadata requested. + repeated zitadel.metadata.v2.Metadata metadata = 2; +} + +message DeleteUserMetadataRequest { + // ID of the user which metadata is to be deleted is stored on. + string user_id = 1; + // The keys for the user metadata to be deleted. + repeated string keys = 2 [(validate.rules).repeated.items.string = {min_len: 1, max_len: 200}]; +} + +message DeleteUserMetadataResponse{ + // The timestamp of the deletion of the user metadata. + google.protobuf.Timestamp deletion_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} \ No newline at end of file diff --git a/proto/zitadel/user/v2beta/idp.proto b/proto/zitadel/user/v2beta/idp.proto index 7d58ec5363..237c8de114 100644 --- a/proto/zitadel/user/v2beta/idp.proto +++ b/proto/zitadel/user/v2beta/idp.proto @@ -162,3 +162,21 @@ message IDPLink { } ]; } + +message FormData { + // The URL to which the form should be submitted using the POST method. + string url = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"https://idp.com/saml/v2/acs\""; + } + ]; + // The form fields to be submitted. + // Each field is represented as a key-value pair, where the key is the field / input name + // and the value is the field / input value. + // All fields need to be submitted as is and as input type "text". + map fields = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "{\"relayState\":\"state\",\"SAMLRequest\":\"asjfkj3ir2fj248=\"}"; + } + ]; +} \ No newline at end of file diff --git a/proto/zitadel/user/v2beta/user_service.proto b/proto/zitadel/user/v2beta/user_service.proto index 03bc36220e..bcb091abf2 100644 --- a/proto/zitadel/user/v2beta/user_service.proto +++ b/proto/zitadel/user/v2beta/user_service.proto @@ -563,7 +563,7 @@ service UserService { option (zitadel.protoc_gen_zitadel.v2.options) = { auth_option: { - permission: "user.delete" + permission: "authenticated" } }; @@ -1788,22 +1788,23 @@ message StartIdentityProviderIntentRequest{ message StartIdentityProviderIntentResponse{ zitadel.object.v2beta.Details details = 1; oneof next_step { + // URL to which the client should redirect string auth_url = 2 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - description: "URL to which the client should redirect" example: "\"https://accounts.google.com/o/oauth2/v2/auth?client_id=clientID&callback=https%3A%2F%2Fzitadel.cloud%2Fidps%2Fcallback\""; } ]; - IDPIntent idp_intent = 3 [ - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - description: "IDP Intent information" - } - ]; + // IDP Intent information + IDPIntent idp_intent = 3; + // POST call information + // Deprecated: Use form_data instead bytes post_form = 4 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { description: "POST call information" } ]; + // Data for a form POST call + FormData form_data = 5; } } diff --git a/proto/zitadel/v1.proto b/proto/zitadel/v1.proto index c186ea7d61..beb91116f1 100644 --- a/proto/zitadel/v1.proto +++ b/proto/zitadel/v1.proto @@ -172,10 +172,12 @@ message DataOIDCApplication { message DataHumanUser { string user_id = 1; zitadel.management.v1.ImportHumanUserRequest user = 2; + zitadel.user.v1.UserState state = 3; } message DataMachineUser { string user_id = 1; zitadel.management.v1.AddMachineUserRequest user = 2; + zitadel.user.v1.UserState state = 3; } message DataAction { string action_id = 1; diff --git a/proto/zitadel/webkey/v2/key.proto b/proto/zitadel/webkey/v2/key.proto new file mode 100644 index 0000000000..4ec85fa168 --- /dev/null +++ b/proto/zitadel/webkey/v2/key.proto @@ -0,0 +1,109 @@ +syntax = "proto3"; + +package zitadel.webkey.v2; + +import "google/protobuf/timestamp.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/webkey/v2;webkey"; + +enum State { + STATE_UNSPECIFIED = 0; + // A newly created key is in the initial state and published to the public key endpoint. + STATE_INITIAL = 1; + // The active key is used to sign tokens. Only one key can be active at a time. + STATE_ACTIVE = 2; + // The inactive key is not used to sign tokens anymore, but still published to the public key endpoint. + STATE_INACTIVE = 3; + // The removed key is not used to sign tokens anymore and not published to the public key endpoint. + STATE_REMOVED = 4; +} + +message WebKey { + // The unique identifier of the key. + string id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629012906488334\""; + } + ]; + // The timestamp of the key creation. + google.protobuf.Timestamp creation_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; + // The timestamp of the last change to the key (e.g. creation, activation, deactivation). + google.protobuf.Timestamp change_date = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; + // State of the key + State state = 4; + // Configured type of the key (either RSA, ECDSA or ED25519) + oneof key { + RSA rsa = 5; + ECDSA ecdsa = 6; + ED25519 ed25519 = 7; + } +} + +message RSA { + // Bit size of the RSA key. Default is 2048 bits. + RSABits bits = 1 [ + (validate.rules).enum = {defined_only: true, not_in: [0]}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "RSA_BITS_2048"; + } + ]; + // Signing algrithm used. Default is SHA256. + RSAHasher hasher = 2 [ + (validate.rules).enum = {defined_only: true, not_in: [0]}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "RSA_HASHER_SHA256"; + } + ]; +} + +enum RSABits { + RSA_BITS_UNSPECIFIED = 0; + // 2048 bit RSA key + RSA_BITS_2048 = 1; + // 3072 bit RSA key + RSA_BITS_3072 = 2; + // 4096 bit RSA key + RSA_BITS_4096 = 3; +} + +enum RSAHasher { + RSA_HASHER_UNSPECIFIED = 0; + // SHA256 hashing algorithm resulting in the RS256 algorithm header + RSA_HASHER_SHA256 = 1; + // SHA384 hashing algorithm resulting in the RS384 algorithm header + RSA_HASHER_SHA384 = 2; + // SHA512 hashing algorithm resulting in the RS512 algorithm header + RSA_HASHER_SHA512 = 3; +} + +message ECDSA { + // Curve of the ECDSA key. Default is P-256. + ECDSACurve curve = 1 [ + (validate.rules).enum = {defined_only: true, not_in: [0]}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + default: "ECDSA_CURVE_P256"; + } + ]; +} + +enum ECDSACurve { + ECDSA_CURVE_UNSPECIFIED = 0; + // NIST P-256 curve resulting in the ES256 algorithm header + ECDSA_CURVE_P256 = 1; + // NIST P-384 curve resulting in the ES384 algorithm header + ECDSA_CURVE_P384 = 2; + // NIST P-512 curve resulting in the ES512 algorithm header + ECDSA_CURVE_P512 = 3; +} + +message ED25519 {} diff --git a/proto/zitadel/webkey/v2/webkey_service.proto b/proto/zitadel/webkey/v2/webkey_service.proto new file mode 100644 index 0000000000..f29f291c38 --- /dev/null +++ b/proto/zitadel/webkey/v2/webkey_service.proto @@ -0,0 +1,335 @@ +syntax = "proto3"; + +package zitadel.webkey.v2; + +import "google/api/annotations.proto"; +import "google/api/field_behavior.proto"; +import "google/protobuf/timestamp.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; +import "zitadel/protoc_gen_zitadel/v2/options.proto"; +import "zitadel/webkey/v2/key.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/webkey/v2;webkey"; + +option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { + info: { + title: "Web Key Service"; + version: "2.0"; + description: "This API is intended to manage web keys for a ZITADEL instance, used to sign and validate OIDC tokens.\n\nThe public key endpoint (outside of this service) is used to retrieve the public keys of the active and inactive keys.\n\nPlease make sure to enable the `web_key` feature flag on your instance to use this service."; + contact:{ + name: "ZITADEL" + url: "https://zitadel.com" + email: "hi@zitadel.com" + } + license: { + name: "Apache 2.0", + url: "https://github.com/zitadel/zitadel/blob/main/LICENSING.md"; + }; + }; + schemes: HTTPS; + schemes: HTTP; + + consumes: "application/json"; + produces: "application/json"; + + consumes: "application/grpc"; + produces: "application/grpc"; + + consumes: "application/grpc-web+proto"; + produces: "application/grpc-web+proto"; + + host: "$CUSTOM-DOMAIN"; + base_path: "/"; + + external_docs: { + description: "Detailed information about ZITADEL", + url: "https://zitadel.com/docs" + } + security_definitions: { + security: { + key: "OAuth2"; + value: { + type: TYPE_OAUTH2; + flow: FLOW_ACCESS_CODE; + authorization_url: "$CUSTOM-DOMAIN/oauth/v2/authorize"; + token_url: "$CUSTOM-DOMAIN/oauth/v2/token"; + scopes: { + scope: { + key: "openid"; + value: "openid"; + } + scope: { + key: "urn:zitadel:iam:org:project:id:zitadel:aud"; + value: "urn:zitadel:iam:org:project:id:zitadel:aud"; + } + } + } + } + } + security: { + security_requirement: { + key: "OAuth2"; + value: { + scope: "openid"; + scope: "urn:zitadel:iam:org:project:id:zitadel:aud"; + } + } + } + responses: { + key: "403"; + value: { + description: "Returned when the user does not have permission to access the resource."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + } + } + } + } + responses: { + key: "404"; + value: { + description: "Returned when the resource does not exist."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + } + } + } + } +}; + +// Service to manage web keys for OIDC token signing and validation. +// The service provides methods to create, activate, delete and list web keys. +// The public key endpoint (outside of this service) is used to retrieve the public keys of the active and inactive keys. +// +// Please make sure to enable the `web_key` feature flag on your instance to use this service. +service WebKeyService { + // Create Web Key + // + // Generate a private and public key pair. The private key can be used to sign OIDC tokens after activation. + // The public key can be used to validate OIDC tokens. + // The newly created key will have the state `STATE_INITIAL` and is published to the public key endpoint. + // Note that the JWKs OIDC endpoint returns a cacheable response. + // + // If no key type is provided, a RSA key pair with 2048 bits and SHA256 hashing will be created. + // + // Required permission: + // - `iam.web_key.write` + // + // Required feature flag: + // - `web_key` + rpc CreateWebKey(CreateWebKeyRequest) returns (CreateWebKeyResponse) { + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "iam.web_key.write" + } + }; + } + + // Activate Web Key + // + // Switch the active signing web key. The previously active key will be deactivated. + // Note that the JWKs OIDC endpoint returns a cacheable response. + // Therefore it is not advised to activate a key that has been created within the cache duration (default is 5min), + // as the public key may not have been propagated to caches and clients yet. + // + // Required permission: + // - `iam.web_key.write` + // + // Required feature flag: + // - `web_key` + rpc ActivateWebKey(ActivateWebKeyRequest) returns (ActivateWebKeyResponse) { + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "iam.web_key.write" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200" + value: { + description: "Web key activated successfully."; + } + }; + responses: { + key: "400" + value: { + description: "The feature flag `web_key` is not enabled."; + } + }; + responses: { + key: "404" + value: { + description: "The web key to active does not exist."; + } + }; + }; + } + + // Delete Web Key + // + // Delete a web key pair. Only inactive keys can be deleted. Once a key is deleted, + // any tokens signed by this key will be invalid. + // Note that the JWKs OIDC endpoint returns a cacheable response. + // In case the web key is not found, the request will return a successful response as + // the desired state is already achieved. + // You can check the change date in the response to verify if the web key was deleted during the request. + // + // Required permission: + // - `iam.web_key.delete` + // + // Required feature flag: + // - `web_key` + rpc DeleteWebKey(DeleteWebKeyRequest) returns (DeleteWebKeyResponse) { + option (google.api.http) = { + delete: "/v2/web_keys/{id}" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "iam.web_key.delete" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200" + value: { + description: "Web key deleted successfully."; + } + }; + responses: { + key: "400" + value: { + description: "The feature flag `web_key` is not enabled or the web key is currently active."; + } + }; + }; + } + + // List Web Keys + // + // List all web keys and their states. + // + // Required permission: + // - `iam.web_key.read` + // + // Required feature flag: + // - `web_key` + rpc ListWebKeys(ListWebKeysRequest) returns (ListWebKeysResponse) { + option (google.api.http) = { + get: "/v2/web_keys" + }; + + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "iam.web_key.read" + } + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + responses: { + key: "200" + value: { + description: "List of all web keys."; + } + }; + responses: { + key: "400" + value: { + description: "The feature flag `web_key` is not enabled."; + } + }; + }; + } +} + +message CreateWebKeyRequest { + // The key type to create (RSA, ECDSA, ED25519). + // If no key type is provided, a RSA key pair with 2048 bits and SHA256 hashing will be created. + oneof key { + // Create a RSA key pair and specify the bit size and hashing algorithm. + // If no bits and hasher are provided, a RSA key pair with 2048 bits and SHA256 hashing will be created. + RSA rsa = 1; + // Create a ECDSA key pair and specify the curve. + // If no curve is provided, a ECDSA key pair with P-256 curve will be created. + ECDSA ecdsa = 2; + // Create a ED25519 key pair. + ED25519 ed25519 = 3; + } + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { + example: "{\"rsa\":{\"bits\":\"RSA_BITS_2048\",\"hasher\":\"RSA_HASHER_SHA256\"}}"; + }; +} + +message CreateWebKeyResponse { + // The unique identifier of the newly created key. + string id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629012906488334\""; + } + ]; + // The timestamp of the key creation. + google.protobuf.Timestamp creation_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; +} + +message ActivateWebKeyRequest { + string id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"69629026806489455\""; + } + ]; +} + +message ActivateWebKeyResponse { + // The timestamp of the activation of the key. + google.protobuf.Timestamp change_date = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message DeleteWebKeyRequest { + string id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"69629026806489455\""; + } + ]; +} + +message DeleteWebKeyResponse { + // The timestamp of the deletion of the key. + // Note that the deletion date is only guaranteed to be set if the deletion was successful during the request. + // In case the deletion occurred in a previous request, the deletion date might be empty. + google.protobuf.Timestamp deletion_date = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message ListWebKeysRequest {} + +message ListWebKeysResponse { + repeated WebKey web_keys = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[{\"id\":\"69629012906488334\",\"creationDate\":\"2024-12-18T07:50:47.492Z\",\"changeDate\":\"2024-12-18T08:04:47.492Z\",\"state\":\"STATE_ACTIVE\",\"rsa\":{\"bits\":\"RSA_BITS_2048\",\"hasher\":\"RSA_HASHER_SHA256\"}},{\"id\":\"69629012909346200\",\"creationDate\":\"2025-01-18T12:05:47.492Z\",\"state\":\"STATE_INITIAL\",\"ecdsa\":{\"curve\":\"ECDSA_CURVE_P256\"}}]"; + } + ]; +} \ No newline at end of file diff --git a/turbo.json b/turbo.json new file mode 100644 index 0000000000..2dad100d23 --- /dev/null +++ b/turbo.json @@ -0,0 +1,51 @@ +{ + "$schema": "https://turbo.build/schema.json", + "ui": "tui", + "globalDependencies": ["**/.env.*local"], + "globalEnv": [ + "DEBUG", + "VERCEL_URL", + "EMAIL_VERIFICATION", + "AUDIENCE", + "SYSTEM_USER_ID", + "SYSTEM_USER_PRIVATE_KEY", + "ZITADEL_API_URL", + "ZITADEL_SERVICE_USER_TOKEN", + "NEXT_PUBLIC_BASE_PATH", + "CUSTOM_REQUEST_HEADERS", + "NODE_ENV", + "PORT", + "INKEEP_API_KEY", + "DISPLAY", + "CYPRESS_DISPLAY" + ], + "tasks": { + "generate": { + "cache": true + }, + "build": {}, + "build:login:standalone": {}, + "quality": { + "with": [ + "lint", + "test:unit", + "test:integration:login", + "test:acceptance", + "test:e2e" + ] + }, + "start": {}, + "test:unit": {}, + "test:acceptance": {}, + "test:e2e": {}, + "lint": {}, + "lint:fix": {}, + "dev": { + "cache": false, + "persistent": true + }, + "clean": { + "cache": false + } + } +}